bootstrap-x-editable-rails 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -15,4 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
- .DS_Store
18
+ .DS_Store
19
+ *.sublime-workspace
@@ -0,0 +1,13 @@
1
+ {
2
+ "folders":
3
+ [
4
+ {
5
+ "path": "."
6
+ }
7
+ ],
8
+ "settings":
9
+ {
10
+ "tab_size": 2,
11
+ "translate_tabs_to_spaces": true
12
+ }
13
+ }
@@ -1,5 +1,5 @@
1
1
  module BootstrapXEditableRails
2
2
  module Rails
3
- VERSION = "1.3.0"
3
+ VERSION = "1.4.0"
4
4
  end
5
5
  end
@@ -1,7 +1,7 @@
1
- /*! X-editable - v1.3.0
1
+ /*! X-editable - v1.4.0
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.
@@ -20,24 +20,16 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
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
41
33
  this.value = this.input.str2value(this.options.value);
42
34
  },
43
35
  initTemplate: function() {
@@ -52,47 +44,49 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
52
44
  @method render
53
45
  **/
54
46
  render: function() {
47
+ //init loader
55
48
  this.$loading = $($.fn.editableform.loading);
56
49
  this.$div.empty().append(this.$loading);
57
- this.showLoading();
58
50
 
59
51
  //init form template and buttons
60
- this.initTemplate();
52
+ this.initTemplate();
61
53
  if(this.options.showbuttons) {
62
54
  this.initButtons();
63
55
  } else {
64
56
  this.$form.find('.editable-buttons').remove();
65
57
  }
66
58
 
59
+ //show loading state
60
+ this.showLoading();
61
+
67
62
  /**
68
63
  Fired when rendering starts
69
64
  @event rendering
70
65
  @param {Object} event event object
71
66
  **/
72
67
  this.$div.triggerHandler('rendering');
68
+
69
+ //init input
70
+ this.initInput();
71
+
72
+ //append input to form
73
+ this.input.prerender();
74
+ this.$form.find('div.editable-input').append(this.input.$tpl);
73
75
 
76
+ //append form to container
77
+ this.$div.append(this.$form);
78
+
74
79
  //render input
75
80
  $.when(this.input.render())
76
81
  .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
82
+ //setup input to submit automatically when no buttons shown
81
83
  if(!this.options.showbuttons) {
82
84
  this.input.autosubmit();
83
85
  }
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
86
 
93
87
  //attach 'cancel' handler
94
88
  this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
95
-
89
+
96
90
  if(this.input.error) {
97
91
  this.error(this.input.error);
98
92
  this.$form.find('.editable-submit').attr('disabled', true);
@@ -116,6 +110,11 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
116
110
  this.$div.triggerHandler('rendered');
117
111
 
118
112
  this.showForm();
113
+
114
+ //call postrender method to perform actions required visibility of form
115
+ if(this.input.postrender) {
116
+ this.input.postrender();
117
+ }
119
118
  }, this));
120
119
  },
121
120
  cancel: function() {
@@ -127,11 +126,17 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
127
126
  this.$div.triggerHandler('cancel');
128
127
  },
129
128
  showLoading: function() {
130
- var w;
129
+ var w, h;
131
130
  if(this.$form) {
132
- //set loading size equal to form
133
- this.$loading.width(this.$form.outerWidth());
134
- this.$loading.height(this.$form.outerHeight());
131
+ //set loading size equal to form
132
+ w = this.$form.outerWidth();
133
+ h = this.$form.outerHeight();
134
+ if(w) {
135
+ this.$loading.width(w);
136
+ }
137
+ if(h) {
138
+ this.$loading.height(h);
139
+ }
135
140
  this.$form.hide();
136
141
  } else {
137
142
  //stretch loading to fill container width
@@ -159,14 +164,23 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
159
164
 
160
165
  error: function(msg) {
161
166
  var $group = this.$form.find('.control-group'),
162
- $block = this.$form.find('.editable-error-block');
167
+ $block = this.$form.find('.editable-error-block'),
168
+ lines;
163
169
 
164
170
  if(msg === false) {
165
171
  $group.removeClass($.fn.editableform.errorGroupClass);
166
172
  $block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
167
173
  } else {
174
+ //convert newline to <br> for more pretty error display
175
+ if(msg) {
176
+ lines = msg.split("\n");
177
+ for (var i = 0; i < lines.length; i++) {
178
+ lines[i] = $('<div>').text(lines[i]).html();
179
+ }
180
+ msg = lines.join('<br>');
181
+ }
168
182
  $group.addClass($.fn.editableform.errorGroupClass);
169
- $block.addClass($.fn.editableform.errorBlockClass).text(msg).show();
183
+ $block.addClass($.fn.editableform.errorBlockClass).html(msg).show();
170
184
  }
171
185
  },
172
186
 
@@ -299,10 +313,15 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
299
313
  },
300
314
 
301
315
  option: function(key, value) {
302
- this.options[key] = value;
316
+ if(key in this.options) {
317
+ this.options[key] = value;
318
+ }
319
+
303
320
  if(key === 'value') {
304
321
  this.setValue(value);
305
322
  }
323
+
324
+ //do not pass option to input as it is passed in editable-element
306
325
  },
307
326
 
308
327
  setValue: function(value, convertStr) {
@@ -311,6 +330,11 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
311
330
  } else {
312
331
  this.value = value;
313
332
  }
333
+
334
+ //if form is visible, update input
335
+ if(this.$form && this.$form.is(':visible')) {
336
+ this.input.value2input(this.value);
337
+ }
314
338
  }
315
339
  };
316
340
 
@@ -372,7 +396,7 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
372
396
  url: function(params) {
373
397
  if(params.value === 'abc') {
374
398
  var d = new $.Deferred;
375
- return d.reject('field cannot be "abc"'); //returning error via deferred object
399
+ return d.reject('error message'); //returning error via deferred object
376
400
  } else {
377
401
  someModel.set(params.name, params.value); //save data in some js model
378
402
  }
@@ -463,21 +487,21 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
463
487
  /**
464
488
  Additional options for ajax request.
465
489
  List of values: http://api.jquery.com/jQuery.ajax
466
-
490
+
467
491
  @property ajaxOptions
468
492
  @type object
469
493
  @default null
470
494
  @since 1.1.1
495
+ @example
496
+ ajaxOptions: {
497
+ type: 'put',
498
+ dataType: 'json'
499
+ }
471
500
  **/
472
501
  ajaxOptions: null,
473
502
  /**
474
503
  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
- }
504
+ Form without buttons is auto-submitted.
481
505
 
482
506
  @property showbuttons
483
507
  @type boolean
@@ -621,19 +645,22 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
621
645
  return newObj;
622
646
  },
623
647
 
624
- /**
625
- * exclude complex objects from $.data() before pass to config
648
+ /*
649
+ exclude complex objects from $.data() before pass to config
626
650
  */
627
651
  getConfigData: function($element) {
628
652
  var data = {};
629
653
  $.each($element.data(), function(k, v) {
630
- if(typeof v !== 'object' || (v && typeof v === 'object' && v.constructor === Object)) {
654
+ if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) {
631
655
  data[k] = v;
632
656
  }
633
657
  });
634
658
  return data;
635
659
  },
636
660
 
661
+ /*
662
+ returns keys of object
663
+ */
637
664
  objectKeys: function(o) {
638
665
  if (Object.keys) {
639
666
  return Object.keys(o);
@@ -657,9 +684,82 @@ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
657
684
  **/
658
685
  escape: function(str) {
659
686
  return $('<div>').text(str).html();
660
- }
687
+ },
688
+
689
+ /*
690
+ returns array items from sourceData having value property equal or inArray of 'value'
691
+ */
692
+ itemsByValue: function(value, sourceData) {
693
+ if(!sourceData || value === null) {
694
+ return [];
695
+ }
696
+
697
+ //convert to array
698
+ if(!$.isArray(value)) {
699
+ value = [value];
700
+ }
701
+
702
+ /*jslint eqeq: true*/
703
+ var result = $.grep(sourceData, function(o){
704
+ return $.grep(value, function(v){ return v == o.value; }).length;
705
+ });
706
+ /*jslint eqeq: false*/
707
+
708
+ return result;
709
+ },
710
+
711
+ /*
712
+ Returns input by options: type, mode.
713
+ */
714
+ createInput: function(options) {
715
+ var TypeConstructor, typeOptions, input,
716
+ type = options.type;
717
+
718
+ //`date` is some kind of virtual type that is transformed to one of exact types
719
+ //depending on mode and core lib
720
+ if(type === 'date') {
721
+ //inline
722
+ if(options.mode === 'inline') {
723
+ if($.fn.editabletypes.datefield) {
724
+ type = 'datefield';
725
+ } else if($.fn.editabletypes.dateuifield) {
726
+ type = 'dateuifield';
727
+ }
728
+ //popup
729
+ } else {
730
+ if($.fn.editabletypes.date) {
731
+ type = 'date';
732
+ } else if($.fn.editabletypes.dateui) {
733
+ type = 'dateui';
734
+ }
735
+ }
736
+
737
+ //if type still `date` and not exist in types, replace with `combodate` that is base input
738
+ if(type === 'date' && !$.fn.editabletypes.date) {
739
+ type = 'combodate';
740
+ }
741
+ }
742
+
743
+ //change wysihtml5 to textarea for jquery UI and plain versions
744
+ if(type === 'wysihtml5' && !$.fn.editabletypes[type]) {
745
+ type = 'textarea';
746
+ }
747
+
748
+ //create input of specified type. Input will be used for converting value, not in form
749
+ if(typeof $.fn.editabletypes[type] === 'function') {
750
+ TypeConstructor = $.fn.editabletypes[type];
751
+ typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults));
752
+ input = new TypeConstructor(typeOptions);
753
+ return input;
754
+ } else {
755
+ $.error('Unknown type: '+ type);
756
+ return false;
757
+ }
758
+ }
759
+
661
760
  };
662
761
  }(window.jQuery));
762
+
663
763
  /**
664
764
  Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
665
765
  This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
@@ -671,12 +771,16 @@ Applied as jQuery method.
671
771
  **/
672
772
  (function ($) {
673
773
 
674
- var EditableContainer = function (element, options) {
774
+ var Popup = function (element, options) {
675
775
  this.init(element, options);
676
776
  };
777
+
778
+ var Inline = function (element, options) {
779
+ this.init(element, options);
780
+ };
677
781
 
678
782
  //methods
679
- EditableContainer.prototype = {
783
+ Popup.prototype = {
680
784
  containerName: null, //tbd in child class
681
785
  innerCss: null, //tbd in child class
682
786
  init: function(element, options) {
@@ -684,6 +788,10 @@ Applied as jQuery method.
684
788
  //todo: what is in priority: data or js?
685
789
  this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options);
686
790
  this.splitOptions();
791
+
792
+ //set scope of form callbacks to element
793
+ this.formOptions.scope = this.$element[0];
794
+
687
795
  this.initContainer();
688
796
 
689
797
  //bind 'destroyed' listener to destroy container when element is removed from dom
@@ -691,7 +799,7 @@ Applied as jQuery method.
691
799
  this.destroy();
692
800
  }, this));
693
801
 
694
- //attach document handlers (once)
802
+ //attach document handler to close containers on click / escape
695
803
  if(!$(document).data('editable-handlers-attached')) {
696
804
  //close all on escape
697
805
  $(document).on('keyup.editable', function (e) {
@@ -703,15 +811,22 @@ Applied as jQuery method.
703
811
 
704
812
  //close containers when click outside
705
813
  $(document).on('click.editable', function(e) {
706
- var $target = $(e.target);
814
+ var $target = $(e.target), i,
815
+ exclude_classes = ['.editable-container',
816
+ '.ui-datepicker-header',
817
+ '.modal-backdrop',
818
+ '.bootstrap-wysihtml5-insert-image-modal',
819
+ '.bootstrap-wysihtml5-insert-link-modal'];
707
820
 
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);
821
+ //if click inside one of exclude classes --> no nothing
822
+ for(i=0; i<exclude_classes.length; i++) {
823
+ if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) {
824
+ return;
825
+ }
714
826
  }
827
+
828
+ //close all open containers (except one - target)
829
+ Popup.prototype.closeOthers(e.target);
715
830
  });
716
831
 
717
832
  $(document).data('editable-handlers-attached', true);
@@ -723,6 +838,7 @@ Applied as jQuery method.
723
838
  this.containerOptions = {};
724
839
  this.formOptions = {};
725
840
  var cDef = $.fn[this.containerName].defaults;
841
+ //keys defined in container defaults go to container, others go to form
726
842
  for(var k in this.options) {
727
843
  if(k in cDef) {
728
844
  this.containerOptions[k] = this.options[k];
@@ -732,18 +848,34 @@ Applied as jQuery method.
732
848
  }
733
849
  },
734
850
 
851
+ /*
852
+ Returns jquery object of container
853
+ @method tip()
854
+ */
855
+ tip: function() {
856
+ return this.container() ? this.container().$tip : null;
857
+ },
858
+
859
+ /* returns container object */
860
+ container: function() {
861
+ return this.$element.data(this.containerName);
862
+ },
863
+
864
+ call: function() {
865
+ this.$element[this.containerName].apply(this.$element, arguments);
866
+ },
867
+
735
868
  initContainer: function(){
736
869
  this.call(this.containerOptions);
737
870
  },
738
871
 
739
- initForm: function() {
740
- this.formOptions.scope = this.$element[0]; //set scope of form callbacks to element
741
- this.$form = $('<div>')
872
+ renderForm: function() {
873
+ this.$form
742
874
  .editableform(this.formOptions)
743
875
  .on({
744
- save: $.proxy(this.save, this),
745
- cancel: $.proxy(function(){ this.hide('cancel'); }, this),
746
- nochange: $.proxy(function(){ this.hide('nochange'); }, this),
876
+ save: $.proxy(this.save, this), //click on submit button (value changed)
877
+ nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
878
+ cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
747
879
  show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state)
748
880
  rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
749
881
  rendered: $.proxy(function(){
@@ -760,31 +892,16 @@ Applied as jQuery method.
760
892
  **/
761
893
  this.$element.triggerHandler('shown');
762
894
  }, this)
763
- });
764
- return this.$form;
895
+ })
896
+ .editableform('render');
765
897
  },
766
898
 
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
899
  /**
784
900
  Shows container with form
785
901
  @method show()
786
902
  @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
787
- **/
903
+ **/
904
+ /* Note: poshytip owerwrites this method totally! */
788
905
  show: function (closeAll) {
789
906
  this.$element.addClass('editable-open');
790
907
  if(closeAll !== false) {
@@ -792,16 +909,37 @@ Applied as jQuery method.
792
909
  this.closeOthers(this.$element[0]);
793
910
  }
794
911
 
912
+ //show container itself
795
913
  this.innerShow();
796
- },
797
-
798
- /* internal show method. To be overwritten in child classes */
799
- innerShow: function () {
800
- this.call('show');
801
914
  this.tip().addClass('editable-container');
802
- this.initForm();
803
- this.tip().find(this.innerCss).empty().append(this.$form);
804
- this.$form.editableform('render');
915
+
916
+ /*
917
+ Currently, form is re-rendered on every show.
918
+ The main reason is that we dont know, what container will do with content when closed:
919
+ remove(), detach() or just hide().
920
+
921
+ Detaching form itself before hide and re-insert before show is good solution,
922
+ but visually it looks ugly, as container changes size before hide.
923
+ */
924
+
925
+ //if form already exist - delete previous data
926
+ if(this.$form) {
927
+ //todo: destroy prev data!
928
+ //this.$form.destroy();
929
+ }
930
+
931
+ this.$form = $('<div>');
932
+
933
+ //insert form into container body
934
+ if(this.tip().is(this.innerCss)) {
935
+ //for inline container
936
+ this.tip().append(this.$form);
937
+ } else {
938
+ this.tip().find(this.innerCss).append(this.$form);
939
+ }
940
+
941
+ //render form
942
+ this.renderForm();
805
943
  },
806
944
 
807
945
  /**
@@ -813,8 +951,10 @@ Applied as jQuery method.
813
951
  if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
814
952
  return;
815
953
  }
954
+
816
955
  this.$element.removeClass('editable-open');
817
956
  this.innerHide();
957
+
818
958
  /**
819
959
  Fired when container was hidden. It occurs on both save or cancel.
820
960
 
@@ -832,9 +972,14 @@ Applied as jQuery method.
832
972
  this.$element.triggerHandler('hidden', reason);
833
973
  },
834
974
 
975
+ /* internal show method. To be overwritten in child classes */
976
+ innerShow: function () {
977
+
978
+ },
979
+
835
980
  /* internal hide method. To be overwritten in child classes */
836
981
  innerHide: function () {
837
- this.call('hide');
982
+
838
983
  },
839
984
 
840
985
  /**
@@ -843,7 +988,7 @@ Applied as jQuery method.
843
988
  @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
844
989
  **/
845
990
  toggle: function(closeAll) {
846
- if(this.tip && this.tip().is(':visible')) {
991
+ if(this.container() && this.tip() && this.tip().is(':visible')) {
847
992
  this.hide();
848
993
  } else {
849
994
  this.show(closeAll);
@@ -911,9 +1056,17 @@ Applied as jQuery method.
911
1056
  @method destroy()
912
1057
  **/
913
1058
  destroy: function() {
914
- this.call('destroy');
1059
+ this.hide();
1060
+ this.innerDestroy();
1061
+ this.$element.off('destroyed');
1062
+ this.$element.removeData('editableContainer');
915
1063
  },
916
1064
 
1065
+ /* to be overwritten in child classes */
1066
+ innerDestroy: function() {
1067
+
1068
+ },
1069
+
917
1070
  /*
918
1071
  Closes other containers except one related to passed element.
919
1072
  Other containers can be cancelled or submitted (depends on onblur option)
@@ -972,11 +1125,12 @@ Applied as jQuery method.
972
1125
  return this.each(function () {
973
1126
  var $this = $(this),
974
1127
  dataKey = 'editableContainer',
975
- data = $this.data(dataKey),
976
- options = typeof option === 'object' && option;
1128
+ data = $this.data(dataKey),
1129
+ options = typeof option === 'object' && option,
1130
+ Constructor = (options.mode === 'inline') ? Inline : Popup;
977
1131
 
978
1132
  if (!data) {
979
- $this.data(dataKey, (data = new EditableContainer(this, options)));
1133
+ $this.data(dataKey, (data = new Constructor(this, options)));
980
1134
  }
981
1135
 
982
1136
  if (typeof option === 'string') { //call method
@@ -985,8 +1139,9 @@ Applied as jQuery method.
985
1139
  });
986
1140
  };
987
1141
 
988
- //store constructor
989
- $.fn.editableContainer.Constructor = EditableContainer;
1142
+ //store constructors
1143
+ $.fn.editableContainer.Popup = Popup;
1144
+ $.fn.editableContainer.Inline = Inline;
990
1145
 
991
1146
  //defaults
992
1147
  $.fn.editableContainer.defaults = {
@@ -1025,7 +1180,25 @@ Applied as jQuery method.
1025
1180
  @default 'cancel'
1026
1181
  @since 1.1.1
1027
1182
  **/
1028
- onblur: 'cancel'
1183
+ onblur: 'cancel',
1184
+
1185
+ /**
1186
+ Animation speed (inline mode)
1187
+ @property anim
1188
+ @type string
1189
+ @default 'fast'
1190
+ **/
1191
+ anim: 'fast',
1192
+
1193
+ /**
1194
+ Mode of editable, can be `popup` or `inline`
1195
+
1196
+ @property mode
1197
+ @type string
1198
+ @default 'popup'
1199
+ @since 1.4.0
1200
+ **/
1201
+ mode: 'popup'
1029
1202
  };
1030
1203
 
1031
1204
  /*
@@ -1042,6 +1215,56 @@ Applied as jQuery method.
1042
1215
 
1043
1216
  }(window.jQuery));
1044
1217
 
1218
+ /**
1219
+ * Editable Inline
1220
+ * ---------------------
1221
+ */
1222
+ (function ($) {
1223
+
1224
+ //copy prototype from EditableContainer
1225
+ //extend methods
1226
+ $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, {
1227
+ containerName: 'editableform',
1228
+ innerCss: '.editable-inline',
1229
+
1230
+ initContainer: function(){
1231
+ //container is <span> element
1232
+ this.$tip = $('<span></span>').addClass('editable-inline');
1233
+
1234
+ //convert anim to miliseconds (int)
1235
+ if(!this.options.anim) {
1236
+ this.options.anim = 0;
1237
+ }
1238
+ },
1239
+
1240
+ splitOptions: function() {
1241
+ //all options are passed to form
1242
+ this.containerOptions = {};
1243
+ this.formOptions = this.options;
1244
+ },
1245
+
1246
+ tip: function() {
1247
+ return this.$tip;
1248
+ },
1249
+
1250
+ innerShow: function () {
1251
+ this.$element.hide();
1252
+ this.tip().insertAfter(this.$element).show();
1253
+ },
1254
+
1255
+ innerHide: function () {
1256
+ this.$tip.hide(this.options.anim, $.proxy(function() {
1257
+ this.$element.show();
1258
+ this.tip().empty().remove();
1259
+ }, this));
1260
+ },
1261
+
1262
+ innerDestroy: function() {
1263
+ this.tip().remove();
1264
+ }
1265
+ });
1266
+
1267
+ }(window.jQuery));
1045
1268
  /**
1046
1269
  Makes editable any HTML element on the page. Applied as jQuery method.
1047
1270
 
@@ -1059,27 +1282,15 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1059
1282
  Editable.prototype = {
1060
1283
  constructor: Editable,
1061
1284
  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
-
1285
+ var isValueByText = false,
1286
+ doAutotext, finalize;
1287
+
1073
1288
  //name
1074
1289
  this.options.name = this.options.name || this.$element.attr('id');
1075
1290
 
1076
1291
  //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);
1292
+ this.input = $.fn.editableutils.createInput(this.options);
1293
+ if(!this.input) {
1083
1294
  return;
1084
1295
  }
1085
1296
 
@@ -1140,8 +1351,12 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1140
1351
 
1141
1352
  @event init
1142
1353
  @param {Object} event event object
1143
- @param {Object} editable editable instance
1354
+ @param {Object} editable editable instance (as here it cannot accessed via data('editable'))
1144
1355
  @since 1.2.0
1356
+ @example
1357
+ $('#username').on('init', function(e, editable) {
1358
+ alert('initialized ' + editable.options.name);
1359
+ });
1145
1360
  **/
1146
1361
  this.$element.triggerHandler('init', this);
1147
1362
  }, this));
@@ -1152,18 +1367,20 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1152
1367
  Can call custom display method from options.
1153
1368
  Can return deferred object.
1154
1369
  @method render()
1370
+ @param {mixed} response server response (if exist) to pass into display function
1155
1371
  */
1156
- render: function() {
1372
+ render: function(response) {
1157
1373
  //do not display anything
1158
1374
  if(this.options.display === false) {
1159
1375
  return;
1160
1376
  }
1377
+
1161
1378
  //if it is input with source, we pass callback in third param to be called when source is loaded
1162
1379
  if(this.input.options.hasOwnProperty('source')) {
1163
- return this.input.value2html(this.value, this.$element[0], this.options.display);
1380
+ return this.input.value2html(this.value, this.$element[0], this.options.display, response);
1164
1381
  //if display method defined --> use it
1165
1382
  } else if(typeof this.options.display === 'function') {
1166
- return this.options.display.call(this.$element[0], this.value);
1383
+ return this.options.display.call(this.$element[0], this.value, response);
1167
1384
  //else use input's original value2html() method
1168
1385
  } else {
1169
1386
  return this.input.value2html(this.value, this.$element[0]);
@@ -1233,12 +1450,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1233
1450
 
1234
1451
  //disabled
1235
1452
  if(key === 'disabled') {
1236
- if(value) {
1237
- this.disable();
1238
- } else {
1239
- this.enable();
1240
- }
1241
- return;
1453
+ return value ? this.disable() : this.enable();
1242
1454
  }
1243
1455
 
1244
1456
  //value
@@ -1250,6 +1462,12 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1250
1462
  if(this.container) {
1251
1463
  this.container.option(key, value);
1252
1464
  }
1465
+
1466
+ //pass option to input directly (as it points to the same in form)
1467
+ if(this.input.option) {
1468
+ this.input.option(key, value);
1469
+ }
1470
+
1253
1471
  },
1254
1472
 
1255
1473
  /*
@@ -1291,7 +1509,8 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1291
1509
  //init editableContainer: popover, tooltip, inline, etc..
1292
1510
  if(!this.container) {
1293
1511
  var containerOptions = $.extend({}, this.options, {
1294
- value: this.value
1512
+ value: this.value,
1513
+ input: this.input //pass input to form (as it is already created)
1295
1514
  });
1296
1515
  this.$element.editableContainer(containerOptions);
1297
1516
  this.$element.on("save.internal", $.proxy(this.save, this));
@@ -1338,8 +1557,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1338
1557
  this.$element.removeClass('editable-unsaved');
1339
1558
  }
1340
1559
 
1341
- // this.hide();
1342
- this.setValue(params.newValue);
1560
+ this.setValue(params.newValue, false, params.response);
1343
1561
 
1344
1562
  /**
1345
1563
  Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
@@ -1351,13 +1569,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1351
1569
  @param {Object} params.response ajax response
1352
1570
  @example
1353
1571
  $('#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
- }
1572
+ alert('Saved value: ' + params.newValue);
1361
1573
  });
1362
1574
  **/
1363
1575
  //event itself is triggered by editableContainer. Description here is only for documentation
@@ -1375,7 +1587,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1375
1587
  @param {mixed} value new value
1376
1588
  @param {boolean} convertStr whether to convert value from string to internal format
1377
1589
  **/
1378
- setValue: function(value, convertStr) {
1590
+ setValue: function(value, convertStr, response) {
1379
1591
  if(convertStr) {
1380
1592
  this.value = this.input.str2value(value);
1381
1593
  } else {
@@ -1384,7 +1596,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1384
1596
  if(this.container) {
1385
1597
  this.container.option('value', this.value);
1386
1598
  }
1387
- $.when(this.render())
1599
+ $.when(this.render(response))
1388
1600
  .then($.proxy(function() {
1389
1601
  this.handleEmpty();
1390
1602
  }, this));
@@ -1398,7 +1610,28 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1398
1610
  if(this.container) {
1399
1611
  this.container.activate();
1400
1612
  }
1401
- }
1613
+ },
1614
+
1615
+ /**
1616
+ Removes editable feature from element
1617
+ @method destroy()
1618
+ **/
1619
+ destroy: function() {
1620
+ if(this.container) {
1621
+ this.container.destroy();
1622
+ }
1623
+
1624
+ if(this.options.toggle !== 'manual') {
1625
+ this.$element.removeClass('editable-click');
1626
+ this.$element.off(this.options.toggle + '.editable');
1627
+ }
1628
+
1629
+ this.$element.off("save.internal");
1630
+
1631
+ this.$element.removeClass('editable');
1632
+ this.$element.removeClass('editable-open');
1633
+ this.$element.removeData('editable');
1634
+ }
1402
1635
  };
1403
1636
 
1404
1637
  /* EDITABLE PLUGIN DEFINITION
@@ -1584,7 +1817,7 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1584
1817
  **/
1585
1818
  autotext: 'auto',
1586
1819
  /**
1587
- Initial value of input. Taken from <code>data-value</code> or element's text.
1820
+ Initial value of input. If not set, taken from element's text.
1588
1821
 
1589
1822
  @property value
1590
1823
  @type mixed
@@ -1593,10 +1826,21 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1593
1826
  value: null,
1594
1827
  /**
1595
1828
  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.
1829
+ If `null`, default input's display used.
1830
+ If `false`, no displaying methods will be called, element's text will never change.
1598
1831
  Runs under element's scope.
1599
- Second parameter __sourceData__ is passed for inputs with source (select, checklist).
1832
+ _Parameters:_
1833
+
1834
+ * `value` current value to be displayed
1835
+ * `response` server response (if display called after ajax submit), since 1.4.0
1836
+
1837
+ For **inputs with source** (select, checklist) parameters are different:
1838
+
1839
+ * `value` current value to be displayed
1840
+ * `sourceData` array of items for current input (e.g. dropdown items)
1841
+ * `response` server response (if display called after ajax submit), since 1.4.0
1842
+
1843
+ To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`.
1600
1844
 
1601
1845
  @property display
1602
1846
  @type function|boolean
@@ -1604,8 +1848,16 @@ Makes editable any HTML element on the page. Applied as jQuery method.
1604
1848
  @since 1.2.0
1605
1849
  @example
1606
1850
  display: function(value, sourceData) {
1607
- var escapedValue = $('<div>').text(value).html();
1608
- $(this).html('<b>'+escapedValue+'</b>');
1851
+ //display checklist as comma-separated values
1852
+ var html = [],
1853
+ checked = $.fn.editableutils.itemsByValue(value, sourceData);
1854
+
1855
+ if(checked.length) {
1856
+ $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
1857
+ $(this).html(html.join(', '));
1858
+ } else {
1859
+ $(this).empty();
1860
+ }
1609
1861
  }
1610
1862
  **/
1611
1863
  display: null
@@ -1635,26 +1887,27 @@ To create your own input you can inherit from this class.
1635
1887
  **/
1636
1888
  init: function(type, options, defaults) {
1637
1889
  this.type = type;
1638
- this.options = $.extend({}, defaults, options);
1639
- this.$input = null;
1640
- this.$clear = null;
1641
- this.error = null;
1890
+ this.options = $.extend({}, defaults, options);
1891
+ },
1892
+
1893
+ /*
1894
+ this method called before render to init $tpl that is inserted in DOM
1895
+ */
1896
+ prerender: function() {
1897
+ this.$tpl = $(this.options.tpl); //whole tpl as jquery object
1898
+ this.$input = this.$tpl; //control itself, can be changed in render method
1899
+ this.$clear = null; //clear button
1900
+ this.error = null; //error message, if input cannot be rendered
1642
1901
  },
1643
1902
 
1644
1903
  /**
1645
1904
  Renders input from tpl. Can return jQuery deferred object.
1905
+ Can be overwritten in child objects
1646
1906
 
1647
1907
  @method render()
1648
1908
  **/
1649
1909
  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
- }
1910
+
1658
1911
  },
1659
1912
 
1660
1913
  /**
@@ -1691,7 +1944,7 @@ To create your own input you can inherit from this class.
1691
1944
  },
1692
1945
 
1693
1946
  /**
1694
- Converts string received from server into value.
1947
+ Converts string received from server into value. Usually from `data-value` attribute.
1695
1948
 
1696
1949
  @method str2value(str)
1697
1950
  @param {string} str
@@ -1702,7 +1955,7 @@ To create your own input you can inherit from this class.
1702
1955
  },
1703
1956
 
1704
1957
  /**
1705
- Converts value for submitting to server
1958
+ Converts value for submitting to server. Result can be string or object.
1706
1959
 
1707
1960
  @method value2submit(value)
1708
1961
  @param {mixed} value
@@ -1763,7 +2016,25 @@ To create your own input you can inherit from this class.
1763
2016
  **/
1764
2017
  autosubmit: function() {
1765
2018
 
2019
+ },
2020
+
2021
+ // -------- helper functions --------
2022
+ setClass: function() {
2023
+ if(this.options.inputclass) {
2024
+ this.$input.addClass(this.options.inputclass);
2025
+ }
2026
+ },
2027
+
2028
+ setAttr: function(attr) {
2029
+ if (this.options[attr]) {
2030
+ this.$input.attr(attr, this.options[attr]);
2031
+ }
2032
+ },
2033
+
2034
+ option: function(key, value) {
2035
+ this.options[key] = value;
1766
2036
  }
2037
+
1767
2038
  };
1768
2039
 
1769
2040
  AbstractInput.defaults = {
@@ -1813,11 +2084,9 @@ List - abstract class for inputs that have source option loaded from js array or
1813
2084
 
1814
2085
  $.extend(List.prototype, {
1815
2086
  render: function () {
1816
- List.superclass.render.call(this);
1817
2087
  var deferred = $.Deferred();
2088
+
1818
2089
  this.error = null;
1819
- this.sourceData = null;
1820
- this.prependData = null;
1821
2090
  this.onSourceReady(function () {
1822
2091
  this.renderList();
1823
2092
  deferred.resolve();
@@ -1833,20 +2102,24 @@ List - abstract class for inputs that have source option loaded from js array or
1833
2102
  return null; //can't set value by text
1834
2103
  },
1835
2104
 
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
- });
2105
+ value2html: function (value, element, display, response) {
2106
+ var deferred = $.Deferred(),
2107
+ success = function () {
2108
+ if(typeof display === 'function') {
2109
+ //custom display method
2110
+ display.call(element, value, this.sourceData, response);
2111
+ } else {
2112
+ this.value2htmlFinal(value, element);
2113
+ }
2114
+ deferred.resolve();
2115
+ };
2116
+
2117
+ //for null value just call success without loading source
2118
+ if(value === null) {
2119
+ success.call(this);
2120
+ } else {
2121
+ this.onSourceReady(success, function () { deferred.resolve(); });
2122
+ }
1850
2123
 
1851
2124
  return deferred.promise();
1852
2125
  },
@@ -1872,7 +2145,7 @@ List - abstract class for inputs that have source option loaded from js array or
1872
2145
  if (typeof this.options.source === 'string') {
1873
2146
  //try to get from cache
1874
2147
  if(this.options.sourceCache) {
1875
- var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''),
2148
+ var cacheID = this.options.source,
1876
2149
  cache;
1877
2150
 
1878
2151
  if (!$(document).data(cacheID)) {
@@ -1883,11 +2156,13 @@ List - abstract class for inputs that have source option loaded from js array or
1883
2156
  //check for cached data
1884
2157
  if (cache.loading === false && cache.sourceData) { //take source from cache
1885
2158
  this.sourceData = cache.sourceData;
2159
+ this.doPrepend();
1886
2160
  success.call(this);
1887
2161
  return;
1888
2162
  } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
1889
2163
  cache.callbacks.push($.proxy(function () {
1890
2164
  this.sourceData = cache.sourceData;
2165
+ this.doPrepend();
1891
2166
  success.call(this);
1892
2167
  }, this));
1893
2168
 
@@ -1906,7 +2181,6 @@ List - abstract class for inputs that have source option loaded from js array or
1906
2181
  url: this.options.source,
1907
2182
  type: 'get',
1908
2183
  cache: false,
1909
- data: this.options.name ? {name: this.options.name} : {},
1910
2184
  dataType: 'json',
1911
2185
  success: $.proxy(function (data) {
1912
2186
  if(cache) {
@@ -1914,17 +2188,19 @@ List - abstract class for inputs that have source option loaded from js array or
1914
2188
  }
1915
2189
  this.sourceData = this.makeArray(data);
1916
2190
  if($.isArray(this.sourceData)) {
1917
- this.doPrepend();
1918
- success.call(this);
1919
2191
  if(cache) {
1920
2192
  //store result in cache
1921
2193
  cache.sourceData = this.sourceData;
1922
- $.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields
2194
+ //run success callbacks for other fields waiting for this source
2195
+ $.each(cache.callbacks, function () { this.call(); });
1923
2196
  }
2197
+ this.doPrepend();
2198
+ success.call(this);
1924
2199
  } else {
1925
2200
  error.call(this);
1926
2201
  if(cache) {
1927
- $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
2202
+ //run error callbacks for other fields waiting for this source
2203
+ $.each(cache.err_callbacks, function () { this.call(); });
1928
2204
  }
1929
2205
  }
1930
2206
  }, this),
@@ -1937,8 +2213,13 @@ List - abstract class for inputs that have source option loaded from js array or
1937
2213
  }
1938
2214
  }, this)
1939
2215
  });
1940
- } else { //options as json/array
1941
- this.sourceData = this.makeArray(this.options.source);
2216
+ } else { //options as json/array/function
2217
+ if (typeof this.options.source === 'function') {
2218
+ this.sourceData = this.makeArray(this.options.source());
2219
+ } else {
2220
+ this.sourceData = this.makeArray(this.options.source);
2221
+ }
2222
+
1942
2223
  if($.isArray(this.sourceData)) {
1943
2224
  this.doPrepend();
1944
2225
  success.call(this);
@@ -1959,7 +2240,11 @@ List - abstract class for inputs that have source option loaded from js array or
1959
2240
  if (typeof this.options.prepend === 'string') {
1960
2241
  this.options.prepend = {'': this.options.prepend};
1961
2242
  }
1962
- this.prependData = this.makeArray(this.options.prepend);
2243
+ if (typeof this.options.prepend === 'function') {
2244
+ this.prependData = this.makeArray(this.options.prepend());
2245
+ } else {
2246
+ this.prependData = this.makeArray(this.options.prepend);
2247
+ }
1963
2248
  }
1964
2249
 
1965
2250
  if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
@@ -2021,41 +2306,41 @@ List - abstract class for inputs that have source option loaded from js array or
2021
2306
  return result;
2022
2307
  },
2023
2308
 
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
- }
2309
+ option: function(key, value) {
2310
+ this.options[key] = value;
2311
+ if(key === 'source') {
2312
+ this.sourceData = null;
2034
2313
  }
2314
+ if(key === 'prepend') {
2315
+ this.prependData = null;
2316
+ }
2035
2317
  }
2036
2318
 
2037
2319
  });
2038
2320
 
2039
2321
  List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2040
2322
  /**
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.
2323
+ Source data for list.
2324
+ If **array** - it should be in format: `[{value: 1, text: "text1"}, {...}]`
2325
+ For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
2045
2326
 
2327
+ 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.
2328
+
2329
+ If **function**, it should return data in format above (since 1.4.0).
2330
+
2046
2331
  @property source
2047
- @type string|array|object
2332
+ @type string | array | object | function
2048
2333
  @default null
2049
2334
  **/
2050
- source:null,
2335
+ source: null,
2051
2336
  /**
2052
2337
  Data automatically prepended to the beginning of dropdown list.
2053
2338
 
2054
2339
  @property prepend
2055
- @type string|array|object
2340
+ @type string | array | object | function
2056
2341
  @default false
2057
2342
  **/
2058
- prepend:false,
2343
+ prepend: false,
2059
2344
  /**
2060
2345
  Error message when list cannot be loaded (e.g. ajax error)
2061
2346
 
@@ -2079,6 +2364,7 @@ List - abstract class for inputs that have source option loaded from js array or
2079
2364
  $.fn.editabletypes.list = List;
2080
2365
 
2081
2366
  }(window.jQuery));
2367
+
2082
2368
  /**
2083
2369
  Text input
2084
2370
 
@@ -2104,11 +2390,64 @@ $(function(){
2104
2390
  $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
2105
2391
 
2106
2392
  $.extend(Text.prototype, {
2107
- activate: function() {
2108
- if(this.$input.is(':visible')) {
2393
+ render: function() {
2394
+ this.renderClear();
2395
+ this.setClass();
2396
+ this.setAttr('placeholder');
2397
+ },
2398
+
2399
+ activate: function() {
2400
+ if(this.$input.is(':visible')) {
2109
2401
  this.$input.focus();
2110
2402
  $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2403
+ if(this.toggleClear) {
2404
+ this.toggleClear();
2405
+ }
2406
+ }
2407
+ },
2408
+
2409
+ //render clear button
2410
+ renderClear: function() {
2411
+ if (this.options.clear) {
2412
+ this.$clear = $('<span class="editable-clear-x"></span>');
2413
+ this.$input.after(this.$clear)
2414
+ .css('padding-right', 20)
2415
+ .keyup($.proxy(this.toggleClear, this))
2416
+ .parent().css('position', 'relative');
2417
+
2418
+ this.$clear.click($.proxy(function(){
2419
+ this.$clear.hide();
2420
+ this.$input.val('').focus();
2421
+ }, this));
2422
+ }
2423
+ },
2424
+
2425
+ postrender: function() {
2426
+ if(this.$clear) {
2427
+ //can position clear button only here, when form is shown and height can be calculated
2428
+ var h = this.$input.outerHeight() || 20,
2429
+ delta = (h - this.$clear.height()) / 2;
2430
+
2431
+ //workaround for plain-popup
2432
+ if(delta < 3) {
2433
+ delta = 3;
2434
+ }
2435
+
2436
+ this.$clear.css({top: delta, right: delta});
2437
+ }
2438
+ },
2439
+
2440
+ //show / hide clear button
2441
+ toggleClear: function() {
2442
+ if(!this.$clear) {
2443
+ return;
2111
2444
  }
2445
+
2446
+ if(this.$input.val()) {
2447
+ this.$clear.show();
2448
+ } else {
2449
+ this.$clear.hide();
2450
+ }
2112
2451
  }
2113
2452
  });
2114
2453
 
@@ -2125,7 +2464,16 @@ $(function(){
2125
2464
  @type string
2126
2465
  @default null
2127
2466
  **/
2128
- placeholder: null
2467
+ placeholder: null,
2468
+
2469
+ /**
2470
+ Whether to show `clear` button
2471
+
2472
+ @property clear
2473
+ @type boolean
2474
+ @default true
2475
+ **/
2476
+ clear: true
2129
2477
  });
2130
2478
 
2131
2479
  $.fn.editabletypes.text = Text;
@@ -2144,7 +2492,8 @@ Textarea input
2144
2492
  $(function(){
2145
2493
  $('#comments').editable({
2146
2494
  url: '/post',
2147
- title: 'Enter comments'
2495
+ title: 'Enter comments',
2496
+ rows: 10
2148
2497
  });
2149
2498
  });
2150
2499
  </script>
@@ -2159,8 +2508,10 @@ $(function(){
2159
2508
 
2160
2509
  $.extend(Textarea.prototype, {
2161
2510
  render: function () {
2162
- Textarea.superclass.render.call(this);
2163
-
2511
+ this.setClass();
2512
+ this.setAttr('placeholder');
2513
+ this.setAttr('rows');
2514
+
2164
2515
  //ctrl + enter
2165
2516
  this.$input.keydown(function (e) {
2166
2517
  if (e.ctrlKey && e.which === 13) {
@@ -2185,43 +2536,56 @@ $(function(){
2185
2536
  if(!html) {
2186
2537
  return '';
2187
2538
  }
2539
+
2540
+ var regex = new RegExp(String.fromCharCode(10), 'g');
2188
2541
  var lines = html.split(/<br\s*\/?>/i);
2189
2542
  for (var i = 0; i < lines.length; i++) {
2190
- lines[i] = $('<div>').html(lines[i]).text();
2543
+ var text = $('<div>').html(lines[i]).text();
2544
+
2545
+ // Remove newline characters (\n) to avoid them being converted by value2html() method
2546
+ // thus adding extra <br> tags
2547
+ text = text.replace(regex, '');
2548
+
2549
+ lines[i] = text;
2191
2550
  }
2192
- return lines.join("\n");
2193
- },
2551
+ return lines.join("\n");
2552
+ },
2194
2553
 
2195
2554
  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
- }
2555
+ $.fn.editabletypes.text.prototype.activate.call(this);
2556
+ }
2201
2557
  });
2202
2558
 
2203
2559
  Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2204
2560
  /**
2205
- @property tpl
2561
+ @property tpl
2206
2562
  @default <textarea></textarea>
2207
- **/
2563
+ **/
2208
2564
  tpl:'<textarea></textarea>',
2209
2565
  /**
2210
- @property inputclass
2566
+ @property inputclass
2211
2567
  @default input-large
2212
- **/
2568
+ **/
2213
2569
  inputclass: 'input-large',
2214
2570
  /**
2215
2571
  Placeholder attribute of input. Shown when input is empty.
2216
2572
 
2217
- @property placeholder
2573
+ @property placeholder
2218
2574
  @type string
2219
2575
  @default null
2220
- **/
2221
- placeholder: null
2576
+ **/
2577
+ placeholder: null,
2578
+ /**
2579
+ Number of rows in textarea
2580
+
2581
+ @property rows
2582
+ @type integer
2583
+ @default 7
2584
+ **/
2585
+ rows: 7
2222
2586
  });
2223
2587
 
2224
- $.fn.editabletypes.textarea = Textarea;
2588
+ $.fn.editabletypes.textarea = Textarea;
2225
2589
 
2226
2590
  }(window.jQuery));
2227
2591
 
@@ -2257,6 +2621,8 @@ $(function(){
2257
2621
 
2258
2622
  $.extend(Select.prototype, {
2259
2623
  renderList: function() {
2624
+ this.$input.empty();
2625
+
2260
2626
  if(!$.isArray(this.sourceData)) {
2261
2627
  return;
2262
2628
  }
@@ -2265,6 +2631,8 @@ $(function(){
2265
2631
  this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text));
2266
2632
  }
2267
2633
 
2634
+ this.setClass();
2635
+
2268
2636
  //enter submit
2269
2637
  this.$input.on('keydown.editable', function (e) {
2270
2638
  if (e.which === 13) {
@@ -2274,11 +2642,14 @@ $(function(){
2274
2642
  },
2275
2643
 
2276
2644
  value2htmlFinal: function(value, element) {
2277
- var text = '', item = this.itemByVal(value);
2278
- if(item) {
2279
- text = item.text;
2645
+ var text = '',
2646
+ items = $.fn.editableutils.itemsByValue(value, this.sourceData);
2647
+
2648
+ if(items.length) {
2649
+ text = items[0].text;
2280
2650
  }
2281
- Select.superclass.constructor.superclass.value2html(text, element);
2651
+
2652
+ $(element).text(text);
2282
2653
  },
2283
2654
 
2284
2655
  autosubmit: function() {
@@ -2333,6 +2704,9 @@ $(function(){
2333
2704
  $.extend(Checklist.prototype, {
2334
2705
  renderList: function() {
2335
2706
  var $label, $div;
2707
+
2708
+ this.$tpl.empty();
2709
+
2336
2710
  if(!$.isArray(this.sourceData)) {
2337
2711
  return;
2338
2712
  }
@@ -2345,8 +2719,11 @@ $(function(){
2345
2719
  }))
2346
2720
  .append($('<span>').text(' '+this.sourceData[i].text));
2347
2721
 
2348
- $('<div>').append($label).appendTo(this.$input);
2722
+ $('<div>').append($label).appendTo(this.$tpl);
2349
2723
  }
2724
+
2725
+ this.$input = this.$tpl.find('input[type="checkbox"]');
2726
+ this.setClass();
2350
2727
  },
2351
2728
 
2352
2729
  value2str: function(value) {
@@ -2367,10 +2744,9 @@ $(function(){
2367
2744
 
2368
2745
  //set checked on required checkboxes
2369
2746
  value2input: function(value) {
2370
- var $checks = this.$input.find('input[type="checkbox"]');
2371
- $checks.removeAttr('checked');
2747
+ this.$input.removeAttr('checked');
2372
2748
  if($.isArray(value) && value.length) {
2373
- $checks.each(function(i, el) {
2749
+ this.$input.each(function(i, el) {
2374
2750
  var $el = $(el);
2375
2751
  // cannot use $.inArray as it performs strict comparison
2376
2752
  $.each(value, function(j, val){
@@ -2386,7 +2762,7 @@ $(function(){
2386
2762
 
2387
2763
  input2value: function() {
2388
2764
  var checked = [];
2389
- this.$input.find('input:checked').each(function(i, el) {
2765
+ this.$input.filter(':checked').each(function(i, el) {
2390
2766
  checked.push($(el).val());
2391
2767
  });
2392
2768
  return checked;
@@ -2395,11 +2771,8 @@ $(function(){
2395
2771
  //collect text of checked boxes
2396
2772
  value2htmlFinal: function(value, element) {
2397
2773
  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*/
2774
+ checked = $.fn.editableutils.itemsByValue(value, this.sourceData);
2775
+
2403
2776
  if(checked.length) {
2404
2777
  $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
2405
2778
  $(element).html(html.join('<br>'));
@@ -2409,11 +2782,11 @@ $(function(){
2409
2782
  },
2410
2783
 
2411
2784
  activate: function() {
2412
- this.$input.find('input[type="checkbox"]').first().focus();
2785
+ this.$input.first().focus();
2413
2786
  },
2414
2787
 
2415
2788
  autosubmit: function() {
2416
- this.$input.find('input[type="checkbox"]').on('keydown', function(e){
2789
+ this.$input.on('keydown', function(e){
2417
2790
  if (e.which === 13) {
2418
2791
  $(this).closest('form').submit();
2419
2792
  }
@@ -2426,21 +2799,21 @@ $(function(){
2426
2799
  @property tpl
2427
2800
  @default <div></div>
2428
2801
  **/
2429
- tpl:'<div></div>',
2802
+ tpl:'<div class="editable-checklist"></div>',
2430
2803
 
2431
2804
  /**
2432
2805
  @property inputclass
2433
2806
  @type string
2434
- @default editable-checklist
2807
+ @default null
2435
2808
  **/
2436
- inputclass: 'editable-checklist',
2809
+ inputclass: null,
2437
2810
 
2438
2811
  /**
2439
- Separator of values when reading from 'data-value' string
2812
+ Separator of values when reading from `data-value` attribute
2440
2813
 
2441
2814
  @property separator
2442
2815
  @type string
2443
- @default ', '
2816
+ @default ','
2444
2817
  **/
2445
2818
  separator: ','
2446
2819
  });
@@ -2571,18 +2944,9 @@ Number
2571
2944
  $.extend(NumberInput.prototype, {
2572
2945
  render: function () {
2573
2946
  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
- }
2947
+ this.setAttr('min');
2948
+ this.setAttr('max');
2949
+ this.setAttr('step');
2586
2950
  }
2587
2951
  });
2588
2952
  NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
@@ -2606,29 +2970,19 @@ Range (inherit from number)
2606
2970
  $.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
2607
2971
  $.extend(Range.prototype, {
2608
2972
  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
- }
2973
+ this.$input = this.$tpl.filter('input');
2621
2974
 
2622
- if (this.options.step !== null) {
2623
- $slider.attr('step', this.options.step);
2624
- }
2975
+ this.setClass();
2976
+ this.setAttr('min');
2977
+ this.setAttr('max');
2978
+ this.setAttr('step');
2625
2979
 
2626
- $slider.on('input', function(){
2980
+ this.$input.on('input', function(){
2627
2981
  $(this).siblings('output').text($(this).val());
2628
2982
  });
2629
2983
  },
2630
2984
  activate: function() {
2631
- this.$input.filter('input').focus();
2985
+ this.$input.focus();
2632
2986
  }
2633
2987
  });
2634
2988
  Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
@@ -2637,6 +2991,590 @@ Range (inherit from number)
2637
2991
  });
2638
2992
  $.fn.editabletypes.range = Range;
2639
2993
  }(window.jQuery));
2994
+ /**
2995
+ * Combodate - 1.0.1
2996
+ * Dropdown date and time picker.
2997
+ * Converts text input into dropdowns to pick day, month, year, hour, minute and second.
2998
+ * Uses momentjs as datetime library http://momentjs.com.
2999
+ * For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang
3000
+ *
3001
+ * Author: Vitaliy Potapov
3002
+ * Project page: http://github.com/vitalets/combodate
3003
+ * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License.
3004
+ **/
3005
+ (function ($) {
3006
+
3007
+ var Combodate = function (element, options) {
3008
+ this.$element = $(element);
3009
+ if(!this.$element.is('input')) {
3010
+ $.error('Combodate should be applied to INPUT element');
3011
+ return;
3012
+ }
3013
+ this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data());
3014
+ this.init();
3015
+ };
3016
+
3017
+ Combodate.prototype = {
3018
+ constructor: Combodate,
3019
+ init: function () {
3020
+ this.map = {
3021
+ //key regexp moment.method
3022
+ day: ['D', 'date'],
3023
+ month: ['M', 'month'],
3024
+ year: ['Y', 'year'],
3025
+ hour: ['[Hh]', 'hours'],
3026
+ minute: ['m', 'minutes'],
3027
+ second: ['s', 'seconds'],
3028
+ ampm: ['[Aa]', '']
3029
+ };
3030
+
3031
+ this.$widget = $('<span class="combodate"></span>').html(this.getTemplate());
3032
+
3033
+ this.initCombos();
3034
+
3035
+ //update original input on change
3036
+ this.$widget.on('change', 'select', $.proxy(function(){
3037
+ this.$element.val(this.getValue());
3038
+ }, this));
3039
+
3040
+ this.$widget.find('select').css('width', 'auto');
3041
+
3042
+ //hide original input and insert widget
3043
+ this.$element.hide().after(this.$widget);
3044
+
3045
+ //set initial value
3046
+ this.setValue(this.$element.val() || this.options.value);
3047
+ },
3048
+
3049
+ /*
3050
+ Replace tokens in template with <select> elements
3051
+ */
3052
+ getTemplate: function() {
3053
+ var tpl = this.options.template;
3054
+
3055
+ //first pass
3056
+ $.each(this.map, function(k, v) {
3057
+ v = v[0];
3058
+ var r = new RegExp(v+'+'),
3059
+ token = v.length > 1 ? v.substring(1, 2) : v;
3060
+
3061
+ tpl = tpl.replace(r, '{'+token+'}');
3062
+ });
3063
+
3064
+ //replace spaces with &nbsp;
3065
+ tpl = tpl.replace(/ /g, '&nbsp;');
3066
+
3067
+ //second pass
3068
+ $.each(this.map, function(k, v) {
3069
+ v = v[0];
3070
+ var token = v.length > 1 ? v.substring(1, 2) : v;
3071
+
3072
+ tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>');
3073
+ });
3074
+
3075
+ return tpl;
3076
+ },
3077
+
3078
+ /*
3079
+ Initialize combos that presents in template
3080
+ */
3081
+ initCombos: function() {
3082
+ var that = this;
3083
+ $.each(this.map, function(k, v) {
3084
+ var $c = that.$widget.find('.'+k), f, items;
3085
+ if($c.length) {
3086
+ that['$'+k] = $c; //set properties like this.$day, this.$month etc.
3087
+ f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays`
3088
+ items = that[f]();
3089
+ that['$'+k].html(that.renderItems(items));
3090
+ }
3091
+ });
3092
+ },
3093
+
3094
+ /*
3095
+ Initialize items of combos. Handles `firstItem` option
3096
+ */
3097
+ initItems: function(key) {
3098
+ var values = [];
3099
+ if(this.options.firstItem === 'name') {
3100
+ var header = typeof moment.relativeTime[key] === 'function' ? moment.relativeTime[key](1, true, key, false) : moment.relativeTime[key];
3101
+ //take last entry (see momentjs lang files structure)
3102
+ header = header.split(' ').reverse()[0];
3103
+ values.push(['', header]);
3104
+ } else if(this.options.firstItem === 'empty') {
3105
+ values.push(['', '']);
3106
+ }
3107
+ return values;
3108
+ },
3109
+
3110
+ /*
3111
+ render items to string of <option> tags
3112
+ */
3113
+ renderItems: function(items) {
3114
+ var str = [];
3115
+ for(var i=0; i<items.length; i++) {
3116
+ str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
3117
+ }
3118
+ return str.join("\n");
3119
+ },
3120
+
3121
+ /*
3122
+ fill day
3123
+ */
3124
+ fillDay: function() {
3125
+ var items = this.initItems('d'), name, i,
3126
+ twoDigit = this.options.template.indexOf('DD') !== -1;
3127
+
3128
+ for(i=1; i<=31; i++) {
3129
+ name = twoDigit ? this.leadZero(i) : i;
3130
+ items.push([i, name]);
3131
+ }
3132
+ return items;
3133
+ },
3134
+
3135
+ /*
3136
+ fill month
3137
+ */
3138
+ fillMonth: function() {
3139
+ var items = this.initItems('M'), name, i,
3140
+ longNames = this.options.template.indexOf('MMMM') !== -1,
3141
+ shortNames = this.options.template.indexOf('MMM') !== -1,
3142
+ twoDigit = this.options.template.indexOf('MM') !== -1;
3143
+
3144
+ for(i=0; i<=11; i++) {
3145
+ if(longNames) {
3146
+ name = moment.months[i];
3147
+ } else if(shortNames) {
3148
+ name = moment.monthsShort[i];
3149
+ } else if(twoDigit) {
3150
+ name = this.leadZero(i+1);
3151
+ } else {
3152
+ name = i+1;
3153
+ }
3154
+ items.push([i, name]);
3155
+ }
3156
+ return items;
3157
+ },
3158
+
3159
+ /*
3160
+ fill year
3161
+ */
3162
+ fillYear: function() {
3163
+ var items = this.initItems('y'), name, i,
3164
+ longNames = this.options.template.indexOf('YYYY') !== -1;
3165
+
3166
+ for(i=this.options.maxYear; i>=this.options.minYear; i--) {
3167
+ name = longNames ? i : (i+'').substring(2);
3168
+ items.push([i, name]);
3169
+ }
3170
+ return items;
3171
+ },
3172
+
3173
+ /*
3174
+ fill hour
3175
+ */
3176
+ fillHour: function() {
3177
+ var items = this.initItems('h'), name, i,
3178
+ h12 = this.options.template.indexOf('h') !== -1,
3179
+ h24 = this.options.template.indexOf('H') !== -1,
3180
+ twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1,
3181
+ max = h12 ? 12 : 23;
3182
+
3183
+ for(i=0; i<=max; i++) {
3184
+ name = twoDigit ? this.leadZero(i) : i;
3185
+ items.push([i, name]);
3186
+ }
3187
+ return items;
3188
+ },
3189
+
3190
+ /*
3191
+ fill minute
3192
+ */
3193
+ fillMinute: function() {
3194
+ var items = this.initItems('m'), name, i,
3195
+ twoDigit = this.options.template.indexOf('mm') !== -1;
3196
+
3197
+ for(i=0; i<=59; i+= this.options.minuteStep) {
3198
+ name = twoDigit ? this.leadZero(i) : i;
3199
+ items.push([i, name]);
3200
+ }
3201
+ return items;
3202
+ },
3203
+
3204
+ /*
3205
+ fill second
3206
+ */
3207
+ fillSecond: function() {
3208
+ var items = this.initItems('s'), name, i,
3209
+ twoDigit = this.options.template.indexOf('ss') !== -1;
3210
+
3211
+ for(i=0; i<=59; i+= this.options.secondStep) {
3212
+ name = twoDigit ? this.leadZero(i) : i;
3213
+ items.push([i, name]);
3214
+ }
3215
+ return items;
3216
+ },
3217
+
3218
+ /*
3219
+ fill ampm
3220
+ */
3221
+ fillAmpm: function() {
3222
+ var ampmL = this.options.template.indexOf('a') !== -1,
3223
+ ampmU = this.options.template.indexOf('A') !== -1,
3224
+ items = [
3225
+ ['am', ampmL ? 'am' : 'AM'],
3226
+ ['pm', ampmL ? 'pm' : 'PM']
3227
+ ];
3228
+ return items;
3229
+ },
3230
+
3231
+ /*
3232
+ Returns current date value.
3233
+ If format not specified - `options.format` used.
3234
+ If format = `null` - Moment object returned.
3235
+ */
3236
+ getValue: function(format) {
3237
+ var dt, values = {},
3238
+ that = this,
3239
+ notSelected = false;
3240
+
3241
+ //getting selected values
3242
+ $.each(this.map, function(k, v) {
3243
+ if(k === 'ampm') {
3244
+ return;
3245
+ }
3246
+ var def = k === 'day' ? 1 : 0;
3247
+
3248
+ values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def;
3249
+
3250
+ if(isNaN(values[k])) {
3251
+ notSelected = true;
3252
+ return false;
3253
+ }
3254
+ });
3255
+
3256
+ //if at least one visible combo not selected - return empty string
3257
+ if(notSelected) {
3258
+ return '';
3259
+ }
3260
+
3261
+ //convert hours if 12h format
3262
+ if(this.$ampm) {
3263
+ values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
3264
+ if(values.hour === 24) {
3265
+ values.hour = 0;
3266
+ }
3267
+ }
3268
+
3269
+ dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
3270
+
3271
+ //highlight invalid date
3272
+ this.highlight(dt);
3273
+
3274
+ format = format === undefined ? this.options.format : format;
3275
+ if(format === null) {
3276
+ return dt.isValid() ? dt : null;
3277
+ } else {
3278
+ return dt.isValid() ? dt.format(format) : '';
3279
+ }
3280
+ },
3281
+
3282
+ setValue: function(value) {
3283
+ if(!value) {
3284
+ return;
3285
+ }
3286
+
3287
+ var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value),
3288
+ that = this,
3289
+ values = {};
3290
+
3291
+ if(dt.isValid()) {
3292
+ //read values from date object
3293
+ $.each(this.map, function(k, v) {
3294
+ if(k === 'ampm') {
3295
+ return;
3296
+ }
3297
+ values[k] = dt[v[1]]();
3298
+ });
3299
+
3300
+ if(this.$ampm) {
3301
+ if(values.hour > 12) {
3302
+ values.hour -= 12;
3303
+ values.ampm = 'pm';
3304
+ } else {
3305
+ values.ampm = 'am';
3306
+ }
3307
+ }
3308
+
3309
+ $.each(values, function(k, v) {
3310
+ if(that['$'+k]) {
3311
+ that['$'+k].val(v);
3312
+ }
3313
+ });
3314
+
3315
+ this.$element.val(dt.format(this.options.format));
3316
+ }
3317
+ },
3318
+
3319
+ /*
3320
+ highlight combos if date is invalid
3321
+ */
3322
+ highlight: function(dt) {
3323
+ if(!dt.isValid()) {
3324
+ if(this.options.errorClass) {
3325
+ this.$widget.addClass(this.options.errorClass);
3326
+ } else {
3327
+ //store original border color
3328
+ if(!this.borderColor) {
3329
+ this.borderColor = this.$widget.find('select').css('border-color');
3330
+ }
3331
+ this.$widget.find('select').css('border-color', 'red');
3332
+ }
3333
+ } else {
3334
+ if(this.options.errorClass) {
3335
+ this.$widget.removeClass(this.options.errorClass);
3336
+ } else {
3337
+ this.$widget.find('select').css('border-color', this.borderColor);
3338
+ }
3339
+ }
3340
+ },
3341
+
3342
+ leadZero: function(v) {
3343
+ return v <= 9 ? '0' + v : v;
3344
+ },
3345
+
3346
+ destroy: function() {
3347
+ this.$widget.remove();
3348
+ this.$element.removeData('combodate').show();
3349
+ }
3350
+
3351
+ //todo: clear method
3352
+ };
3353
+
3354
+ $.fn.combodate = function ( option ) {
3355
+ var d, args = Array.apply(null, arguments);
3356
+ args.shift();
3357
+
3358
+ //getValue returns date as string / object (not jQuery object)
3359
+ if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) {
3360
+ return d.getValue.apply(d, args);
3361
+ }
3362
+
3363
+ return this.each(function () {
3364
+ var $this = $(this),
3365
+ data = $this.data('combodate'),
3366
+ options = typeof option == 'object' && option;
3367
+ if (!data) {
3368
+ $this.data('combodate', (data = new Combodate(this, options)));
3369
+ }
3370
+ if (typeof option == 'string' && typeof data[option] == 'function') {
3371
+ data[option].apply(data, args);
3372
+ }
3373
+ });
3374
+ };
3375
+
3376
+ $.fn.combodate.defaults = {
3377
+ //in this format value stored in original input
3378
+ format: 'DD-MM-YYYY HH:mm',
3379
+ //in this format items in dropdowns are displayed
3380
+ template: 'D / MMM / YYYY H : mm',
3381
+ //initial value, can be `new Date()`
3382
+ value: null,
3383
+ minYear: 1970,
3384
+ maxYear: 2015,
3385
+ minuteStep: 5,
3386
+ secondStep: 1,
3387
+ firstItem: 'empty', //'name', 'empty', 'none'
3388
+ errorClass: null
3389
+ };
3390
+
3391
+ }(window.jQuery));
3392
+ /**
3393
+ Combodate input - dropdown date and time picker.
3394
+ Based on [combodate](http://vitalets.github.com/combodate) plugin. To use it you should manually include [momentjs](http://momentjs.com).
3395
+
3396
+ <script src="js/moment.min.js"></script>
3397
+
3398
+ Allows to input:
3399
+
3400
+ * only date
3401
+ * only time
3402
+ * both date and time
3403
+
3404
+ Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker.
3405
+ Internally value stored as `momentjs` object.
3406
+
3407
+ @class combodate
3408
+ @extends abstractinput
3409
+ @final
3410
+ @since 1.4.0
3411
+ @example
3412
+ <a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a>
3413
+ <script>
3414
+ $(function(){
3415
+ $('#dob').editable({
3416
+ format: 'YYYY-MM-DD',
3417
+ viewformat: 'DD.MM.YYYY',
3418
+ template: 'D / MMMM / YYYY',
3419
+ combodate: {
3420
+ minYear: 2000,
3421
+ maxYear: 2015,
3422
+ minuteStep: 1
3423
+ }
3424
+ }
3425
+ });
3426
+ });
3427
+ </script>
3428
+ **/
3429
+
3430
+ /*global moment*/
3431
+
3432
+ (function ($) {
3433
+
3434
+ var Constructor = function (options) {
3435
+ this.init('combodate', options, Constructor.defaults);
3436
+
3437
+ //by default viewformat equals to format
3438
+ if(!this.options.viewformat) {
3439
+ this.options.viewformat = this.options.format;
3440
+ }
3441
+
3442
+ //overriding combodate config (as by default jQuery extend() is not recursive)
3443
+ this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, {
3444
+ format: this.options.format,
3445
+ template: this.options.template
3446
+ });
3447
+ };
3448
+
3449
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
3450
+
3451
+ $.extend(Constructor.prototype, {
3452
+ render: function () {
3453
+ this.$input.combodate(this.options.combodate);
3454
+
3455
+ //"clear" link
3456
+ /*
3457
+ if(this.options.clear) {
3458
+ this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
3459
+ e.preventDefault();
3460
+ e.stopPropagation();
3461
+ this.clear();
3462
+ }, this));
3463
+
3464
+ this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
3465
+ }
3466
+ */
3467
+ },
3468
+
3469
+ value2html: function(value, element) {
3470
+ var text = value ? value.format(this.options.viewformat) : '';
3471
+ $(element).text(text);
3472
+ },
3473
+
3474
+ html2value: function(html) {
3475
+ return html ? moment(html, this.options.viewformat) : null;
3476
+ },
3477
+
3478
+ value2str: function(value) {
3479
+ return value ? value.format(this.options.format) : '';
3480
+ },
3481
+
3482
+ str2value: function(str) {
3483
+ return str ? moment(str, this.options.format) : null;
3484
+ },
3485
+
3486
+ value2submit: function(value) {
3487
+ return this.value2str(value);
3488
+ },
3489
+
3490
+ value2input: function(value) {
3491
+ this.$input.combodate('setValue', value);
3492
+ },
3493
+
3494
+ input2value: function() {
3495
+ return this.$input.combodate('getValue', null);
3496
+ },
3497
+
3498
+ activate: function() {
3499
+ this.$input.siblings('.combodate').find('select').eq(0).focus();
3500
+ },
3501
+
3502
+ /*
3503
+ clear: function() {
3504
+ this.$input.data('datepicker').date = null;
3505
+ this.$input.find('.active').removeClass('active');
3506
+ },
3507
+ */
3508
+
3509
+ autosubmit: function() {
3510
+
3511
+ }
3512
+
3513
+ });
3514
+
3515
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3516
+ /**
3517
+ @property tpl
3518
+ @default <input type="text">
3519
+ **/
3520
+ tpl:'<input type="text">',
3521
+ /**
3522
+ @property inputclass
3523
+ @default null
3524
+ **/
3525
+ inputclass: null,
3526
+ /**
3527
+ Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
3528
+ See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format)
3529
+
3530
+ @property format
3531
+ @type string
3532
+ @default YYYY-MM-DD
3533
+ **/
3534
+ format:'YYYY-MM-DD',
3535
+ /**
3536
+ Format used for displaying date. Also applied when converting date from element's text on init.
3537
+ If not specified equals to `format`.
3538
+
3539
+ @property viewformat
3540
+ @type string
3541
+ @default null
3542
+ **/
3543
+ viewformat: null,
3544
+ /**
3545
+ Template used for displaying dropdowns.
3546
+
3547
+ @property template
3548
+ @type string
3549
+ @default D / MMM / YYYY
3550
+ **/
3551
+ template: 'D / MMM / YYYY',
3552
+ /**
3553
+ Configuration of combodate.
3554
+ Full list of options: http://vitalets.github.com/combodate/#docs
3555
+
3556
+ @property combodate
3557
+ @type object
3558
+ @default null
3559
+ **/
3560
+ combodate: null
3561
+
3562
+ /*
3563
+ (not implemented yet)
3564
+ Text shown as clear date button.
3565
+ If <code>false</code> clear button will not be rendered.
3566
+
3567
+ @property clear
3568
+ @type boolean|string
3569
+ @default 'x clear'
3570
+ */
3571
+ //clear: '&times; clear'
3572
+ });
3573
+
3574
+ $.fn.editabletypes.combodate = Constructor;
3575
+
3576
+ }(window.jQuery));
3577
+
2640
3578
  /*
2641
3579
  Editableform based on Twitter Bootstrap
2642
3580
  */
@@ -2666,7 +3604,7 @@ Editableform based on Twitter Bootstrap
2666
3604
  (function ($) {
2667
3605
 
2668
3606
  //extend methods
2669
- $.extend($.fn.editableContainer.Constructor.prototype, {
3607
+ $.extend($.fn.editableContainer.Popup.prototype, {
2670
3608
  containerName: 'popover',
2671
3609
  //for compatibility with bootstrap <= 2.2.1 (content inserted into <p> instead of directly .popover-content)
2672
3610
  innerCss: $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content',
@@ -2675,10 +3613,39 @@ Editableform based on Twitter Bootstrap
2675
3613
  $.extend(this.containerOptions, {
2676
3614
  trigger: 'manual',
2677
3615
  selector: false,
2678
- content: ' '
3616
+ content: ' ',
3617
+ template: $.fn.popover.defaults.template
2679
3618
  });
3619
+
3620
+ //as template property is used in inputs, hide it from popover
3621
+ var t;
3622
+ if(this.$element.data('template')) {
3623
+ t = this.$element.data('template');
3624
+ this.$element.removeData('template');
3625
+ }
3626
+
2680
3627
  this.call(this.containerOptions);
2681
- },
3628
+
3629
+ if(t) {
3630
+ //restore data('template')
3631
+ this.$element.data('template', t);
3632
+ }
3633
+ },
3634
+
3635
+ /* show */
3636
+ innerShow: function () {
3637
+ this.call('show');
3638
+ },
3639
+
3640
+ /* hide */
3641
+ innerHide: function () {
3642
+ this.call('hide');
3643
+ },
3644
+
3645
+ /* destroy */
3646
+ innerDestroy: function() {
3647
+ this.call('destroy');
3648
+ },
2682
3649
 
2683
3650
  setContainerOption: function(key, value) {
2684
3651
  this.container().options[key] = value;
@@ -2742,18 +3709,13 @@ Editableform based on Twitter Bootstrap
2742
3709
  }
2743
3710
  });
2744
3711
 
2745
- //defaults
2746
- /*
2747
- $.fn.editableContainer.defaults = $.extend({}, $.fn.popover.defaults, $.fn.editableContainer.defaults, {
2748
-
2749
- });
2750
- */
2751
-
2752
3712
  }(window.jQuery));
2753
3713
  /**
2754
3714
  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
3715
+ Description and examples: https://github.com/eternicode/bootstrap-datepicker.
3716
+ For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
3717
+ and set `language` option.
3718
+ Since 1.4.0 date has different appearance in **popup** and **inline** modes.
2757
3719
 
2758
3720
  @class date
2759
3721
  @extends abstractinput
@@ -2777,45 +3739,52 @@ $(function(){
2777
3739
 
2778
3740
  var Date = function (options) {
2779
3741
  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);
3742
+ this.initPicker(options, Date.defaults);
2801
3743
  };
2802
3744
 
2803
3745
  $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput);
2804
3746
 
2805
3747
  $.extend(Date.prototype, {
3748
+ initPicker: function(options, defaults) {
3749
+ //'format' is set directly from settings or data-* attributes
3750
+
3751
+ //by default viewformat equals to format
3752
+ if(!this.options.viewformat) {
3753
+ this.options.viewformat = this.options.format;
3754
+ }
3755
+
3756
+ //overriding datepicker config (as by default jQuery extend() is not recursive)
3757
+ //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only
3758
+ this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, {
3759
+ format: this.options.viewformat
3760
+ });
3761
+
3762
+ //language
3763
+ this.options.datepicker.language = this.options.datepicker.language || 'en';
3764
+
3765
+ //store DPglobal
3766
+ this.dpg = $.fn.datepicker.DPGlobal;
3767
+
3768
+ //store parsed formats
3769
+ this.parsedFormat = this.dpg.parseFormat(this.options.format);
3770
+ this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
3771
+ },
3772
+
2806
3773
  render: function () {
2807
- Date.superclass.render.call(this);
2808
3774
  this.$input.datepicker(this.options.datepicker);
2809
-
3775
+
3776
+ //"clear" link
2810
3777
  if(this.options.clear) {
2811
3778
  this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
2812
3779
  e.preventDefault();
2813
3780
  e.stopPropagation();
2814
3781
  this.clear();
2815
3782
  }, this));
2816
- }
3783
+
3784
+ this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
3785
+ }
2817
3786
  },
2818
-
3787
+
2819
3788
  value2html: function(value, element) {
2820
3789
  var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
2821
3790
  Date.superclass.value2html(text, element);
@@ -2869,12 +3838,12 @@ $(function(){
2869
3838
  @property tpl
2870
3839
  @default <div></div>
2871
3840
  **/
2872
- tpl:'<div></div>',
3841
+ tpl:'<div class="editable-date well"></div>',
2873
3842
  /**
2874
3843
  @property inputclass
2875
- @default editable-date well
3844
+ @default null
2876
3845
  **/
2877
- inputclass: 'editable-date well',
3846
+ inputclass: null,
2878
3847
  /**
2879
3848
  Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
2880
3849
  Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code>
@@ -2925,6 +3894,84 @@ $(function(){
2925
3894
 
2926
3895
  }(window.jQuery));
2927
3896
 
3897
+ /**
3898
+ Bootstrap datefield input - modification for inline mode.
3899
+ Shows normal <input type="text"> and binds popup datepicker.
3900
+ Automatically shown in inline mode.
3901
+
3902
+ @class datefield
3903
+ @extends date
3904
+
3905
+ @since 1.4.0
3906
+ **/
3907
+ (function ($) {
3908
+
3909
+ var DateField = function (options) {
3910
+ this.init('datefield', options, DateField.defaults);
3911
+ this.initPicker(options, DateField.defaults);
3912
+ };
3913
+
3914
+ $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date);
3915
+
3916
+ $.extend(DateField.prototype, {
3917
+ render: function () {
3918
+ this.$input = this.$tpl.find('input');
3919
+ this.setClass();
3920
+ this.setAttr('placeholder');
3921
+
3922
+ this.$tpl.datepicker(this.options.datepicker);
3923
+
3924
+ //need to disable original event handlers
3925
+ this.$input.off('focus keydown');
3926
+
3927
+ //update value of datepicker
3928
+ this.$input.keyup($.proxy(function(){
3929
+ this.$tpl.removeData('date');
3930
+ this.$tpl.datepicker('update');
3931
+ }, this));
3932
+
3933
+ },
3934
+
3935
+ value2input: function(value) {
3936
+ this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '');
3937
+ this.$tpl.datepicker('update');
3938
+ },
3939
+
3940
+ input2value: function() {
3941
+ return this.html2value(this.$input.val());
3942
+ },
3943
+
3944
+ activate: function() {
3945
+ $.fn.editabletypes.text.prototype.activate.call(this);
3946
+ },
3947
+
3948
+ autosubmit: function() {
3949
+ //reset autosubmit to empty
3950
+ }
3951
+ });
3952
+
3953
+ DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, {
3954
+ /**
3955
+ @property tpl
3956
+ **/
3957
+ tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',
3958
+ /**
3959
+ @property inputclass
3960
+ @default 'input-small'
3961
+ **/
3962
+ inputclass: 'input-small',
3963
+
3964
+ /* datepicker config */
3965
+ datepicker: {
3966
+ weekStart: 0,
3967
+ startView: 0,
3968
+ autoclose: true
3969
+ }
3970
+ });
3971
+
3972
+ $.fn.editabletypes.datefield = DateField;
3973
+
3974
+ }(window.jQuery));
2928
3975
  /* =========================================================
2929
3976
  * bootstrap-datepicker.js
2930
3977
  * http://www.eyecon.ro/bootstrap-datepicker
@@ -2963,51 +4010,45 @@ $(function(){
2963
4010
  this.element = $(element);
2964
4011
  this.language = options.language||this.element.data('date-language')||"en";
2965
4012
  this.language = this.language in dates ? this.language : "en";
4013
+ this.isRTL = dates[this.language].rtl||false;
2966
4014
  this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
2967
- this.isInline = false;
4015
+ this.isInline = false;
2968
4016
  this.isInput = this.element.is('input');
2969
4017
  this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
2970
4018
  this.hasInput = this.component && this.element.find('input').length;
2971
4019
  if(this.component && this.component.length === 0)
2972
4020
  this.component = false;
2973
4021
 
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
- }
4022
+ this._attachEvents();
3007
4023
 
4024
+ this.forceParse = true;
4025
+ if ('forceParse' in options) {
4026
+ this.forceParse = options.forceParse;
4027
+ } else if ('dateForceParse' in this.element.data()) {
4028
+ this.forceParse = this.element.data('date-force-parse');
4029
+ }
4030
+
4031
+
4032
+ this.picker = $(DPGlobal.template)
4033
+ .appendTo(this.isInline ? this.element : 'body')
4034
+ .on({
4035
+ click: $.proxy(this.click, this),
4036
+ mousedown: $.proxy(this.mousedown, this)
4037
+ });
4038
+
4039
+ if(this.isInline) {
4040
+ this.picker.addClass('datepicker-inline');
4041
+ } else {
4042
+ this.picker.addClass('datepicker-dropdown dropdown-menu');
4043
+ }
4044
+ if (this.isRTL){
4045
+ this.picker.addClass('datepicker-rtl');
4046
+ this.picker.find('.prev i, .next i')
4047
+ .toggleClass('icon-arrow-left icon-arrow-right');
4048
+ }
3008
4049
  $(document).on('mousedown', function (e) {
3009
4050
  // Clicked outside the datepicker, hide it
3010
- if ($(e.target).closest('.datepicker').length == 0) {
4051
+ if ($(e.target).closest('.datepicker').length === 0) {
3011
4052
  that.hide();
3012
4053
  }
3013
4054
  });
@@ -3026,6 +4067,7 @@ $(function(){
3026
4067
  this.keyboardNavigation = this.element.data('date-keyboard-navigation');
3027
4068
  }
3028
4069
 
4070
+ this.viewMode = this.startViewMode = 0;
3029
4071
  switch(options.startView || this.element.data('date-start-view')){
3030
4072
  case 2:
3031
4073
  case 'decade':
@@ -3035,11 +4077,6 @@ $(function(){
3035
4077
  case 'year':
3036
4078
  this.viewMode = this.startViewMode = 1;
3037
4079
  break;
3038
- case 0:
3039
- case 'month':
3040
- default:
3041
- this.viewMode = this.startViewMode = 0;
3042
- break;
3043
4080
  }
3044
4081
 
3045
4082
  this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
@@ -3049,21 +4086,73 @@ $(function(){
3049
4086
  this.weekEnd = ((this.weekStart + 6) % 7);
3050
4087
  this.startDate = -Infinity;
3051
4088
  this.endDate = Infinity;
4089
+ this.daysOfWeekDisabled = [];
3052
4090
  this.setStartDate(options.startDate||this.element.data('date-startdate'));
3053
4091
  this.setEndDate(options.endDate||this.element.data('date-enddate'));
4092
+ this.setDaysOfWeekDisabled(options.daysOfWeekDisabled||this.element.data('date-days-of-week-disabled'));
3054
4093
  this.fillDow();
3055
4094
  this.fillMonths();
3056
4095
  this.update();
3057
4096
  this.showMode();
3058
4097
 
3059
- if(this.isInline) {
3060
- this.show();
3061
- }
4098
+ if(this.isInline) {
4099
+ this.show();
4100
+ }
3062
4101
  };
3063
4102
 
3064
4103
  Datepicker.prototype = {
3065
4104
  constructor: Datepicker,
3066
4105
 
4106
+ _events: [],
4107
+ _attachEvents: function(){
4108
+ this._detachEvents();
4109
+ if (this.isInput) { // single input
4110
+ this._events = [
4111
+ [this.element, {
4112
+ focus: $.proxy(this.show, this),
4113
+ keyup: $.proxy(this.update, this),
4114
+ keydown: $.proxy(this.keydown, this)
4115
+ }]
4116
+ ];
4117
+ }
4118
+ else if (this.component && this.hasInput){ // component: input + button
4119
+ this._events = [
4120
+ // For components that are not readonly, allow keyboard nav
4121
+ [this.element.find('input'), {
4122
+ focus: $.proxy(this.show, this),
4123
+ keyup: $.proxy(this.update, this),
4124
+ keydown: $.proxy(this.keydown, this)
4125
+ }],
4126
+ [this.component, {
4127
+ click: $.proxy(this.show, this)
4128
+ }]
4129
+ ];
4130
+ }
4131
+ else if (this.element.is('div')) { // inline datepicker
4132
+ this.isInline = true;
4133
+ }
4134
+ else {
4135
+ this._events = [
4136
+ [this.element, {
4137
+ click: $.proxy(this.show, this)
4138
+ }]
4139
+ ];
4140
+ }
4141
+ for (var i=0, el, ev; i<this._events.length; i++){
4142
+ el = this._events[i][0];
4143
+ ev = this._events[i][1];
4144
+ el.on(ev);
4145
+ }
4146
+ },
4147
+ _detachEvents: function(){
4148
+ for (var i=0, el, ev; i<this._events.length; i++){
4149
+ el = this._events[i][0];
4150
+ ev = this._events[i][1];
4151
+ el.off(ev);
4152
+ }
4153
+ this._events = [];
4154
+ },
4155
+
3067
4156
  show: function(e) {
3068
4157
  this.picker.show();
3069
4158
  this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
@@ -3081,7 +4170,7 @@ $(function(){
3081
4170
  },
3082
4171
 
3083
4172
  hide: function(e){
3084
- if(this.isInline) return;
4173
+ if(this.isInline) return;
3085
4174
  this.picker.hide();
3086
4175
  $(window).off('resize', this.place);
3087
4176
  this.viewMode = this.startViewMode;
@@ -3089,7 +4178,14 @@ $(function(){
3089
4178
  if (!this.isInput) {
3090
4179
  $(document).off('mousedown', this.hide);
3091
4180
  }
3092
- if (e && e.currentTarget.value)
4181
+
4182
+ if (
4183
+ this.forceParse &&
4184
+ (
4185
+ this.isInput && this.element.val() ||
4186
+ this.hasInput && this.element.find('input').val()
4187
+ )
4188
+ )
3093
4189
  this.setValue();
3094
4190
  this.element.trigger({
3095
4191
  type: 'hide',
@@ -3097,9 +4193,15 @@ $(function(){
3097
4193
  });
3098
4194
  },
3099
4195
 
4196
+ remove: function() {
4197
+ this._detachEvents();
4198
+ this.picker.remove();
4199
+ delete this.element.data().datepicker;
4200
+ },
4201
+
3100
4202
  getDate: function() {
3101
4203
  var d = this.getUTCDate();
3102
- return new Date(d.getTime() + (d.getTimezoneOffset()*60000))
4204
+ return new Date(d.getTime() + (d.getTimezoneOffset()*60000));
3103
4205
  },
3104
4206
 
3105
4207
  getUTCDate: function() {
@@ -3119,18 +4221,19 @@ $(function(){
3119
4221
  var formatted = this.getFormattedDate();
3120
4222
  if (!this.isInput) {
3121
4223
  if (this.component){
3122
- this.element.find('input').prop('value', formatted);
4224
+ this.element.find('input').val(formatted);
3123
4225
  }
3124
4226
  this.element.data('date', formatted);
3125
4227
  } else {
3126
- this.element.prop('value', formatted);
4228
+ this.element.val(formatted);
3127
4229
  }
3128
4230
  },
3129
4231
 
3130
- getFormattedDate: function(format) {
3131
- if(format == undefined) format = this.format;
3132
- return DPGlobal.formatDate(this.date, format, this.language);
3133
- },
4232
+ getFormattedDate: function(format) {
4233
+ if (format === undefined)
4234
+ format = this.format;
4235
+ return DPGlobal.formatDate(this.date, format, this.language);
4236
+ },
3134
4237
 
3135
4238
  setStartDate: function(startDate){
3136
4239
  this.startDate = startDate||-Infinity;
@@ -3150,32 +4253,46 @@ $(function(){
3150
4253
  this.updateNavArrows();
3151
4254
  },
3152
4255
 
4256
+ setDaysOfWeekDisabled: function(daysOfWeekDisabled){
4257
+ this.daysOfWeekDisabled = daysOfWeekDisabled||[];
4258
+ if (!$.isArray(this.daysOfWeekDisabled)) {
4259
+ this.daysOfWeekDisabled = this.daysOfWeekDisabled.split(/,\s*/);
4260
+ }
4261
+ this.daysOfWeekDisabled = $.map(this.daysOfWeekDisabled, function (d) {
4262
+ return parseInt(d, 10);
4263
+ });
4264
+ this.update();
4265
+ this.updateNavArrows();
4266
+ },
4267
+
3153
4268
  place: function(){
3154
- if(this.isInline) return;
4269
+ if(this.isInline) return;
3155
4270
  var zIndex = parseInt(this.element.parents().filter(function() {
3156
4271
  return $(this).css('z-index') != 'auto';
3157
4272
  }).first().css('z-index'))+10;
3158
4273
  var offset = this.component ? this.component.offset() : this.element.offset();
4274
+ var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true);
3159
4275
  this.picker.css({
3160
- top: offset.top + this.height,
4276
+ top: offset.top + height,
3161
4277
  left: offset.left,
3162
4278
  zIndex: zIndex
3163
4279
  });
3164
4280
  },
3165
4281
 
3166
4282
  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
- }
4283
+ var date, fromArgs = false;
4284
+ if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
4285
+ date = arguments[0];
4286
+ fromArgs = true;
4287
+ } else {
4288
+ date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val();
4289
+ }
3174
4290
 
3175
4291
  this.date = DPGlobal.parseDate(date, this.format, this.language);
3176
4292
 
3177
- if(fromArgs) this.setValue();
4293
+ if(fromArgs) this.setValue();
3178
4294
 
4295
+ var oldViewDate = this.viewDate;
3179
4296
  if (this.date < this.startDate) {
3180
4297
  this.viewDate = new Date(this.startDate);
3181
4298
  } else if (this.date > this.endDate) {
@@ -3183,12 +4300,19 @@ $(function(){
3183
4300
  } else {
3184
4301
  this.viewDate = new Date(this.date);
3185
4302
  }
4303
+
4304
+ if (oldViewDate && oldViewDate.getTime() != this.viewDate.getTime()){
4305
+ this.element.trigger({
4306
+ type: 'changeDate',
4307
+ date: this.viewDate
4308
+ });
4309
+ }
3186
4310
  this.fill();
3187
4311
  },
3188
4312
 
3189
4313
  fillDow: function(){
3190
- var dowCnt = this.weekStart;
3191
- var html = '<tr>';
4314
+ var dowCnt = this.weekStart,
4315
+ html = '<tr>';
3192
4316
  while (dowCnt < this.weekStart + 7) {
3193
4317
  html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>';
3194
4318
  }
@@ -3197,8 +4321,8 @@ $(function(){
3197
4321
  },
3198
4322
 
3199
4323
  fillMonths: function(){
3200
- var html = '';
3201
- var i = 0
4324
+ var html = '',
4325
+ i = 0;
3202
4326
  while (i < 12) {
3203
4327
  html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>';
3204
4328
  }
@@ -3251,7 +4375,8 @@ $(function(){
3251
4375
  if (currentDate && prevMonth.valueOf() == currentDate) {
3252
4376
  clsName += ' active';
3253
4377
  }
3254
- if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate) {
4378
+ if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate ||
4379
+ $.inArray(prevMonth.getUTCDay(), this.daysOfWeekDisabled) !== -1) {
3255
4380
  clsName += ' disabled';
3256
4381
  }
3257
4382
  html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>');
@@ -3392,7 +4517,7 @@ $(function(){
3392
4517
  var year = this.viewDate.getUTCFullYear(),
3393
4518
  month = this.viewDate.getUTCMonth();
3394
4519
  if (target.is('.old')) {
3395
- if (month == 0) {
4520
+ if (month === 0) {
3396
4521
  month = 11;
3397
4522
  year -= 1;
3398
4523
  } else {
@@ -3432,8 +4557,8 @@ $(function(){
3432
4557
  }
3433
4558
  if (element) {
3434
4559
  element.change();
3435
- if (this.autoclose) {
3436
- this.hide();
4560
+ if (this.autoclose && (!which || which == 'date')) {
4561
+ this.hide();
3437
4562
  }
3438
4563
  }
3439
4564
  },
@@ -3579,16 +4704,16 @@ $(function(){
3579
4704
  if (dir) {
3580
4705
  this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir));
3581
4706
  }
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();
4707
+ /*
4708
+ vitalets: fixing bug of very special conditions:
4709
+ jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
4710
+ Method show() does not set display css correctly and datepicker is not shown.
4711
+ Changed to .css('display', 'block') solve the problem.
4712
+ See https://github.com/vitalets/x-editable/issues/37
4713
+
4714
+ In jquery 1.7.2+ everything works fine.
4715
+ */
4716
+ //this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
3592
4717
  this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
3593
4718
  this.updateNavArrows();
3594
4719
  }
@@ -3622,7 +4747,7 @@ $(function(){
3622
4747
  monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
3623
4748
  today: "Today"
3624
4749
  }
3625
- }
4750
+ };
3626
4751
 
3627
4752
  var DPGlobal = {
3628
4753
  modes: [
@@ -3642,28 +4767,28 @@ $(function(){
3642
4767
  navStep: 10
3643
4768
  }],
3644
4769
  isLeapYear: function (year) {
3645
- return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
4770
+ return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
3646
4771
  },
3647
4772
  getDaysInMonth: function (year, month) {
3648
- return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
4773
+ return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
3649
4774
  },
3650
- validParts: /dd?|mm?|MM?|yy(?:yy)?/g,
3651
- nonpunctuation: /[^ -\/:-@\[-`{-~\t\n\r]+/g,
4775
+ validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
4776
+ nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
3652
4777
  parseFormat: function(format){
3653
4778
  // IE treats \0 as a string end in inputs (truncating the value),
3654
4779
  // so it's a bad format delimiter, anyway
3655
4780
  var separators = format.replace(this.validParts, '\0').split('\0'),
3656
4781
  parts = format.match(this.validParts);
3657
- if (!separators || !separators.length || !parts || parts.length == 0){
4782
+ if (!separators || !separators.length || !parts || parts.length === 0){
3658
4783
  throw new Error("Invalid date format.");
3659
4784
  }
3660
4785
  return {separators: separators, parts: parts};
3661
4786
  },
3662
4787
  parseDate: function(date, format, language) {
3663
4788
  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),
4789
+ if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) {
4790
+ var part_re = /([\-+]\d+)([dmwy])/,
4791
+ parts = date.match(/([\-+]\d+)([dmwy])/g),
3667
4792
  part, dir;
3668
4793
  date = new Date();
3669
4794
  for (var i=0; i<parts.length; i++) {
@@ -3708,10 +4833,18 @@ $(function(){
3708
4833
  setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
3709
4834
  setters_map['dd'] = setters_map['d'];
3710
4835
  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++) {
4836
+ var fparts = format.parts.slice();
4837
+ // Remove noop parts
4838
+ if (parts.length != fparts.length) {
4839
+ fparts = $(fparts).filter(function(i,p){
4840
+ return $.inArray(p, setters_order) !== -1;
4841
+ }).toArray();
4842
+ }
4843
+ // Process remainder
4844
+ if (parts.length == fparts.length) {
4845
+ for (var i=0, cnt = fparts.length; i < cnt; i++) {
3713
4846
  val = parseInt(parts[i], 10);
3714
- part = format.parts[i];
4847
+ part = fparts[i];
3715
4848
  if (isNaN(val)) {
3716
4849
  switch(part) {
3717
4850
  case 'MM':
@@ -3736,8 +4869,8 @@ $(function(){
3736
4869
  }
3737
4870
  for (var i=0, s; i<setters_order.length; i++){
3738
4871
  s = setters_order[i];
3739
- if (s in parsed)
3740
- setters_map[s](date, parsed[s])
4872
+ if (s in parsed && !isNaN(parsed[s]))
4873
+ setters_map[s](date, parsed[s]);
3741
4874
  }
3742
4875
  }
3743
4876
  return date;
@@ -3745,6 +4878,8 @@ $(function(){
3745
4878
  formatDate: function(date, format, language){
3746
4879
  var val = {
3747
4880
  d: date.getUTCDate(),
4881
+ D: dates[language].daysShort[date.getUTCDay()],
4882
+ DD: dates[language].days[date.getUTCDay()],
3748
4883
  m: date.getUTCMonth() + 1,
3749
4884
  M: dates[language].monthsShort[date.getUTCMonth()],
3750
4885
  MM: dates[language].months[date.getUTCMonth()],
@@ -3757,7 +4892,7 @@ $(function(){
3757
4892
  seps = $.extend([], format.separators);
3758
4893
  for (var i=0, cnt = format.parts.length; i < cnt; i++) {
3759
4894
  if (seps.length)
3760
- date.push(seps.shift())
4895
+ date.push(seps.shift());
3761
4896
  date.push(val[format.parts[i]]);
3762
4897
  }
3763
4898
  return date.join('');
@@ -3795,7 +4930,7 @@ $(function(){
3795
4930
  '</table>'+
3796
4931
  '</div>'+
3797
4932
  '</div>';
3798
-
3799
- $.fn.datepicker.DPGlobal = DPGlobal;
3800
-
4933
+
4934
+ $.fn.datepicker.DPGlobal = DPGlobal;
4935
+
3801
4936
  }( window.jQuery );