bootstrap-x-editable-rails 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3801 @@
1
+ /*! X-editable - v1.3.0
2
+ * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
3
+ * http://github.com/vitalets/x-editable
4
+ * Copyright (c) 2012 Vitaliy Potapov; Licensed MIT */
5
+
6
+ /**
7
+ Form with single input element, two buttons and two states: normal/loading.
8
+ Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown.
9
+ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
10
+
11
+ @class editableform
12
+ @uses text
13
+ @uses textarea
14
+ **/
15
+ (function ($) {
16
+
17
+ var EditableForm = function (div, options) {
18
+ this.options = $.extend({}, $.fn.editableform.defaults, options);
19
+ this.$div = $(div); //div, containing form. Not form tag! Not editable-element.
20
+ if(!this.options.scope) {
21
+ this.options.scope = this;
22
+ }
23
+ this.initInput();
24
+ };
25
+
26
+ EditableForm.prototype = {
27
+ constructor: EditableForm,
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
+
41
+ this.value = this.input.str2value(this.options.value);
42
+ },
43
+ initTemplate: function() {
44
+ this.$form = $($.fn.editableform.template);
45
+ },
46
+ initButtons: function() {
47
+ this.$form.find('.editable-buttons').append($.fn.editableform.buttons);
48
+ },
49
+ /**
50
+ Renders editableform
51
+
52
+ @method render
53
+ **/
54
+ render: function() {
55
+ this.$loading = $($.fn.editableform.loading);
56
+ this.$div.empty().append(this.$loading);
57
+ this.showLoading();
58
+
59
+ //init form template and buttons
60
+ this.initTemplate();
61
+ if(this.options.showbuttons) {
62
+ this.initButtons();
63
+ } else {
64
+ this.$form.find('.editable-buttons').remove();
65
+ }
66
+
67
+ /**
68
+ Fired when rendering starts
69
+ @event rendering
70
+ @param {Object} event event object
71
+ **/
72
+ this.$div.triggerHandler('rendering');
73
+
74
+ //render input
75
+ $.when(this.input.render())
76
+ .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
81
+ if(!this.options.showbuttons) {
82
+ this.input.autosubmit();
83
+ }
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
+
93
+ //attach 'cancel' handler
94
+ this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
95
+
96
+ if(this.input.error) {
97
+ this.error(this.input.error);
98
+ this.$form.find('.editable-submit').attr('disabled', true);
99
+ this.input.$input.attr('disabled', true);
100
+ //prevent form from submitting
101
+ this.$form.submit(function(e){ e.preventDefault(); });
102
+ } else {
103
+ this.error(false);
104
+ this.input.$input.removeAttr('disabled');
105
+ this.$form.find('.editable-submit').removeAttr('disabled');
106
+ this.input.value2input(this.value);
107
+ //attach submit handler
108
+ this.$form.submit($.proxy(this.submit, this));
109
+ }
110
+
111
+ /**
112
+ Fired when form is rendered
113
+ @event rendered
114
+ @param {Object} event event object
115
+ **/
116
+ this.$div.triggerHandler('rendered');
117
+
118
+ this.showForm();
119
+ }, this));
120
+ },
121
+ cancel: function() {
122
+ /**
123
+ Fired when form was cancelled by user
124
+ @event cancel
125
+ @param {Object} event event object
126
+ **/
127
+ this.$div.triggerHandler('cancel');
128
+ },
129
+ showLoading: function() {
130
+ var w;
131
+ if(this.$form) {
132
+ //set loading size equal to form
133
+ this.$loading.width(this.$form.outerWidth());
134
+ this.$loading.height(this.$form.outerHeight());
135
+ this.$form.hide();
136
+ } else {
137
+ //stretch loading to fill container width
138
+ w = this.$loading.parent().width();
139
+ if(w) {
140
+ this.$loading.width(w);
141
+ }
142
+ }
143
+ this.$loading.show();
144
+ },
145
+
146
+ showForm: function(activate) {
147
+ this.$loading.hide();
148
+ this.$form.show();
149
+ if(activate !== false) {
150
+ this.input.activate();
151
+ }
152
+ /**
153
+ Fired when form is shown
154
+ @event show
155
+ @param {Object} event event object
156
+ **/
157
+ this.$div.triggerHandler('show');
158
+ },
159
+
160
+ error: function(msg) {
161
+ var $group = this.$form.find('.control-group'),
162
+ $block = this.$form.find('.editable-error-block');
163
+
164
+ if(msg === false) {
165
+ $group.removeClass($.fn.editableform.errorGroupClass);
166
+ $block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
167
+ } else {
168
+ $group.addClass($.fn.editableform.errorGroupClass);
169
+ $block.addClass($.fn.editableform.errorBlockClass).text(msg).show();
170
+ }
171
+ },
172
+
173
+ submit: function(e) {
174
+ e.stopPropagation();
175
+ e.preventDefault();
176
+
177
+ var error,
178
+ newValue = this.input.input2value(); //get new value from input
179
+
180
+ //validation
181
+ if (error = this.validate(newValue)) {
182
+ this.error(error);
183
+ this.showForm();
184
+ return;
185
+ }
186
+
187
+ //if value not changed --> trigger 'nochange' event and return
188
+ /*jslint eqeq: true*/
189
+ if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) {
190
+ /*jslint eqeq: false*/
191
+ /**
192
+ Fired when value not changed but form is submitted. Requires savenochange = false.
193
+ @event nochange
194
+ @param {Object} event event object
195
+ **/
196
+ this.$div.triggerHandler('nochange');
197
+ return;
198
+ }
199
+
200
+ //sending data to server
201
+ $.when(this.save(newValue))
202
+ .done($.proxy(function(response) {
203
+ //run success callback
204
+ var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null;
205
+
206
+ //if success callback returns false --> keep form open and do not activate input
207
+ if(res === false) {
208
+ this.error(false);
209
+ this.showForm(false);
210
+ return;
211
+ }
212
+
213
+ //if success callback returns string --> keep form open, show error and activate input
214
+ if(typeof res === 'string') {
215
+ this.error(res);
216
+ this.showForm();
217
+ return;
218
+ }
219
+
220
+ //if success callback returns object like {newValue: <something>} --> use that value instead of submitted
221
+ if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
222
+ newValue = res.newValue;
223
+ }
224
+
225
+ //clear error message
226
+ this.error(false);
227
+ this.value = newValue;
228
+ /**
229
+ Fired when form is submitted
230
+ @event save
231
+ @param {Object} event event object
232
+ @param {Object} params additional params
233
+ @param {mixed} params.newValue submitted value
234
+ @param {Object} params.response ajax response
235
+
236
+ @example
237
+ $('#form-div').on('save'), function(e, params){
238
+ if(params.newValue === 'username') {...}
239
+ });
240
+ **/
241
+ this.$div.triggerHandler('save', {newValue: newValue, response: response});
242
+ }, this))
243
+ .fail($.proxy(function(xhr) {
244
+ this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!');
245
+ this.showForm();
246
+ }, this));
247
+ },
248
+
249
+ save: function(newValue) {
250
+ //convert value for submitting to server
251
+ var submitValue = this.input.value2submit(newValue);
252
+
253
+ //try parse composite pk defined as json string in data-pk
254
+ this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true);
255
+
256
+ var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk,
257
+ send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))),
258
+ params;
259
+
260
+ if (send) { //send to server
261
+ this.showLoading();
262
+
263
+ //standard params
264
+ params = {
265
+ name: this.options.name || '',
266
+ value: submitValue,
267
+ pk: pk
268
+ };
269
+
270
+ //additional params
271
+ if(typeof this.options.params === 'function') {
272
+ params = this.options.params.call(this.options.scope, params);
273
+ } else {
274
+ //try parse json in single quotes (from data-params attribute)
275
+ this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true);
276
+ $.extend(params, this.options.params);
277
+ }
278
+
279
+ if(typeof this.options.url === 'function') { //user's function
280
+ return this.options.url.call(this.options.scope, params);
281
+ } else {
282
+ //send ajax to server and return deferred object
283
+ return $.ajax($.extend({
284
+ url : this.options.url,
285
+ data : params,
286
+ type : 'POST'
287
+ }, this.options.ajaxOptions));
288
+ }
289
+ }
290
+ },
291
+
292
+ validate: function (value) {
293
+ if (value === undefined) {
294
+ value = this.value;
295
+ }
296
+ if (typeof this.options.validate === 'function') {
297
+ return this.options.validate.call(this.options.scope, value);
298
+ }
299
+ },
300
+
301
+ option: function(key, value) {
302
+ this.options[key] = value;
303
+ if(key === 'value') {
304
+ this.setValue(value);
305
+ }
306
+ },
307
+
308
+ setValue: function(value, convertStr) {
309
+ if(convertStr) {
310
+ this.value = this.input.str2value(value);
311
+ } else {
312
+ this.value = value;
313
+ }
314
+ }
315
+ };
316
+
317
+ /*
318
+ Initialize editableform. Applied to jQuery object.
319
+
320
+ @method $().editableform(options)
321
+ @params {Object} options
322
+ @example
323
+ var $form = $('&lt;div&gt;').editableform({
324
+ type: 'text',
325
+ name: 'username',
326
+ url: '/post',
327
+ value: 'vitaliy'
328
+ });
329
+
330
+ //to display form you should call 'render' method
331
+ $form.editableform('render');
332
+ */
333
+ $.fn.editableform = function (option) {
334
+ var args = arguments;
335
+ return this.each(function () {
336
+ var $this = $(this),
337
+ data = $this.data('editableform'),
338
+ options = typeof option === 'object' && option;
339
+ if (!data) {
340
+ $this.data('editableform', (data = new EditableForm(this, options)));
341
+ }
342
+
343
+ if (typeof option === 'string') { //call method
344
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
345
+ }
346
+ });
347
+ };
348
+
349
+ //keep link to constructor to allow inheritance
350
+ $.fn.editableform.Constructor = EditableForm;
351
+
352
+ //defaults
353
+ $.fn.editableform.defaults = {
354
+ /* see also defaults for input */
355
+
356
+ /**
357
+ Type of input. Can be <code>text|textarea|select|date|checklist</code>
358
+
359
+ @property type
360
+ @type string
361
+ @default 'text'
362
+ **/
363
+ type: 'text',
364
+ /**
365
+ Url for submit, e.g. <code>'/post'</code>
366
+ If function - it will be called instead of ajax. Function can return deferred object to run fail/done callbacks.
367
+
368
+ @property url
369
+ @type string|function
370
+ @default null
371
+ @example
372
+ url: function(params) {
373
+ if(params.value === 'abc') {
374
+ var d = new $.Deferred;
375
+ return d.reject('field cannot be "abc"'); //returning error via deferred object
376
+ } else {
377
+ someModel.set(params.name, params.value); //save data in some js model
378
+ }
379
+ }
380
+ **/
381
+ url:null,
382
+ /**
383
+ Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value).
384
+ If defined as <code>function</code> - returned object **overwrites** original ajax data.
385
+ @example
386
+ params: function(params) {
387
+ //originally params contain pk, name and value
388
+ params.a = 1;
389
+ return params;
390
+ }
391
+
392
+ @property params
393
+ @type object|function
394
+ @default null
395
+ **/
396
+ params:null,
397
+ /**
398
+ Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute
399
+
400
+ @property name
401
+ @type string
402
+ @default null
403
+ **/
404
+ name: null,
405
+ /**
406
+ Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>.
407
+ Can be calculated dynamically via function.
408
+
409
+ @property pk
410
+ @type string|object|function
411
+ @default null
412
+ **/
413
+ pk: null,
414
+ /**
415
+ Initial value. If not defined - will be taken from element's content.
416
+ For __select__ type should be defined (as it is ID of shown text).
417
+
418
+ @property value
419
+ @type string|object
420
+ @default null
421
+ **/
422
+ value: null,
423
+ /**
424
+ Strategy for sending data on server. Can be <code>auto|always|never</code>.
425
+ When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element.
426
+
427
+ @property send
428
+ @type string
429
+ @default 'auto'
430
+ **/
431
+ send: 'auto',
432
+ /**
433
+ Function for client-side validation. If returns string - means validation not passed and string showed as error.
434
+
435
+ @property validate
436
+ @type function
437
+ @default null
438
+ @example
439
+ validate: function(value) {
440
+ if($.trim(value) == '') {
441
+ return 'This field is required';
442
+ }
443
+ }
444
+ **/
445
+ validate: null,
446
+ /**
447
+ Success callback. Called when value successfully sent on server and **response status = 200**.
448
+ Useful to work with json response. For example, if your backend response can be <code>{success: true}</code>
449
+ or <code>{success: false, msg: "server error"}</code> you can check it inside this callback.
450
+ If it returns **string** - means error occured and string is shown as error message.
451
+ If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user.
452
+ Otherwise newValue simply rendered into element.
453
+
454
+ @property success
455
+ @type function
456
+ @default null
457
+ @example
458
+ success: function(response, newValue) {
459
+ if(!response.success) return response.msg;
460
+ }
461
+ **/
462
+ success: null,
463
+ /**
464
+ Additional options for ajax request.
465
+ List of values: http://api.jquery.com/jQuery.ajax
466
+
467
+ @property ajaxOptions
468
+ @type object
469
+ @default null
470
+ @since 1.1.1
471
+ **/
472
+ ajaxOptions: null,
473
+ /**
474
+ 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
+ }
481
+
482
+ @property showbuttons
483
+ @type boolean
484
+ @default true
485
+ @since 1.1.1
486
+ **/
487
+ showbuttons: true,
488
+ /**
489
+ Scope for callback methods (success, validate).
490
+ If <code>null</code> means editableform instance itself.
491
+
492
+ @property scope
493
+ @type DOMElement|object
494
+ @default null
495
+ @since 1.2.0
496
+ @private
497
+ **/
498
+ scope: null,
499
+ /**
500
+ Whether to save or cancel value when it was not changed but form was submitted
501
+
502
+ @property savenochange
503
+ @type boolean
504
+ @default false
505
+ @since 1.2.0
506
+ **/
507
+ savenochange: false
508
+ };
509
+
510
+ /*
511
+ Note: following params could redefined in engine: bootstrap or jqueryui:
512
+ Classes 'control-group' and 'editable-error-block' must always present!
513
+ */
514
+ $.fn.editableform.template = '<form class="form-inline editableform">'+
515
+ '<div class="control-group">' +
516
+ '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
517
+ '<div class="editable-error-block"></div>' +
518
+ '</div>' +
519
+ '</form>';
520
+
521
+ //loading div
522
+ $.fn.editableform.loading = '<div class="editableform-loading"></div>';
523
+
524
+ //buttons
525
+ $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
526
+ '<button type="button" class="editable-cancel">cancel</button>';
527
+
528
+ //error class attached to control-group
529
+ $.fn.editableform.errorGroupClass = null;
530
+
531
+ //error class attached to editable-error-block
532
+ $.fn.editableform.errorBlockClass = 'editable-error';
533
+ }(window.jQuery));
534
+ /**
535
+ * EditableForm utilites
536
+ */
537
+ (function ($) {
538
+ //utils
539
+ $.fn.editableutils = {
540
+ /**
541
+ * classic JS inheritance function
542
+ */
543
+ inherit: function (Child, Parent) {
544
+ var F = function() { };
545
+ F.prototype = Parent.prototype;
546
+ Child.prototype = new F();
547
+ Child.prototype.constructor = Child;
548
+ Child.superclass = Parent.prototype;
549
+ },
550
+
551
+ /**
552
+ * set caret position in input
553
+ * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
554
+ */
555
+ setCursorPosition: function(elem, pos) {
556
+ if (elem.setSelectionRange) {
557
+ elem.setSelectionRange(pos, pos);
558
+ } else if (elem.createTextRange) {
559
+ var range = elem.createTextRange();
560
+ range.collapse(true);
561
+ range.moveEnd('character', pos);
562
+ range.moveStart('character', pos);
563
+ range.select();
564
+ }
565
+ },
566
+
567
+ /**
568
+ * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes)
569
+ * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}">
570
+ * safe = true --> means no exception will be thrown
571
+ * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
572
+ */
573
+ tryParseJson: function(s, safe) {
574
+ if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
575
+ if (safe) {
576
+ try {
577
+ /*jslint evil: true*/
578
+ s = (new Function('return ' + s))();
579
+ /*jslint evil: false*/
580
+ } catch (e) {} finally {
581
+ return s;
582
+ }
583
+ } else {
584
+ /*jslint evil: true*/
585
+ s = (new Function('return ' + s))();
586
+ /*jslint evil: false*/
587
+ }
588
+ }
589
+ return s;
590
+ },
591
+
592
+ /**
593
+ * slice object by specified keys
594
+ */
595
+ sliceObj: function(obj, keys, caseSensitive /* default: false */) {
596
+ var key, keyLower, newObj = {};
597
+
598
+ if (!$.isArray(keys) || !keys.length) {
599
+ return newObj;
600
+ }
601
+
602
+ for (var i = 0; i < keys.length; i++) {
603
+ key = keys[i];
604
+ if (obj.hasOwnProperty(key)) {
605
+ newObj[key] = obj[key];
606
+ }
607
+
608
+ if(caseSensitive === true) {
609
+ continue;
610
+ }
611
+
612
+ //when getting data-* attributes via $.data() it's converted to lowercase.
613
+ //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
614
+ //workaround is code below.
615
+ keyLower = key.toLowerCase();
616
+ if (obj.hasOwnProperty(keyLower)) {
617
+ newObj[key] = obj[keyLower];
618
+ }
619
+ }
620
+
621
+ return newObj;
622
+ },
623
+
624
+ /**
625
+ * exclude complex objects from $.data() before pass to config
626
+ */
627
+ getConfigData: function($element) {
628
+ var data = {};
629
+ $.each($element.data(), function(k, v) {
630
+ if(typeof v !== 'object' || (v && typeof v === 'object' && v.constructor === Object)) {
631
+ data[k] = v;
632
+ }
633
+ });
634
+ return data;
635
+ },
636
+
637
+ objectKeys: function(o) {
638
+ if (Object.keys) {
639
+ return Object.keys(o);
640
+ } else {
641
+ if (o !== Object(o)) {
642
+ throw new TypeError('Object.keys called on a non-object');
643
+ }
644
+ var k=[], p;
645
+ for (p in o) {
646
+ if (Object.prototype.hasOwnProperty.call(o,p)) {
647
+ k.push(p);
648
+ }
649
+ }
650
+ return k;
651
+ }
652
+
653
+ },
654
+
655
+ /**
656
+ method to escape html.
657
+ **/
658
+ escape: function(str) {
659
+ return $('<div>').text(str).html();
660
+ }
661
+ };
662
+ }(window.jQuery));
663
+ /**
664
+ Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
665
+ This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
666
+ Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br>
667
+ Applied as jQuery method.
668
+
669
+ @class editableContainer
670
+ @uses editableform
671
+ **/
672
+ (function ($) {
673
+
674
+ var EditableContainer = function (element, options) {
675
+ this.init(element, options);
676
+ };
677
+
678
+ //methods
679
+ EditableContainer.prototype = {
680
+ containerName: null, //tbd in child class
681
+ innerCss: null, //tbd in child class
682
+ init: function(element, options) {
683
+ this.$element = $(element);
684
+ //todo: what is in priority: data or js?
685
+ this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options);
686
+ this.splitOptions();
687
+ this.initContainer();
688
+
689
+ //bind 'destroyed' listener to destroy container when element is removed from dom
690
+ this.$element.on('destroyed', $.proxy(function(){
691
+ this.destroy();
692
+ }, this));
693
+
694
+ //attach document handlers (once)
695
+ if(!$(document).data('editable-handlers-attached')) {
696
+ //close all on escape
697
+ $(document).on('keyup.editable', function (e) {
698
+ if (e.which === 27) {
699
+ $('.editable-open').editableContainer('hide');
700
+ //todo: return focus on element
701
+ }
702
+ });
703
+
704
+ //close containers when click outside
705
+ $(document).on('click.editable', function(e) {
706
+ var $target = $(e.target);
707
+
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);
714
+ }
715
+ });
716
+
717
+ $(document).data('editable-handlers-attached', true);
718
+ }
719
+ },
720
+
721
+ //split options on containerOptions and formOptions
722
+ splitOptions: function() {
723
+ this.containerOptions = {};
724
+ this.formOptions = {};
725
+ var cDef = $.fn[this.containerName].defaults;
726
+ for(var k in this.options) {
727
+ if(k in cDef) {
728
+ this.containerOptions[k] = this.options[k];
729
+ } else {
730
+ this.formOptions[k] = this.options[k];
731
+ }
732
+ }
733
+ },
734
+
735
+ initContainer: function(){
736
+ this.call(this.containerOptions);
737
+ },
738
+
739
+ initForm: function() {
740
+ this.formOptions.scope = this.$element[0]; //set scope of form callbacks to element
741
+ this.$form = $('<div>')
742
+ .editableform(this.formOptions)
743
+ .on({
744
+ save: $.proxy(this.save, this),
745
+ cancel: $.proxy(function(){ this.hide('cancel'); }, this),
746
+ nochange: $.proxy(function(){ this.hide('nochange'); }, this),
747
+ show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state)
748
+ rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
749
+ rendered: $.proxy(function(){
750
+ /**
751
+ Fired when container is shown and form is rendered (for select will wait for loading dropdown options)
752
+
753
+ @event shown
754
+ @param {Object} event event object
755
+ @example
756
+ $('#username').on('shown', function() {
757
+ var $tip = $(this).data('editableContainer').tip();
758
+ $tip.find('input').val('overwriting value of input..');
759
+ });
760
+ **/
761
+ this.$element.triggerHandler('shown');
762
+ }, this)
763
+ });
764
+ return this.$form;
765
+ },
766
+
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
+ /**
784
+ Shows container with form
785
+ @method show()
786
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
787
+ **/
788
+ show: function (closeAll) {
789
+ this.$element.addClass('editable-open');
790
+ if(closeAll !== false) {
791
+ //close all open containers (except this)
792
+ this.closeOthers(this.$element[0]);
793
+ }
794
+
795
+ this.innerShow();
796
+ },
797
+
798
+ /* internal show method. To be overwritten in child classes */
799
+ innerShow: function () {
800
+ this.call('show');
801
+ this.tip().addClass('editable-container');
802
+ this.initForm();
803
+ this.tip().find(this.innerCss).empty().append(this.$form);
804
+ this.$form.editableform('render');
805
+ },
806
+
807
+ /**
808
+ Hides container with form
809
+ @method hide()
810
+ @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
811
+ **/
812
+ hide: function(reason) {
813
+ if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
814
+ return;
815
+ }
816
+ this.$element.removeClass('editable-open');
817
+ this.innerHide();
818
+ /**
819
+ Fired when container was hidden. It occurs on both save or cancel.
820
+
821
+ @event hidden
822
+ @param {object} event event object
823
+ @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
824
+ @example
825
+ $('#username').on('hidden', function(e, reason) {
826
+ if(reason === 'save' || reason === 'cancel') {
827
+ //auto-open next editable
828
+ $(this).closest('tr').next().find('.editable').editable('show');
829
+ }
830
+ });
831
+ **/
832
+ this.$element.triggerHandler('hidden', reason);
833
+ },
834
+
835
+ /* internal hide method. To be overwritten in child classes */
836
+ innerHide: function () {
837
+ this.call('hide');
838
+ },
839
+
840
+ /**
841
+ Toggles container visibility (show / hide)
842
+ @method toggle()
843
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
844
+ **/
845
+ toggle: function(closeAll) {
846
+ if(this.tip && this.tip().is(':visible')) {
847
+ this.hide();
848
+ } else {
849
+ this.show(closeAll);
850
+ }
851
+ },
852
+
853
+ /*
854
+ Updates the position of container when content changed.
855
+ @method setPosition()
856
+ */
857
+ setPosition: function() {
858
+ //tbd in child class
859
+ },
860
+
861
+ save: function(e, params) {
862
+ this.hide('save');
863
+ /**
864
+ Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
865
+
866
+ @event save
867
+ @param {Object} event event object
868
+ @param {Object} params additional params
869
+ @param {mixed} params.newValue submitted value
870
+ @param {Object} params.response ajax response
871
+ @example
872
+ $('#username').on('save', function(e, params) {
873
+ //assuming server response: '{success: true}'
874
+ var pk = $(this).data('editableContainer').options.pk;
875
+ if(params.response && params.response.success) {
876
+ alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
877
+ } else {
878
+ alert('error!');
879
+ }
880
+ });
881
+ **/
882
+ this.$element.triggerHandler('save', params);
883
+ },
884
+
885
+ /**
886
+ Sets new option
887
+
888
+ @method option(key, value)
889
+ @param {string} key
890
+ @param {mixed} value
891
+ **/
892
+ option: function(key, value) {
893
+ this.options[key] = value;
894
+ if(key in this.containerOptions) {
895
+ this.containerOptions[key] = value;
896
+ this.setContainerOption(key, value);
897
+ } else {
898
+ this.formOptions[key] = value;
899
+ if(this.$form) {
900
+ this.$form.editableform('option', key, value);
901
+ }
902
+ }
903
+ },
904
+
905
+ setContainerOption: function(key, value) {
906
+ this.call('option', key, value);
907
+ },
908
+
909
+ /**
910
+ Destroys the container instance
911
+ @method destroy()
912
+ **/
913
+ destroy: function() {
914
+ this.call('destroy');
915
+ },
916
+
917
+ /*
918
+ Closes other containers except one related to passed element.
919
+ Other containers can be cancelled or submitted (depends on onblur option)
920
+ */
921
+ closeOthers: function(element) {
922
+ $('.editable-open').each(function(i, el){
923
+ //do nothing with passed element and it's children
924
+ if(el === element || $(el).find(element).length) {
925
+ return;
926
+ }
927
+
928
+ //otherwise cancel or submit all open containers
929
+ var $el = $(el),
930
+ ec = $el.data('editableContainer');
931
+
932
+ if(!ec) {
933
+ return;
934
+ }
935
+
936
+ if(ec.options.onblur === 'cancel') {
937
+ $el.data('editableContainer').hide('onblur');
938
+ } else if(ec.options.onblur === 'submit') {
939
+ $el.data('editableContainer').tip().find('form').submit();
940
+ }
941
+ });
942
+
943
+ },
944
+
945
+ /**
946
+ Activates input of visible container (e.g. set focus)
947
+ @method activate()
948
+ **/
949
+ activate: function() {
950
+ if(this.tip && this.tip().is(':visible') && this.$form) {
951
+ this.$form.data('editableform').input.activate();
952
+ }
953
+ }
954
+
955
+ };
956
+
957
+ /**
958
+ jQuery method to initialize editableContainer.
959
+
960
+ @method $().editableContainer(options)
961
+ @params {Object} options
962
+ @example
963
+ $('#edit').editableContainer({
964
+ type: 'text',
965
+ url: '/post',
966
+ pk: 1,
967
+ value: 'hello'
968
+ });
969
+ **/
970
+ $.fn.editableContainer = function (option) {
971
+ var args = arguments;
972
+ return this.each(function () {
973
+ var $this = $(this),
974
+ dataKey = 'editableContainer',
975
+ data = $this.data(dataKey),
976
+ options = typeof option === 'object' && option;
977
+
978
+ if (!data) {
979
+ $this.data(dataKey, (data = new EditableContainer(this, options)));
980
+ }
981
+
982
+ if (typeof option === 'string') { //call method
983
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
984
+ }
985
+ });
986
+ };
987
+
988
+ //store constructor
989
+ $.fn.editableContainer.Constructor = EditableContainer;
990
+
991
+ //defaults
992
+ $.fn.editableContainer.defaults = {
993
+ /**
994
+ Initial value of form input
995
+
996
+ @property value
997
+ @type mixed
998
+ @default null
999
+ @private
1000
+ **/
1001
+ value: null,
1002
+ /**
1003
+ Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inline container.
1004
+
1005
+ @property placement
1006
+ @type string
1007
+ @default 'top'
1008
+ **/
1009
+ placement: 'top',
1010
+ /**
1011
+ Whether to hide container on save/cancel.
1012
+
1013
+ @property autohide
1014
+ @type boolean
1015
+ @default true
1016
+ @private
1017
+ **/
1018
+ autohide: true,
1019
+ /**
1020
+ Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>.
1021
+ Setting <code>ignore</code> allows to have several containers open.
1022
+
1023
+ @property onblur
1024
+ @type string
1025
+ @default 'cancel'
1026
+ @since 1.1.1
1027
+ **/
1028
+ onblur: 'cancel'
1029
+ };
1030
+
1031
+ /*
1032
+ * workaround to have 'destroyed' event to destroy popover when element is destroyed
1033
+ * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
1034
+ */
1035
+ jQuery.event.special.destroyed = {
1036
+ remove: function(o) {
1037
+ if (o.handler) {
1038
+ o.handler();
1039
+ }
1040
+ }
1041
+ };
1042
+
1043
+ }(window.jQuery));
1044
+
1045
+ /**
1046
+ Makes editable any HTML element on the page. Applied as jQuery method.
1047
+
1048
+ @class editable
1049
+ @uses editableContainer
1050
+ **/
1051
+ (function ($) {
1052
+
1053
+ var Editable = function (element, options) {
1054
+ this.$element = $(element);
1055
+ this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options);
1056
+ this.init();
1057
+ };
1058
+
1059
+ Editable.prototype = {
1060
+ constructor: Editable,
1061
+ 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
+
1073
+ //name
1074
+ this.options.name = this.options.name || this.$element.attr('id');
1075
+
1076
+ //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);
1083
+ return;
1084
+ }
1085
+
1086
+ //set value from settings or by element's text
1087
+ if (this.options.value === undefined || this.options.value === null) {
1088
+ this.value = this.input.html2value($.trim(this.$element.html()));
1089
+ isValueByText = true;
1090
+ } else {
1091
+ /*
1092
+ value can be string when received from 'data-value' attribute
1093
+ for complext objects value can be set as json string in data-value attribute,
1094
+ e.g. data-value="{city: 'Moscow', street: 'Lenina'}"
1095
+ */
1096
+ this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true);
1097
+ if(typeof this.options.value === 'string') {
1098
+ this.value = this.input.str2value(this.options.value);
1099
+ } else {
1100
+ this.value = this.options.value;
1101
+ }
1102
+ }
1103
+
1104
+ //add 'editable' class to every editable element
1105
+ this.$element.addClass('editable');
1106
+
1107
+ //attach handler activating editable. In disabled mode it just prevent default action (useful for links)
1108
+ if(this.options.toggle !== 'manual') {
1109
+ this.$element.addClass('editable-click');
1110
+ this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
1111
+ e.preventDefault();
1112
+ //stop propagation not required anymore because in document click handler it checks event target
1113
+ //e.stopPropagation();
1114
+
1115
+ if(this.options.toggle === 'mouseenter') {
1116
+ //for hover only show container
1117
+ this.show();
1118
+ } else {
1119
+ //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
1120
+ var closeAll = (this.options.toggle !== 'click');
1121
+ this.toggle(closeAll);
1122
+ }
1123
+ }, this));
1124
+ } else {
1125
+ this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
1126
+ }
1127
+
1128
+ //check conditions for autotext:
1129
+ //if value was generated by text or value is empty, no sense to run autotext
1130
+ doAutotext = !isValueByText && this.value !== null && this.value !== undefined;
1131
+ doAutotext &= (this.options.autotext === 'always') || (this.options.autotext === 'auto' && !this.$element.text().length);
1132
+ $.when(doAutotext ? this.render() : true).then($.proxy(function() {
1133
+ if(this.options.disabled) {
1134
+ this.disable();
1135
+ } else {
1136
+ this.enable();
1137
+ }
1138
+ /**
1139
+ Fired when element was initialized by editable method.
1140
+
1141
+ @event init
1142
+ @param {Object} event event object
1143
+ @param {Object} editable editable instance
1144
+ @since 1.2.0
1145
+ **/
1146
+ this.$element.triggerHandler('init', this);
1147
+ }, this));
1148
+ },
1149
+
1150
+ /*
1151
+ Renders value into element's text.
1152
+ Can call custom display method from options.
1153
+ Can return deferred object.
1154
+ @method render()
1155
+ */
1156
+ render: function() {
1157
+ //do not display anything
1158
+ if(this.options.display === false) {
1159
+ return;
1160
+ }
1161
+ //if it is input with source, we pass callback in third param to be called when source is loaded
1162
+ if(this.input.options.hasOwnProperty('source')) {
1163
+ return this.input.value2html(this.value, this.$element[0], this.options.display);
1164
+ //if display method defined --> use it
1165
+ } else if(typeof this.options.display === 'function') {
1166
+ return this.options.display.call(this.$element[0], this.value);
1167
+ //else use input's original value2html() method
1168
+ } else {
1169
+ return this.input.value2html(this.value, this.$element[0]);
1170
+ }
1171
+ },
1172
+
1173
+ /**
1174
+ Enables editable
1175
+ @method enable()
1176
+ **/
1177
+ enable: function() {
1178
+ this.options.disabled = false;
1179
+ this.$element.removeClass('editable-disabled');
1180
+ this.handleEmpty();
1181
+ if(this.options.toggle !== 'manual') {
1182
+ if(this.$element.attr('tabindex') === '-1') {
1183
+ this.$element.removeAttr('tabindex');
1184
+ }
1185
+ }
1186
+ },
1187
+
1188
+ /**
1189
+ Disables editable
1190
+ @method disable()
1191
+ **/
1192
+ disable: function() {
1193
+ this.options.disabled = true;
1194
+ this.hide();
1195
+ this.$element.addClass('editable-disabled');
1196
+ this.handleEmpty();
1197
+ //do not stop focus on this element
1198
+ this.$element.attr('tabindex', -1);
1199
+ },
1200
+
1201
+ /**
1202
+ Toggles enabled / disabled state of editable element
1203
+ @method toggleDisabled()
1204
+ **/
1205
+ toggleDisabled: function() {
1206
+ if(this.options.disabled) {
1207
+ this.enable();
1208
+ } else {
1209
+ this.disable();
1210
+ }
1211
+ },
1212
+
1213
+ /**
1214
+ Sets new option
1215
+
1216
+ @method option(key, value)
1217
+ @param {string|object} key option name or object with several options
1218
+ @param {mixed} value option new value
1219
+ @example
1220
+ $('.editable').editable('option', 'pk', 2);
1221
+ **/
1222
+ option: function(key, value) {
1223
+ //set option(s) by object
1224
+ if(key && typeof key === 'object') {
1225
+ $.each(key, $.proxy(function(k, v){
1226
+ this.option($.trim(k), v);
1227
+ }, this));
1228
+ return;
1229
+ }
1230
+
1231
+ //set option by string
1232
+ this.options[key] = value;
1233
+
1234
+ //disabled
1235
+ if(key === 'disabled') {
1236
+ if(value) {
1237
+ this.disable();
1238
+ } else {
1239
+ this.enable();
1240
+ }
1241
+ return;
1242
+ }
1243
+
1244
+ //value
1245
+ if(key === 'value') {
1246
+ this.setValue(value);
1247
+ }
1248
+
1249
+ //transfer new option to container!
1250
+ if(this.container) {
1251
+ this.container.option(key, value);
1252
+ }
1253
+ },
1254
+
1255
+ /*
1256
+ * set emptytext if element is empty (reverse: remove emptytext if needed)
1257
+ */
1258
+ handleEmpty: function () {
1259
+ //do not handle empty if we do not display anything
1260
+ if(this.options.display === false) {
1261
+ return;
1262
+ }
1263
+
1264
+ var emptyClass = 'editable-empty';
1265
+ //emptytext shown only for enabled
1266
+ if(!this.options.disabled) {
1267
+ if ($.trim(this.$element.text()) === '') {
1268
+ this.$element.addClass(emptyClass).text(this.options.emptytext);
1269
+ } else {
1270
+ this.$element.removeClass(emptyClass);
1271
+ }
1272
+ } else {
1273
+ //below required if element disable property was changed
1274
+ if(this.$element.hasClass(emptyClass)) {
1275
+ this.$element.empty();
1276
+ this.$element.removeClass(emptyClass);
1277
+ }
1278
+ }
1279
+ },
1280
+
1281
+ /**
1282
+ Shows container with form
1283
+ @method show()
1284
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1285
+ **/
1286
+ show: function (closeAll) {
1287
+ if(this.options.disabled) {
1288
+ return;
1289
+ }
1290
+
1291
+ //init editableContainer: popover, tooltip, inline, etc..
1292
+ if(!this.container) {
1293
+ var containerOptions = $.extend({}, this.options, {
1294
+ value: this.value
1295
+ });
1296
+ this.$element.editableContainer(containerOptions);
1297
+ this.$element.on("save.internal", $.proxy(this.save, this));
1298
+ this.container = this.$element.data('editableContainer');
1299
+ } else if(this.container.tip().is(':visible')) {
1300
+ return;
1301
+ }
1302
+
1303
+ //show container
1304
+ this.container.show(closeAll);
1305
+ },
1306
+
1307
+ /**
1308
+ Hides container with form
1309
+ @method hide()
1310
+ **/
1311
+ hide: function () {
1312
+ if(this.container) {
1313
+ this.container.hide();
1314
+ }
1315
+ },
1316
+
1317
+ /**
1318
+ Toggles container visibility (show / hide)
1319
+ @method toggle()
1320
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1321
+ **/
1322
+ toggle: function(closeAll) {
1323
+ if(this.container && this.container.tip().is(':visible')) {
1324
+ this.hide();
1325
+ } else {
1326
+ this.show(closeAll);
1327
+ }
1328
+ },
1329
+
1330
+ /*
1331
+ * called when form was submitted
1332
+ */
1333
+ save: function(e, params) {
1334
+ //if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css.
1335
+ if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) {
1336
+ this.$element.addClass('editable-unsaved');
1337
+ } else {
1338
+ this.$element.removeClass('editable-unsaved');
1339
+ }
1340
+
1341
+ // this.hide();
1342
+ this.setValue(params.newValue);
1343
+
1344
+ /**
1345
+ Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
1346
+
1347
+ @event save
1348
+ @param {Object} event event object
1349
+ @param {Object} params additional params
1350
+ @param {mixed} params.newValue submitted value
1351
+ @param {Object} params.response ajax response
1352
+ @example
1353
+ $('#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
+ }
1361
+ });
1362
+ **/
1363
+ //event itself is triggered by editableContainer. Description here is only for documentation
1364
+ },
1365
+
1366
+ validate: function () {
1367
+ if (typeof this.options.validate === 'function') {
1368
+ return this.options.validate.call(this, this.value);
1369
+ }
1370
+ },
1371
+
1372
+ /**
1373
+ Sets new value of editable
1374
+ @method setValue(value, convertStr)
1375
+ @param {mixed} value new value
1376
+ @param {boolean} convertStr whether to convert value from string to internal format
1377
+ **/
1378
+ setValue: function(value, convertStr) {
1379
+ if(convertStr) {
1380
+ this.value = this.input.str2value(value);
1381
+ } else {
1382
+ this.value = value;
1383
+ }
1384
+ if(this.container) {
1385
+ this.container.option('value', this.value);
1386
+ }
1387
+ $.when(this.render())
1388
+ .then($.proxy(function() {
1389
+ this.handleEmpty();
1390
+ }, this));
1391
+ },
1392
+
1393
+ /**
1394
+ Activates input of visible container (e.g. set focus)
1395
+ @method activate()
1396
+ **/
1397
+ activate: function() {
1398
+ if(this.container) {
1399
+ this.container.activate();
1400
+ }
1401
+ }
1402
+ };
1403
+
1404
+ /* EDITABLE PLUGIN DEFINITION
1405
+ * ======================= */
1406
+
1407
+ /**
1408
+ jQuery method to initialize editable element.
1409
+
1410
+ @method $().editable(options)
1411
+ @params {Object} options
1412
+ @example
1413
+ $('#username').editable({
1414
+ type: 'text',
1415
+ url: '/post',
1416
+ pk: 1
1417
+ });
1418
+ **/
1419
+ $.fn.editable = function (option) {
1420
+ //special API methods returning non-jquery object
1421
+ var result = {}, args = arguments, datakey = 'editable';
1422
+ switch (option) {
1423
+ /**
1424
+ Runs client-side validation for all matched editables
1425
+
1426
+ @method validate()
1427
+ @returns {Object} validation errors map
1428
+ @example
1429
+ $('#username, #fullname').editable('validate');
1430
+ // possible result:
1431
+ {
1432
+ username: "username is required",
1433
+ fullname: "fullname should be minimum 3 letters length"
1434
+ }
1435
+ **/
1436
+ case 'validate':
1437
+ this.each(function () {
1438
+ var $this = $(this), data = $this.data(datakey), error;
1439
+ if (data && (error = data.validate())) {
1440
+ result[data.options.name] = error;
1441
+ }
1442
+ });
1443
+ return result;
1444
+
1445
+ /**
1446
+ Returns current values of editable elements. If value is <code>null</code> or <code>undefined</code> it will not be returned
1447
+ @method getValue()
1448
+ @returns {Object} object of element names and values
1449
+ @example
1450
+ $('#username, #fullname').editable('validate');
1451
+ // possible result:
1452
+ {
1453
+ username: "superuser",
1454
+ fullname: "John"
1455
+ }
1456
+ **/
1457
+ case 'getValue':
1458
+ this.each(function () {
1459
+ var $this = $(this), data = $this.data(datakey);
1460
+ if (data && data.value !== undefined && data.value !== null) {
1461
+ result[data.options.name] = data.input.value2submit(data.value);
1462
+ }
1463
+ });
1464
+ return result;
1465
+
1466
+ /**
1467
+ This method collects values from several editable elements and submit them all to server.
1468
+ Internally it runs client-side validation for all fields and submits only in case of success.
1469
+ See <a href="#newrecord">creating new records</a> for details.
1470
+
1471
+ @method submit(options)
1472
+ @param {object} options
1473
+ @param {object} options.url url to submit data
1474
+ @param {object} options.data additional data to submit
1475
+ @param {object} options.ajaxOptions additional ajax options
1476
+ @param {function} options.error(obj) error handler
1477
+ @param {function} options.success(obj,config) success handler
1478
+ @returns {Object} jQuery object
1479
+ **/
1480
+ case 'submit': //collects value, validate and submit to server for creating new record
1481
+ var config = arguments[1] || {},
1482
+ $elems = this,
1483
+ errors = this.editable('validate'),
1484
+ values;
1485
+
1486
+ if($.isEmptyObject(errors)) {
1487
+ values = this.editable('getValue');
1488
+ if(config.data) {
1489
+ $.extend(values, config.data);
1490
+ }
1491
+
1492
+ $.ajax($.extend({
1493
+ url: config.url,
1494
+ data: values,
1495
+ type: 'POST'
1496
+ }, config.ajaxOptions))
1497
+ .success(function(response) {
1498
+ //successful response 200 OK
1499
+ if(typeof config.success === 'function') {
1500
+ config.success.call($elems, response, config);
1501
+ }
1502
+ })
1503
+ .error(function(){ //ajax error
1504
+ if(typeof config.error === 'function') {
1505
+ config.error.apply($elems, arguments);
1506
+ }
1507
+ });
1508
+ } else { //client-side validation error
1509
+ if(typeof config.error === 'function') {
1510
+ config.error.call($elems, errors);
1511
+ }
1512
+ }
1513
+ return this;
1514
+ }
1515
+
1516
+ //return jquery object
1517
+ return this.each(function () {
1518
+ var $this = $(this),
1519
+ data = $this.data(datakey),
1520
+ options = typeof option === 'object' && option;
1521
+
1522
+ if (!data) {
1523
+ $this.data(datakey, (data = new Editable(this, options)));
1524
+ }
1525
+
1526
+ if (typeof option === 'string') { //call method
1527
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
1528
+ }
1529
+ });
1530
+ };
1531
+
1532
+
1533
+ $.fn.editable.defaults = {
1534
+ /**
1535
+ Type of input. Can be <code>text|textarea|select|date|checklist</code> and more
1536
+
1537
+ @property type
1538
+ @type string
1539
+ @default 'text'
1540
+ **/
1541
+ type: 'text',
1542
+ /**
1543
+ Sets disabled state of editable
1544
+
1545
+ @property disabled
1546
+ @type boolean
1547
+ @default false
1548
+ **/
1549
+ disabled: false,
1550
+ /**
1551
+ How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>.
1552
+ When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
1553
+ **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element,
1554
+ you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document.
1555
+
1556
+ @example
1557
+ $('#edit-button').click(function(e) {
1558
+ e.stopPropagation();
1559
+ $('#username').editable('toggle');
1560
+ });
1561
+
1562
+ @property toggle
1563
+ @type string
1564
+ @default 'click'
1565
+ **/
1566
+ toggle: 'click',
1567
+ /**
1568
+ Text shown when element is empty.
1569
+
1570
+ @property emptytext
1571
+ @type string
1572
+ @default 'Empty'
1573
+ **/
1574
+ emptytext: 'Empty',
1575
+ /**
1576
+ Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date.
1577
+ For example, if dropdown list is <code>{1: 'a', 2: 'b'}</code> and element's value set to <code>1</code>, it's html will be automatically set to <code>'a'</code>.
1578
+ <code>auto</code> - text will be automatically set only if element is empty.
1579
+ <code>always|never</code> - always(never) try to set element's text.
1580
+
1581
+ @property autotext
1582
+ @type string
1583
+ @default 'auto'
1584
+ **/
1585
+ autotext: 'auto',
1586
+ /**
1587
+ Initial value of input. Taken from <code>data-value</code> or element's text.
1588
+
1589
+ @property value
1590
+ @type mixed
1591
+ @default element's text
1592
+ **/
1593
+ value: null,
1594
+ /**
1595
+ 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.
1598
+ Runs under element's scope.
1599
+ Second parameter __sourceData__ is passed for inputs with source (select, checklist).
1600
+
1601
+ @property display
1602
+ @type function|boolean
1603
+ @default null
1604
+ @since 1.2.0
1605
+ @example
1606
+ display: function(value, sourceData) {
1607
+ var escapedValue = $('<div>').text(value).html();
1608
+ $(this).html('<b>'+escapedValue+'</b>');
1609
+ }
1610
+ **/
1611
+ display: null
1612
+ };
1613
+
1614
+ }(window.jQuery));
1615
+
1616
+ /**
1617
+ AbstractInput - base class for all editable inputs.
1618
+ It defines interface to be implemented by any input type.
1619
+ To create your own input you can inherit from this class.
1620
+
1621
+ @class abstractinput
1622
+ **/
1623
+ (function ($) {
1624
+
1625
+ //types
1626
+ $.fn.editabletypes = {};
1627
+
1628
+ var AbstractInput = function () { };
1629
+
1630
+ AbstractInput.prototype = {
1631
+ /**
1632
+ Initializes input
1633
+
1634
+ @method init()
1635
+ **/
1636
+ init: function(type, options, defaults) {
1637
+ this.type = type;
1638
+ this.options = $.extend({}, defaults, options);
1639
+ this.$input = null;
1640
+ this.$clear = null;
1641
+ this.error = null;
1642
+ },
1643
+
1644
+ /**
1645
+ Renders input from tpl. Can return jQuery deferred object.
1646
+
1647
+ @method render()
1648
+ **/
1649
+ 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
+ }
1658
+ },
1659
+
1660
+ /**
1661
+ Sets element's html by value.
1662
+
1663
+ @method value2html(value, element)
1664
+ @param {mixed} value
1665
+ @param {DOMElement} element
1666
+ **/
1667
+ value2html: function(value, element) {
1668
+ $(element).text(value);
1669
+ },
1670
+
1671
+ /**
1672
+ Converts element's html to value
1673
+
1674
+ @method html2value(html)
1675
+ @param {string} html
1676
+ @returns {mixed}
1677
+ **/
1678
+ html2value: function(html) {
1679
+ return $('<div>').html(html).text();
1680
+ },
1681
+
1682
+ /**
1683
+ Converts value to string (for internal compare). For submitting to server used value2submit().
1684
+
1685
+ @method value2str(value)
1686
+ @param {mixed} value
1687
+ @returns {string}
1688
+ **/
1689
+ value2str: function(value) {
1690
+ return value;
1691
+ },
1692
+
1693
+ /**
1694
+ Converts string received from server into value.
1695
+
1696
+ @method str2value(str)
1697
+ @param {string} str
1698
+ @returns {mixed}
1699
+ **/
1700
+ str2value: function(str) {
1701
+ return str;
1702
+ },
1703
+
1704
+ /**
1705
+ Converts value for submitting to server
1706
+
1707
+ @method value2submit(value)
1708
+ @param {mixed} value
1709
+ @returns {mixed}
1710
+ **/
1711
+ value2submit: function(value) {
1712
+ return value;
1713
+ },
1714
+
1715
+ /**
1716
+ Sets value of input.
1717
+
1718
+ @method value2input(value)
1719
+ @param {mixed} value
1720
+ **/
1721
+ value2input: function(value) {
1722
+ this.$input.val(value);
1723
+ },
1724
+
1725
+ /**
1726
+ Returns value of input. Value can be object (e.g. datepicker)
1727
+
1728
+ @method input2value()
1729
+ **/
1730
+ input2value: function() {
1731
+ return this.$input.val();
1732
+ },
1733
+
1734
+ /**
1735
+ Activates input. For text it sets focus.
1736
+
1737
+ @method activate()
1738
+ **/
1739
+ activate: function() {
1740
+ if(this.$input.is(':visible')) {
1741
+ this.$input.focus();
1742
+ }
1743
+ },
1744
+
1745
+ /**
1746
+ Creates input.
1747
+
1748
+ @method clear()
1749
+ **/
1750
+ clear: function() {
1751
+ this.$input.val(null);
1752
+ },
1753
+
1754
+ /**
1755
+ method to escape html.
1756
+ **/
1757
+ escape: function(str) {
1758
+ return $('<div>').text(str).html();
1759
+ },
1760
+
1761
+ /**
1762
+ attach handler to automatically submit form when value changed (useful when buttons not shown)
1763
+ **/
1764
+ autosubmit: function() {
1765
+
1766
+ }
1767
+ };
1768
+
1769
+ AbstractInput.defaults = {
1770
+ /**
1771
+ HTML template of input. Normally you should not change it.
1772
+
1773
+ @property tpl
1774
+ @type string
1775
+ @default ''
1776
+ **/
1777
+ tpl: '',
1778
+ /**
1779
+ CSS class automatically applied to input
1780
+
1781
+ @property inputclass
1782
+ @type string
1783
+ @default input-medium
1784
+ **/
1785
+ inputclass: 'input-medium',
1786
+ /**
1787
+ Name attribute of input
1788
+
1789
+ @property name
1790
+ @type string
1791
+ @default null
1792
+ **/
1793
+ name: null
1794
+ };
1795
+
1796
+ $.extend($.fn.editabletypes, {abstractinput: AbstractInput});
1797
+
1798
+ }(window.jQuery));
1799
+
1800
+ /**
1801
+ List - abstract class for inputs that have source option loaded from js array or via ajax
1802
+
1803
+ @class list
1804
+ @extends abstractinput
1805
+ **/
1806
+ (function ($) {
1807
+
1808
+ var List = function (options) {
1809
+
1810
+ };
1811
+
1812
+ $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);
1813
+
1814
+ $.extend(List.prototype, {
1815
+ render: function () {
1816
+ List.superclass.render.call(this);
1817
+ var deferred = $.Deferred();
1818
+ this.error = null;
1819
+ this.sourceData = null;
1820
+ this.prependData = null;
1821
+ this.onSourceReady(function () {
1822
+ this.renderList();
1823
+ deferred.resolve();
1824
+ }, function () {
1825
+ this.error = this.options.sourceError;
1826
+ deferred.resolve();
1827
+ });
1828
+
1829
+ return deferred.promise();
1830
+ },
1831
+
1832
+ html2value: function (html) {
1833
+ return null; //can't set value by text
1834
+ },
1835
+
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
+ });
1850
+
1851
+ return deferred.promise();
1852
+ },
1853
+
1854
+ // ------------- additional functions ------------
1855
+
1856
+ onSourceReady: function (success, error) {
1857
+ //if allready loaded just call success
1858
+ if($.isArray(this.sourceData)) {
1859
+ success.call(this);
1860
+ return;
1861
+ }
1862
+
1863
+ // try parse json in single quotes (for double quotes jquery does automatically)
1864
+ try {
1865
+ this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false);
1866
+ } catch (e) {
1867
+ error.call(this);
1868
+ return;
1869
+ }
1870
+
1871
+ //loading from url
1872
+ if (typeof this.options.source === 'string') {
1873
+ //try to get from cache
1874
+ if(this.options.sourceCache) {
1875
+ var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''),
1876
+ cache;
1877
+
1878
+ if (!$(document).data(cacheID)) {
1879
+ $(document).data(cacheID, {});
1880
+ }
1881
+ cache = $(document).data(cacheID);
1882
+
1883
+ //check for cached data
1884
+ if (cache.loading === false && cache.sourceData) { //take source from cache
1885
+ this.sourceData = cache.sourceData;
1886
+ success.call(this);
1887
+ return;
1888
+ } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
1889
+ cache.callbacks.push($.proxy(function () {
1890
+ this.sourceData = cache.sourceData;
1891
+ success.call(this);
1892
+ }, this));
1893
+
1894
+ //also collecting error callbacks
1895
+ cache.err_callbacks.push($.proxy(error, this));
1896
+ return;
1897
+ } else { //no cache yet, activate it
1898
+ cache.loading = true;
1899
+ cache.callbacks = [];
1900
+ cache.err_callbacks = [];
1901
+ }
1902
+ }
1903
+
1904
+ //loading sourceData from server
1905
+ $.ajax({
1906
+ url: this.options.source,
1907
+ type: 'get',
1908
+ cache: false,
1909
+ data: this.options.name ? {name: this.options.name} : {},
1910
+ dataType: 'json',
1911
+ success: $.proxy(function (data) {
1912
+ if(cache) {
1913
+ cache.loading = false;
1914
+ }
1915
+ this.sourceData = this.makeArray(data);
1916
+ if($.isArray(this.sourceData)) {
1917
+ this.doPrepend();
1918
+ success.call(this);
1919
+ if(cache) {
1920
+ //store result in cache
1921
+ cache.sourceData = this.sourceData;
1922
+ $.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields
1923
+ }
1924
+ } else {
1925
+ error.call(this);
1926
+ if(cache) {
1927
+ $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
1928
+ }
1929
+ }
1930
+ }, this),
1931
+ error: $.proxy(function () {
1932
+ error.call(this);
1933
+ if(cache) {
1934
+ cache.loading = false;
1935
+ //run error callbacks for other fields
1936
+ $.each(cache.err_callbacks, function () { this.call(); });
1937
+ }
1938
+ }, this)
1939
+ });
1940
+ } else { //options as json/array
1941
+ this.sourceData = this.makeArray(this.options.source);
1942
+ if($.isArray(this.sourceData)) {
1943
+ this.doPrepend();
1944
+ success.call(this);
1945
+ } else {
1946
+ error.call(this);
1947
+ }
1948
+ }
1949
+ },
1950
+
1951
+ doPrepend: function () {
1952
+ if(this.options.prepend === null || this.options.prepend === undefined) {
1953
+ return;
1954
+ }
1955
+
1956
+ if(!$.isArray(this.prependData)) {
1957
+ //try parse json in single quotes
1958
+ this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);
1959
+ if (typeof this.options.prepend === 'string') {
1960
+ this.options.prepend = {'': this.options.prepend};
1961
+ }
1962
+ this.prependData = this.makeArray(this.options.prepend);
1963
+ }
1964
+
1965
+ if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
1966
+ this.sourceData = this.prependData.concat(this.sourceData);
1967
+ }
1968
+ },
1969
+
1970
+ /*
1971
+ renders input list
1972
+ */
1973
+ renderList: function() {
1974
+ // this method should be overwritten in child class
1975
+ },
1976
+
1977
+ /*
1978
+ set element's html by value
1979
+ */
1980
+ value2htmlFinal: function(value, element) {
1981
+ // this method should be overwritten in child class
1982
+ },
1983
+
1984
+ /**
1985
+ * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
1986
+ */
1987
+ makeArray: function(data) {
1988
+ var count, obj, result = [], iterateEl;
1989
+ if(!data || typeof data === 'string') {
1990
+ return null;
1991
+ }
1992
+
1993
+ if($.isArray(data)) { //array
1994
+ iterateEl = function (k, v) {
1995
+ obj = {value: k, text: v};
1996
+ if(count++ >= 2) {
1997
+ return false;// exit each if object has more than one value
1998
+ }
1999
+ };
2000
+
2001
+ for(var i = 0; i < data.length; i++) {
2002
+ if(typeof data[i] === 'object') {
2003
+ count = 0;
2004
+ $.each(data[i], iterateEl);
2005
+ if(count === 1) {
2006
+ result.push(obj);
2007
+ } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
2008
+ result.push(data[i]);
2009
+ } else {
2010
+ //data contains incorrect objects
2011
+ }
2012
+ } else {
2013
+ result.push({value: data[i], text: data[i]});
2014
+ }
2015
+ }
2016
+ } else { //object
2017
+ $.each(data, function (k, v) {
2018
+ result.push({value: k, text: v});
2019
+ });
2020
+ }
2021
+ return result;
2022
+ },
2023
+
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
+ }
2034
+ }
2035
+ }
2036
+
2037
+ });
2038
+
2039
+ List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2040
+ /**
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.
2045
+
2046
+ @property source
2047
+ @type string|array|object
2048
+ @default null
2049
+ **/
2050
+ source:null,
2051
+ /**
2052
+ Data automatically prepended to the beginning of dropdown list.
2053
+
2054
+ @property prepend
2055
+ @type string|array|object
2056
+ @default false
2057
+ **/
2058
+ prepend:false,
2059
+ /**
2060
+ Error message when list cannot be loaded (e.g. ajax error)
2061
+
2062
+ @property sourceError
2063
+ @type string
2064
+ @default Error when loading list
2065
+ **/
2066
+ sourceError: 'Error when loading list',
2067
+ /**
2068
+ if <code>true</code> and source is **string url** - results will be cached for fields with the same source and name.
2069
+ Usefull for editable grids.
2070
+
2071
+ @property sourceCache
2072
+ @type boolean
2073
+ @default true
2074
+ @since 1.2.0
2075
+ **/
2076
+ sourceCache: true
2077
+ });
2078
+
2079
+ $.fn.editabletypes.list = List;
2080
+
2081
+ }(window.jQuery));
2082
+ /**
2083
+ Text input
2084
+
2085
+ @class text
2086
+ @extends abstractinput
2087
+ @final
2088
+ @example
2089
+ <a href="#" id="username" data-type="text" data-pk="1">awesome</a>
2090
+ <script>
2091
+ $(function(){
2092
+ $('#username').editable({
2093
+ url: '/post',
2094
+ title: 'Enter username'
2095
+ });
2096
+ });
2097
+ </script>
2098
+ **/
2099
+ (function ($) {
2100
+ var Text = function (options) {
2101
+ this.init('text', options, Text.defaults);
2102
+ };
2103
+
2104
+ $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
2105
+
2106
+ $.extend(Text.prototype, {
2107
+ activate: function() {
2108
+ if(this.$input.is(':visible')) {
2109
+ this.$input.focus();
2110
+ $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2111
+ }
2112
+ }
2113
+ });
2114
+
2115
+ Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2116
+ /**
2117
+ @property tpl
2118
+ @default <input type="text">
2119
+ **/
2120
+ tpl: '<input type="text">',
2121
+ /**
2122
+ Placeholder attribute of input. Shown when input is empty.
2123
+
2124
+ @property placeholder
2125
+ @type string
2126
+ @default null
2127
+ **/
2128
+ placeholder: null
2129
+ });
2130
+
2131
+ $.fn.editabletypes.text = Text;
2132
+
2133
+ }(window.jQuery));
2134
+
2135
+ /**
2136
+ Textarea input
2137
+
2138
+ @class textarea
2139
+ @extends abstractinput
2140
+ @final
2141
+ @example
2142
+ <a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a>
2143
+ <script>
2144
+ $(function(){
2145
+ $('#comments').editable({
2146
+ url: '/post',
2147
+ title: 'Enter comments'
2148
+ });
2149
+ });
2150
+ </script>
2151
+ **/
2152
+ (function ($) {
2153
+
2154
+ var Textarea = function (options) {
2155
+ this.init('textarea', options, Textarea.defaults);
2156
+ };
2157
+
2158
+ $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput);
2159
+
2160
+ $.extend(Textarea.prototype, {
2161
+ render: function () {
2162
+ Textarea.superclass.render.call(this);
2163
+
2164
+ //ctrl + enter
2165
+ this.$input.keydown(function (e) {
2166
+ if (e.ctrlKey && e.which === 13) {
2167
+ $(this).closest('form').submit();
2168
+ }
2169
+ });
2170
+ },
2171
+
2172
+ value2html: function(value, element) {
2173
+ var html = '', lines;
2174
+ if(value) {
2175
+ lines = value.split("\n");
2176
+ for (var i = 0; i < lines.length; i++) {
2177
+ lines[i] = $('<div>').text(lines[i]).html();
2178
+ }
2179
+ html = lines.join('<br>');
2180
+ }
2181
+ $(element).html(html);
2182
+ },
2183
+
2184
+ html2value: function(html) {
2185
+ if(!html) {
2186
+ return '';
2187
+ }
2188
+ var lines = html.split(/<br\s*\/?>/i);
2189
+ for (var i = 0; i < lines.length; i++) {
2190
+ lines[i] = $('<div>').html(lines[i]).text();
2191
+ }
2192
+ return lines.join("\n");
2193
+ },
2194
+
2195
+ 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
+ }
2201
+ });
2202
+
2203
+ Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2204
+ /**
2205
+ @property tpl
2206
+ @default <textarea></textarea>
2207
+ **/
2208
+ tpl:'<textarea></textarea>',
2209
+ /**
2210
+ @property inputclass
2211
+ @default input-large
2212
+ **/
2213
+ inputclass: 'input-large',
2214
+ /**
2215
+ Placeholder attribute of input. Shown when input is empty.
2216
+
2217
+ @property placeholder
2218
+ @type string
2219
+ @default null
2220
+ **/
2221
+ placeholder: null
2222
+ });
2223
+
2224
+ $.fn.editabletypes.textarea = Textarea;
2225
+
2226
+ }(window.jQuery));
2227
+
2228
+ /**
2229
+ Select (dropdown)
2230
+
2231
+ @class select
2232
+ @extends list
2233
+ @final
2234
+ @example
2235
+ <a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a>
2236
+ <script>
2237
+ $(function(){
2238
+ $('#status').editable({
2239
+ value: 2,
2240
+ source: [
2241
+ {value: 1, text: 'Active'},
2242
+ {value: 2, text: 'Blocked'},
2243
+ {value: 3, text: 'Deleted'}
2244
+ ]
2245
+ }
2246
+ });
2247
+ });
2248
+ </script>
2249
+ **/
2250
+ (function ($) {
2251
+
2252
+ var Select = function (options) {
2253
+ this.init('select', options, Select.defaults);
2254
+ };
2255
+
2256
+ $.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
2257
+
2258
+ $.extend(Select.prototype, {
2259
+ renderList: function() {
2260
+ if(!$.isArray(this.sourceData)) {
2261
+ return;
2262
+ }
2263
+
2264
+ for(var i=0; i<this.sourceData.length; i++) {
2265
+ this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text));
2266
+ }
2267
+
2268
+ //enter submit
2269
+ this.$input.on('keydown.editable', function (e) {
2270
+ if (e.which === 13) {
2271
+ $(this).closest('form').submit();
2272
+ }
2273
+ });
2274
+ },
2275
+
2276
+ value2htmlFinal: function(value, element) {
2277
+ var text = '', item = this.itemByVal(value);
2278
+ if(item) {
2279
+ text = item.text;
2280
+ }
2281
+ Select.superclass.constructor.superclass.value2html(text, element);
2282
+ },
2283
+
2284
+ autosubmit: function() {
2285
+ this.$input.off('keydown.editable').on('change.editable', function(){
2286
+ $(this).closest('form').submit();
2287
+ });
2288
+ }
2289
+ });
2290
+
2291
+ Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
2292
+ /**
2293
+ @property tpl
2294
+ @default <select></select>
2295
+ **/
2296
+ tpl:'<select></select>'
2297
+ });
2298
+
2299
+ $.fn.editabletypes.select = Select;
2300
+
2301
+ }(window.jQuery));
2302
+ /**
2303
+ List of checkboxes.
2304
+ Internally value stored as javascript array of values.
2305
+
2306
+ @class checklist
2307
+ @extends list
2308
+ @final
2309
+ @example
2310
+ <a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a>
2311
+ <script>
2312
+ $(function(){
2313
+ $('#options').editable({
2314
+ value: [2, 3],
2315
+ source: [
2316
+ {value: 1, text: 'option1'},
2317
+ {value: 2, text: 'option2'},
2318
+ {value: 3, text: 'option3'}
2319
+ ]
2320
+ }
2321
+ });
2322
+ });
2323
+ </script>
2324
+ **/
2325
+ (function ($) {
2326
+
2327
+ var Checklist = function (options) {
2328
+ this.init('checklist', options, Checklist.defaults);
2329
+ };
2330
+
2331
+ $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list);
2332
+
2333
+ $.extend(Checklist.prototype, {
2334
+ renderList: function() {
2335
+ var $label, $div;
2336
+ if(!$.isArray(this.sourceData)) {
2337
+ return;
2338
+ }
2339
+
2340
+ for(var i=0; i<this.sourceData.length; i++) {
2341
+ $label = $('<label>').append($('<input>', {
2342
+ type: 'checkbox',
2343
+ value: this.sourceData[i].value,
2344
+ name: this.options.name
2345
+ }))
2346
+ .append($('<span>').text(' '+this.sourceData[i].text));
2347
+
2348
+ $('<div>').append($label).appendTo(this.$input);
2349
+ }
2350
+ },
2351
+
2352
+ value2str: function(value) {
2353
+ return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : '';
2354
+ },
2355
+
2356
+ //parse separated string
2357
+ str2value: function(str) {
2358
+ var reg, value = null;
2359
+ if(typeof str === 'string' && str.length) {
2360
+ reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*');
2361
+ value = str.split(reg);
2362
+ } else if($.isArray(str)) {
2363
+ value = str;
2364
+ }
2365
+ return value;
2366
+ },
2367
+
2368
+ //set checked on required checkboxes
2369
+ value2input: function(value) {
2370
+ var $checks = this.$input.find('input[type="checkbox"]');
2371
+ $checks.removeAttr('checked');
2372
+ if($.isArray(value) && value.length) {
2373
+ $checks.each(function(i, el) {
2374
+ var $el = $(el);
2375
+ // cannot use $.inArray as it performs strict comparison
2376
+ $.each(value, function(j, val){
2377
+ /*jslint eqeq: true*/
2378
+ if($el.val() == val) {
2379
+ /*jslint eqeq: false*/
2380
+ $el.attr('checked', 'checked');
2381
+ }
2382
+ });
2383
+ });
2384
+ }
2385
+ },
2386
+
2387
+ input2value: function() {
2388
+ var checked = [];
2389
+ this.$input.find('input:checked').each(function(i, el) {
2390
+ checked.push($(el).val());
2391
+ });
2392
+ return checked;
2393
+ },
2394
+
2395
+ //collect text of checked boxes
2396
+ value2htmlFinal: function(value, element) {
2397
+ 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*/
2403
+ if(checked.length) {
2404
+ $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
2405
+ $(element).html(html.join('<br>'));
2406
+ } else {
2407
+ $(element).empty();
2408
+ }
2409
+ },
2410
+
2411
+ activate: function() {
2412
+ this.$input.find('input[type="checkbox"]').first().focus();
2413
+ },
2414
+
2415
+ autosubmit: function() {
2416
+ this.$input.find('input[type="checkbox"]').on('keydown', function(e){
2417
+ if (e.which === 13) {
2418
+ $(this).closest('form').submit();
2419
+ }
2420
+ });
2421
+ }
2422
+ });
2423
+
2424
+ Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
2425
+ /**
2426
+ @property tpl
2427
+ @default <div></div>
2428
+ **/
2429
+ tpl:'<div></div>',
2430
+
2431
+ /**
2432
+ @property inputclass
2433
+ @type string
2434
+ @default editable-checklist
2435
+ **/
2436
+ inputclass: 'editable-checklist',
2437
+
2438
+ /**
2439
+ Separator of values when reading from 'data-value' string
2440
+
2441
+ @property separator
2442
+ @type string
2443
+ @default ', '
2444
+ **/
2445
+ separator: ','
2446
+ });
2447
+
2448
+ $.fn.editabletypes.checklist = Checklist;
2449
+
2450
+ }(window.jQuery));
2451
+
2452
+ /**
2453
+ HTML5 input types.
2454
+ Following types are supported:
2455
+
2456
+ * password
2457
+ * email
2458
+ * url
2459
+ * tel
2460
+ * number
2461
+ * range
2462
+
2463
+ Learn more about html5 inputs:
2464
+ http://www.w3.org/wiki/HTML5_form_additions
2465
+ To check browser compatibility please see:
2466
+ https://developer.mozilla.org/en-US/docs/HTML/Element/Input
2467
+
2468
+ @class html5types
2469
+ @extends text
2470
+ @final
2471
+ @since 1.3.0
2472
+ @example
2473
+ <a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a>
2474
+ <script>
2475
+ $(function(){
2476
+ $('#email').editable({
2477
+ url: '/post',
2478
+ title: 'Enter email'
2479
+ });
2480
+ });
2481
+ </script>
2482
+ **/
2483
+
2484
+ /**
2485
+ @property tpl
2486
+ @default depends on type
2487
+ **/
2488
+
2489
+ /*
2490
+ Password
2491
+ */
2492
+ (function ($) {
2493
+ var Password = function (options) {
2494
+ this.init('password', options, Password.defaults);
2495
+ };
2496
+ $.fn.editableutils.inherit(Password, $.fn.editabletypes.text);
2497
+ $.extend(Password.prototype, {
2498
+ //do not display password, show '[hidden]' instead
2499
+ value2html: function(value, element) {
2500
+ if(value) {
2501
+ $(element).text('[hidden]');
2502
+ } else {
2503
+ $(element).empty();
2504
+ }
2505
+ },
2506
+ //as password not displayed, should not set value by html
2507
+ html2value: function(html) {
2508
+ return null;
2509
+ }
2510
+ });
2511
+ Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2512
+ tpl: '<input type="password">'
2513
+ });
2514
+ $.fn.editabletypes.password = Password;
2515
+ }(window.jQuery));
2516
+
2517
+
2518
+ /*
2519
+ Email
2520
+ */
2521
+ (function ($) {
2522
+ var Email = function (options) {
2523
+ this.init('email', options, Email.defaults);
2524
+ };
2525
+ $.fn.editableutils.inherit(Email, $.fn.editabletypes.text);
2526
+ Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2527
+ tpl: '<input type="email">'
2528
+ });
2529
+ $.fn.editabletypes.email = Email;
2530
+ }(window.jQuery));
2531
+
2532
+
2533
+ /*
2534
+ Url
2535
+ */
2536
+ (function ($) {
2537
+ var Url = function (options) {
2538
+ this.init('url', options, Url.defaults);
2539
+ };
2540
+ $.fn.editableutils.inherit(Url, $.fn.editabletypes.text);
2541
+ Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2542
+ tpl: '<input type="url">'
2543
+ });
2544
+ $.fn.editabletypes.url = Url;
2545
+ }(window.jQuery));
2546
+
2547
+
2548
+ /*
2549
+ Tel
2550
+ */
2551
+ (function ($) {
2552
+ var Tel = function (options) {
2553
+ this.init('tel', options, Tel.defaults);
2554
+ };
2555
+ $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text);
2556
+ Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2557
+ tpl: '<input type="tel">'
2558
+ });
2559
+ $.fn.editabletypes.tel = Tel;
2560
+ }(window.jQuery));
2561
+
2562
+
2563
+ /*
2564
+ Number
2565
+ */
2566
+ (function ($) {
2567
+ var NumberInput = function (options) {
2568
+ this.init('number', options, NumberInput.defaults);
2569
+ };
2570
+ $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text);
2571
+ $.extend(NumberInput.prototype, {
2572
+ render: function () {
2573
+ 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
+ }
2586
+ }
2587
+ });
2588
+ NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2589
+ tpl: '<input type="number">',
2590
+ inputclass: 'input-mini',
2591
+ min: null,
2592
+ max: null,
2593
+ step: null
2594
+ });
2595
+ $.fn.editabletypes.number = NumberInput;
2596
+ }(window.jQuery));
2597
+
2598
+
2599
+ /*
2600
+ Range (inherit from number)
2601
+ */
2602
+ (function ($) {
2603
+ var Range = function (options) {
2604
+ this.init('range', options, Range.defaults);
2605
+ };
2606
+ $.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
2607
+ $.extend(Range.prototype, {
2608
+ 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
+ }
2621
+
2622
+ if (this.options.step !== null) {
2623
+ $slider.attr('step', this.options.step);
2624
+ }
2625
+
2626
+ $slider.on('input', function(){
2627
+ $(this).siblings('output').text($(this).val());
2628
+ });
2629
+ },
2630
+ activate: function() {
2631
+ this.$input.filter('input').focus();
2632
+ }
2633
+ });
2634
+ Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
2635
+ tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>',
2636
+ inputclass: 'input-medium'
2637
+ });
2638
+ $.fn.editabletypes.range = Range;
2639
+ }(window.jQuery));
2640
+ /*
2641
+ Editableform based on Twitter Bootstrap
2642
+ */
2643
+ (function ($) {
2644
+
2645
+ $.extend($.fn.editableform.Constructor.prototype, {
2646
+ initTemplate: function() {
2647
+ this.$form = $($.fn.editableform.template);
2648
+ this.$form.find('.editable-error-block').addClass('help-block');
2649
+ }
2650
+ });
2651
+
2652
+ //buttons
2653
+ $.fn.editableform.buttons = '<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button>'+
2654
+ '<button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>';
2655
+
2656
+ //error classes
2657
+ $.fn.editableform.errorGroupClass = 'error';
2658
+ $.fn.editableform.errorBlockClass = null;
2659
+
2660
+ }(window.jQuery));
2661
+ /**
2662
+ * Editable Popover
2663
+ * ---------------------
2664
+ * requires bootstrap-popover.js
2665
+ */
2666
+ (function ($) {
2667
+
2668
+ //extend methods
2669
+ $.extend($.fn.editableContainer.Constructor.prototype, {
2670
+ containerName: 'popover',
2671
+ //for compatibility with bootstrap <= 2.2.1 (content inserted into <p> instead of directly .popover-content)
2672
+ innerCss: $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content',
2673
+
2674
+ initContainer: function(){
2675
+ $.extend(this.containerOptions, {
2676
+ trigger: 'manual',
2677
+ selector: false,
2678
+ content: ' '
2679
+ });
2680
+ this.call(this.containerOptions);
2681
+ },
2682
+
2683
+ setContainerOption: function(key, value) {
2684
+ this.container().options[key] = value;
2685
+ },
2686
+
2687
+ /**
2688
+ * move popover to new position. This function mainly copied from bootstrap-popover.
2689
+ */
2690
+ /*jshint laxcomma: true*/
2691
+ setPosition: function () {
2692
+
2693
+ (function() {
2694
+ var $tip = this.tip()
2695
+ , inside
2696
+ , pos
2697
+ , actualWidth
2698
+ , actualHeight
2699
+ , placement
2700
+ , tp;
2701
+
2702
+ placement = typeof this.options.placement === 'function' ?
2703
+ this.options.placement.call(this, $tip[0], this.$element[0]) :
2704
+ this.options.placement;
2705
+
2706
+ inside = /in/.test(placement);
2707
+
2708
+ $tip
2709
+ // .detach()
2710
+ //vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover
2711
+ .removeClass('top right bottom left')
2712
+ .css({ top: 0, left: 0, display: 'block' });
2713
+ // .insertAfter(this.$element);
2714
+
2715
+ pos = this.getPosition(inside);
2716
+
2717
+ actualWidth = $tip[0].offsetWidth;
2718
+ actualHeight = $tip[0].offsetHeight;
2719
+
2720
+ switch (inside ? placement.split(' ')[1] : placement) {
2721
+ case 'bottom':
2722
+ tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2};
2723
+ break;
2724
+ case 'top':
2725
+ tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2};
2726
+ break;
2727
+ case 'left':
2728
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth};
2729
+ break;
2730
+ case 'right':
2731
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width};
2732
+ break;
2733
+ }
2734
+
2735
+ $tip
2736
+ .offset(tp)
2737
+ .addClass(placement)
2738
+ .addClass('in');
2739
+
2740
+ }).call(this.container());
2741
+ /*jshint laxcomma: false*/
2742
+ }
2743
+ });
2744
+
2745
+ //defaults
2746
+ /*
2747
+ $.fn.editableContainer.defaults = $.extend({}, $.fn.popover.defaults, $.fn.editableContainer.defaults, {
2748
+
2749
+ });
2750
+ */
2751
+
2752
+ }(window.jQuery));
2753
+ /**
2754
+ 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
2757
+
2758
+ @class date
2759
+ @extends abstractinput
2760
+ @final
2761
+ @example
2762
+ <a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a>
2763
+ <script>
2764
+ $(function(){
2765
+ $('#dob').editable({
2766
+ format: 'yyyy-mm-dd',
2767
+ viewformat: 'dd/mm/yyyy',
2768
+ datepicker: {
2769
+ weekStart: 1
2770
+ }
2771
+ }
2772
+ });
2773
+ });
2774
+ </script>
2775
+ **/
2776
+ (function ($) {
2777
+
2778
+ var Date = function (options) {
2779
+ 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);
2801
+ };
2802
+
2803
+ $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput);
2804
+
2805
+ $.extend(Date.prototype, {
2806
+ render: function () {
2807
+ Date.superclass.render.call(this);
2808
+ this.$input.datepicker(this.options.datepicker);
2809
+
2810
+ if(this.options.clear) {
2811
+ this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
2812
+ e.preventDefault();
2813
+ e.stopPropagation();
2814
+ this.clear();
2815
+ }, this));
2816
+ }
2817
+ },
2818
+
2819
+ value2html: function(value, element) {
2820
+ var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
2821
+ Date.superclass.value2html(text, element);
2822
+ },
2823
+
2824
+ html2value: function(html) {
2825
+ return html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datepicker.language) : null;
2826
+ },
2827
+
2828
+ value2str: function(value) {
2829
+ return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : '';
2830
+ },
2831
+
2832
+ str2value: function(str) {
2833
+ return str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datepicker.language) : null;
2834
+ },
2835
+
2836
+ value2submit: function(value) {
2837
+ return this.value2str(value);
2838
+ },
2839
+
2840
+ value2input: function(value) {
2841
+ this.$input.datepicker('update', value);
2842
+ },
2843
+
2844
+ input2value: function() {
2845
+ return this.$input.data('datepicker').date;
2846
+ },
2847
+
2848
+ activate: function() {
2849
+ },
2850
+
2851
+ clear: function() {
2852
+ this.$input.data('datepicker').date = null;
2853
+ this.$input.find('.active').removeClass('active');
2854
+ },
2855
+
2856
+ autosubmit: function() {
2857
+ this.$input.on('changeDate', function(e){
2858
+ var $form = $(this).closest('form');
2859
+ setTimeout(function() {
2860
+ $form.submit();
2861
+ }, 200);
2862
+ });
2863
+ }
2864
+
2865
+ });
2866
+
2867
+ Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2868
+ /**
2869
+ @property tpl
2870
+ @default <div></div>
2871
+ **/
2872
+ tpl:'<div></div>',
2873
+ /**
2874
+ @property inputclass
2875
+ @default editable-date well
2876
+ **/
2877
+ inputclass: 'editable-date well',
2878
+ /**
2879
+ Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
2880
+ Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code>
2881
+
2882
+ @property format
2883
+ @type string
2884
+ @default yyyy-mm-dd
2885
+ **/
2886
+ format:'yyyy-mm-dd',
2887
+ /**
2888
+ Format used for displaying date. Also applied when converting date from element's text on init.
2889
+ If not specified equals to <code>format</code>
2890
+
2891
+ @property viewformat
2892
+ @type string
2893
+ @default null
2894
+ **/
2895
+ viewformat: null,
2896
+ /**
2897
+ Configuration of datepicker.
2898
+ Full list of options: http://vitalets.github.com/bootstrap-datepicker
2899
+
2900
+ @property datepicker
2901
+ @type object
2902
+ @default {
2903
+ weekStart: 0,
2904
+ startView: 0,
2905
+ autoclose: false
2906
+ }
2907
+ **/
2908
+ datepicker:{
2909
+ weekStart: 0,
2910
+ startView: 0,
2911
+ autoclose: false
2912
+ },
2913
+ /**
2914
+ Text shown as clear date button.
2915
+ If <code>false</code> clear button will not be rendered.
2916
+
2917
+ @property clear
2918
+ @type boolean|string
2919
+ @default 'x clear'
2920
+ **/
2921
+ clear: '&times; clear'
2922
+ });
2923
+
2924
+ $.fn.editabletypes.date = Date;
2925
+
2926
+ }(window.jQuery));
2927
+
2928
+ /* =========================================================
2929
+ * bootstrap-datepicker.js
2930
+ * http://www.eyecon.ro/bootstrap-datepicker
2931
+ * =========================================================
2932
+ * Copyright 2012 Stefan Petre
2933
+ * Improvements by Andrew Rowls
2934
+ *
2935
+ * Licensed under the Apache License, Version 2.0 (the "License");
2936
+ * you may not use this file except in compliance with the License.
2937
+ * You may obtain a copy of the License at
2938
+ *
2939
+ * http://www.apache.org/licenses/LICENSE-2.0
2940
+ *
2941
+ * Unless required by applicable law or agreed to in writing, software
2942
+ * distributed under the License is distributed on an "AS IS" BASIS,
2943
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2944
+ * See the License for the specific language governing permissions and
2945
+ * limitations under the License.
2946
+ * ========================================================= */
2947
+
2948
+ !function( $ ) {
2949
+
2950
+ function UTCDate(){
2951
+ return new Date(Date.UTC.apply(Date, arguments));
2952
+ }
2953
+ function UTCToday(){
2954
+ var today = new Date();
2955
+ return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
2956
+ }
2957
+
2958
+ // Picker object
2959
+
2960
+ var Datepicker = function(element, options) {
2961
+ var that = this;
2962
+
2963
+ this.element = $(element);
2964
+ this.language = options.language||this.element.data('date-language')||"en";
2965
+ this.language = this.language in dates ? this.language : "en";
2966
+ this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
2967
+ this.isInline = false;
2968
+ this.isInput = this.element.is('input');
2969
+ this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
2970
+ this.hasInput = this.component && this.element.find('input').length;
2971
+ if(this.component && this.component.length === 0)
2972
+ this.component = false;
2973
+
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
+ }
3007
+
3008
+ $(document).on('mousedown', function (e) {
3009
+ // Clicked outside the datepicker, hide it
3010
+ if ($(e.target).closest('.datepicker').length == 0) {
3011
+ that.hide();
3012
+ }
3013
+ });
3014
+
3015
+ this.autoclose = false;
3016
+ if ('autoclose' in options) {
3017
+ this.autoclose = options.autoclose;
3018
+ } else if ('dateAutoclose' in this.element.data()) {
3019
+ this.autoclose = this.element.data('date-autoclose');
3020
+ }
3021
+
3022
+ this.keyboardNavigation = true;
3023
+ if ('keyboardNavigation' in options) {
3024
+ this.keyboardNavigation = options.keyboardNavigation;
3025
+ } else if ('dateKeyboardNavigation' in this.element.data()) {
3026
+ this.keyboardNavigation = this.element.data('date-keyboard-navigation');
3027
+ }
3028
+
3029
+ switch(options.startView || this.element.data('date-start-view')){
3030
+ case 2:
3031
+ case 'decade':
3032
+ this.viewMode = this.startViewMode = 2;
3033
+ break;
3034
+ case 1:
3035
+ case 'year':
3036
+ this.viewMode = this.startViewMode = 1;
3037
+ break;
3038
+ case 0:
3039
+ case 'month':
3040
+ default:
3041
+ this.viewMode = this.startViewMode = 0;
3042
+ break;
3043
+ }
3044
+
3045
+ this.todayBtn = (options.todayBtn||this.element.data('date-today-btn')||false);
3046
+ this.todayHighlight = (options.todayHighlight||this.element.data('date-today-highlight')||false);
3047
+
3048
+ this.weekStart = ((options.weekStart||this.element.data('date-weekstart')||dates[this.language].weekStart||0) % 7);
3049
+ this.weekEnd = ((this.weekStart + 6) % 7);
3050
+ this.startDate = -Infinity;
3051
+ this.endDate = Infinity;
3052
+ this.setStartDate(options.startDate||this.element.data('date-startdate'));
3053
+ this.setEndDate(options.endDate||this.element.data('date-enddate'));
3054
+ this.fillDow();
3055
+ this.fillMonths();
3056
+ this.update();
3057
+ this.showMode();
3058
+
3059
+ if(this.isInline) {
3060
+ this.show();
3061
+ }
3062
+ };
3063
+
3064
+ Datepicker.prototype = {
3065
+ constructor: Datepicker,
3066
+
3067
+ show: function(e) {
3068
+ this.picker.show();
3069
+ this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
3070
+ this.update();
3071
+ this.place();
3072
+ $(window).on('resize', $.proxy(this.place, this));
3073
+ if (e ) {
3074
+ e.stopPropagation();
3075
+ e.preventDefault();
3076
+ }
3077
+ this.element.trigger({
3078
+ type: 'show',
3079
+ date: this.date
3080
+ });
3081
+ },
3082
+
3083
+ hide: function(e){
3084
+ if(this.isInline) return;
3085
+ this.picker.hide();
3086
+ $(window).off('resize', this.place);
3087
+ this.viewMode = this.startViewMode;
3088
+ this.showMode();
3089
+ if (!this.isInput) {
3090
+ $(document).off('mousedown', this.hide);
3091
+ }
3092
+ if (e && e.currentTarget.value)
3093
+ this.setValue();
3094
+ this.element.trigger({
3095
+ type: 'hide',
3096
+ date: this.date
3097
+ });
3098
+ },
3099
+
3100
+ getDate: function() {
3101
+ var d = this.getUTCDate();
3102
+ return new Date(d.getTime() + (d.getTimezoneOffset()*60000))
3103
+ },
3104
+
3105
+ getUTCDate: function() {
3106
+ return this.date;
3107
+ },
3108
+
3109
+ setDate: function(d) {
3110
+ this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000)));
3111
+ },
3112
+
3113
+ setUTCDate: function(d) {
3114
+ this.date = d;
3115
+ this.setValue();
3116
+ },
3117
+
3118
+ setValue: function() {
3119
+ var formatted = this.getFormattedDate();
3120
+ if (!this.isInput) {
3121
+ if (this.component){
3122
+ this.element.find('input').prop('value', formatted);
3123
+ }
3124
+ this.element.data('date', formatted);
3125
+ } else {
3126
+ this.element.prop('value', formatted);
3127
+ }
3128
+ },
3129
+
3130
+ getFormattedDate: function(format) {
3131
+ if(format == undefined) format = this.format;
3132
+ return DPGlobal.formatDate(this.date, format, this.language);
3133
+ },
3134
+
3135
+ setStartDate: function(startDate){
3136
+ this.startDate = startDate||-Infinity;
3137
+ if (this.startDate !== -Infinity) {
3138
+ this.startDate = DPGlobal.parseDate(this.startDate, this.format, this.language);
3139
+ }
3140
+ this.update();
3141
+ this.updateNavArrows();
3142
+ },
3143
+
3144
+ setEndDate: function(endDate){
3145
+ this.endDate = endDate||Infinity;
3146
+ if (this.endDate !== Infinity) {
3147
+ this.endDate = DPGlobal.parseDate(this.endDate, this.format, this.language);
3148
+ }
3149
+ this.update();
3150
+ this.updateNavArrows();
3151
+ },
3152
+
3153
+ place: function(){
3154
+ if(this.isInline) return;
3155
+ var zIndex = parseInt(this.element.parents().filter(function() {
3156
+ return $(this).css('z-index') != 'auto';
3157
+ }).first().css('z-index'))+10;
3158
+ var offset = this.component ? this.component.offset() : this.element.offset();
3159
+ this.picker.css({
3160
+ top: offset.top + this.height,
3161
+ left: offset.left,
3162
+ zIndex: zIndex
3163
+ });
3164
+ },
3165
+
3166
+ 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
+ }
3174
+
3175
+ this.date = DPGlobal.parseDate(date, this.format, this.language);
3176
+
3177
+ if(fromArgs) this.setValue();
3178
+
3179
+ if (this.date < this.startDate) {
3180
+ this.viewDate = new Date(this.startDate);
3181
+ } else if (this.date > this.endDate) {
3182
+ this.viewDate = new Date(this.endDate);
3183
+ } else {
3184
+ this.viewDate = new Date(this.date);
3185
+ }
3186
+ this.fill();
3187
+ },
3188
+
3189
+ fillDow: function(){
3190
+ var dowCnt = this.weekStart;
3191
+ var html = '<tr>';
3192
+ while (dowCnt < this.weekStart + 7) {
3193
+ html += '<th class="dow">'+dates[this.language].daysMin[(dowCnt++)%7]+'</th>';
3194
+ }
3195
+ html += '</tr>';
3196
+ this.picker.find('.datepicker-days thead').append(html);
3197
+ },
3198
+
3199
+ fillMonths: function(){
3200
+ var html = '';
3201
+ var i = 0
3202
+ while (i < 12) {
3203
+ html += '<span class="month">'+dates[this.language].monthsShort[i++]+'</span>';
3204
+ }
3205
+ this.picker.find('.datepicker-months td').html(html);
3206
+ },
3207
+
3208
+ fill: function() {
3209
+ var d = new Date(this.viewDate),
3210
+ year = d.getUTCFullYear(),
3211
+ month = d.getUTCMonth(),
3212
+ startYear = this.startDate !== -Infinity ? this.startDate.getUTCFullYear() : -Infinity,
3213
+ startMonth = this.startDate !== -Infinity ? this.startDate.getUTCMonth() : -Infinity,
3214
+ endYear = this.endDate !== Infinity ? this.endDate.getUTCFullYear() : Infinity,
3215
+ endMonth = this.endDate !== Infinity ? this.endDate.getUTCMonth() : Infinity,
3216
+ currentDate = this.date && this.date.valueOf(),
3217
+ today = new Date();
3218
+ this.picker.find('.datepicker-days thead th:eq(1)')
3219
+ .text(dates[this.language].months[month]+' '+year);
3220
+ this.picker.find('tfoot th.today')
3221
+ .text(dates[this.language].today)
3222
+ .toggle(this.todayBtn !== false);
3223
+ this.updateNavArrows();
3224
+ this.fillMonths();
3225
+ var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
3226
+ day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
3227
+ prevMonth.setUTCDate(day);
3228
+ prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7)%7);
3229
+ var nextMonth = new Date(prevMonth);
3230
+ nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
3231
+ nextMonth = nextMonth.valueOf();
3232
+ var html = [];
3233
+ var clsName;
3234
+ while(prevMonth.valueOf() < nextMonth) {
3235
+ if (prevMonth.getUTCDay() == this.weekStart) {
3236
+ html.push('<tr>');
3237
+ }
3238
+ clsName = '';
3239
+ if (prevMonth.getUTCFullYear() < year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() < month)) {
3240
+ clsName += ' old';
3241
+ } else if (prevMonth.getUTCFullYear() > year || (prevMonth.getUTCFullYear() == year && prevMonth.getUTCMonth() > month)) {
3242
+ clsName += ' new';
3243
+ }
3244
+ // Compare internal UTC date with local today, not UTC today
3245
+ if (this.todayHighlight &&
3246
+ prevMonth.getUTCFullYear() == today.getFullYear() &&
3247
+ prevMonth.getUTCMonth() == today.getMonth() &&
3248
+ prevMonth.getUTCDate() == today.getDate()) {
3249
+ clsName += ' today';
3250
+ }
3251
+ if (currentDate && prevMonth.valueOf() == currentDate) {
3252
+ clsName += ' active';
3253
+ }
3254
+ if (prevMonth.valueOf() < this.startDate || prevMonth.valueOf() > this.endDate) {
3255
+ clsName += ' disabled';
3256
+ }
3257
+ html.push('<td class="day'+clsName+'">'+prevMonth.getUTCDate() + '</td>');
3258
+ if (prevMonth.getUTCDay() == this.weekEnd) {
3259
+ html.push('</tr>');
3260
+ }
3261
+ prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
3262
+ }
3263
+ this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
3264
+ var currentYear = this.date && this.date.getUTCFullYear();
3265
+
3266
+ var months = this.picker.find('.datepicker-months')
3267
+ .find('th:eq(1)')
3268
+ .text(year)
3269
+ .end()
3270
+ .find('span').removeClass('active');
3271
+ if (currentYear && currentYear == year) {
3272
+ months.eq(this.date.getUTCMonth()).addClass('active');
3273
+ }
3274
+ if (year < startYear || year > endYear) {
3275
+ months.addClass('disabled');
3276
+ }
3277
+ if (year == startYear) {
3278
+ months.slice(0, startMonth).addClass('disabled');
3279
+ }
3280
+ if (year == endYear) {
3281
+ months.slice(endMonth+1).addClass('disabled');
3282
+ }
3283
+
3284
+ html = '';
3285
+ year = parseInt(year/10, 10) * 10;
3286
+ var yearCont = this.picker.find('.datepicker-years')
3287
+ .find('th:eq(1)')
3288
+ .text(year + '-' + (year + 9))
3289
+ .end()
3290
+ .find('td');
3291
+ year -= 1;
3292
+ for (var i = -1; i < 11; i++) {
3293
+ html += '<span class="year'+(i == -1 || i == 10 ? ' old' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>';
3294
+ year += 1;
3295
+ }
3296
+ yearCont.html(html);
3297
+ },
3298
+
3299
+ updateNavArrows: function() {
3300
+ var d = new Date(this.viewDate),
3301
+ year = d.getUTCFullYear(),
3302
+ month = d.getUTCMonth();
3303
+ switch (this.viewMode) {
3304
+ case 0:
3305
+ if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear() && month <= this.startDate.getUTCMonth()) {
3306
+ this.picker.find('.prev').css({visibility: 'hidden'});
3307
+ } else {
3308
+ this.picker.find('.prev').css({visibility: 'visible'});
3309
+ }
3310
+ if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear() && month >= this.endDate.getUTCMonth()) {
3311
+ this.picker.find('.next').css({visibility: 'hidden'});
3312
+ } else {
3313
+ this.picker.find('.next').css({visibility: 'visible'});
3314
+ }
3315
+ break;
3316
+ case 1:
3317
+ case 2:
3318
+ if (this.startDate !== -Infinity && year <= this.startDate.getUTCFullYear()) {
3319
+ this.picker.find('.prev').css({visibility: 'hidden'});
3320
+ } else {
3321
+ this.picker.find('.prev').css({visibility: 'visible'});
3322
+ }
3323
+ if (this.endDate !== Infinity && year >= this.endDate.getUTCFullYear()) {
3324
+ this.picker.find('.next').css({visibility: 'hidden'});
3325
+ } else {
3326
+ this.picker.find('.next').css({visibility: 'visible'});
3327
+ }
3328
+ break;
3329
+ }
3330
+ },
3331
+
3332
+ click: function(e) {
3333
+ e.stopPropagation();
3334
+ e.preventDefault();
3335
+ var target = $(e.target).closest('span, td, th');
3336
+ if (target.length == 1) {
3337
+ switch(target[0].nodeName.toLowerCase()) {
3338
+ case 'th':
3339
+ switch(target[0].className) {
3340
+ case 'switch':
3341
+ this.showMode(1);
3342
+ break;
3343
+ case 'prev':
3344
+ case 'next':
3345
+ var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
3346
+ switch(this.viewMode){
3347
+ case 0:
3348
+ this.viewDate = this.moveMonth(this.viewDate, dir);
3349
+ break;
3350
+ case 1:
3351
+ case 2:
3352
+ this.viewDate = this.moveYear(this.viewDate, dir);
3353
+ break;
3354
+ }
3355
+ this.fill();
3356
+ break;
3357
+ case 'today':
3358
+ var date = new Date();
3359
+ date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
3360
+
3361
+ this.showMode(-2);
3362
+ var which = this.todayBtn == 'linked' ? null : 'view';
3363
+ this._setDate(date, which);
3364
+ break;
3365
+ }
3366
+ break;
3367
+ case 'span':
3368
+ if (!target.is('.disabled')) {
3369
+ this.viewDate.setUTCDate(1);
3370
+ if (target.is('.month')) {
3371
+ var month = target.parent().find('span').index(target);
3372
+ this.viewDate.setUTCMonth(month);
3373
+ this.element.trigger({
3374
+ type: 'changeMonth',
3375
+ date: this.viewDate
3376
+ });
3377
+ } else {
3378
+ var year = parseInt(target.text(), 10)||0;
3379
+ this.viewDate.setUTCFullYear(year);
3380
+ this.element.trigger({
3381
+ type: 'changeYear',
3382
+ date: this.viewDate
3383
+ });
3384
+ }
3385
+ this.showMode(-1);
3386
+ this.fill();
3387
+ }
3388
+ break;
3389
+ case 'td':
3390
+ if (target.is('.day') && !target.is('.disabled')){
3391
+ var day = parseInt(target.text(), 10)||1;
3392
+ var year = this.viewDate.getUTCFullYear(),
3393
+ month = this.viewDate.getUTCMonth();
3394
+ if (target.is('.old')) {
3395
+ if (month == 0) {
3396
+ month = 11;
3397
+ year -= 1;
3398
+ } else {
3399
+ month -= 1;
3400
+ }
3401
+ } else if (target.is('.new')) {
3402
+ if (month == 11) {
3403
+ month = 0;
3404
+ year += 1;
3405
+ } else {
3406
+ month += 1;
3407
+ }
3408
+ }
3409
+ this._setDate(UTCDate(year, month, day,0,0,0,0));
3410
+ }
3411
+ break;
3412
+ }
3413
+ }
3414
+ },
3415
+
3416
+ _setDate: function(date, which){
3417
+ if (!which || which == 'date')
3418
+ this.date = date;
3419
+ if (!which || which == 'view')
3420
+ this.viewDate = date;
3421
+ this.fill();
3422
+ this.setValue();
3423
+ this.element.trigger({
3424
+ type: 'changeDate',
3425
+ date: this.date
3426
+ });
3427
+ var element;
3428
+ if (this.isInput) {
3429
+ element = this.element;
3430
+ } else if (this.component){
3431
+ element = this.element.find('input');
3432
+ }
3433
+ if (element) {
3434
+ element.change();
3435
+ if (this.autoclose) {
3436
+ this.hide();
3437
+ }
3438
+ }
3439
+ },
3440
+
3441
+ moveMonth: function(date, dir){
3442
+ if (!dir) return date;
3443
+ var new_date = new Date(date.valueOf()),
3444
+ day = new_date.getUTCDate(),
3445
+ month = new_date.getUTCMonth(),
3446
+ mag = Math.abs(dir),
3447
+ new_month, test;
3448
+ dir = dir > 0 ? 1 : -1;
3449
+ if (mag == 1){
3450
+ test = dir == -1
3451
+ // If going back one month, make sure month is not current month
3452
+ // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
3453
+ ? function(){ return new_date.getUTCMonth() == month; }
3454
+ // If going forward one month, make sure month is as expected
3455
+ // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
3456
+ : function(){ return new_date.getUTCMonth() != new_month; };
3457
+ new_month = month + dir;
3458
+ new_date.setUTCMonth(new_month);
3459
+ // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
3460
+ if (new_month < 0 || new_month > 11)
3461
+ new_month = (new_month + 12) % 12;
3462
+ } else {
3463
+ // For magnitudes >1, move one month at a time...
3464
+ for (var i=0; i<mag; i++)
3465
+ // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
3466
+ new_date = this.moveMonth(new_date, dir);
3467
+ // ...then reset the day, keeping it in the new month
3468
+ new_month = new_date.getUTCMonth();
3469
+ new_date.setUTCDate(day);
3470
+ test = function(){ return new_month != new_date.getUTCMonth(); };
3471
+ }
3472
+ // Common date-resetting loop -- if date is beyond end of month, make it
3473
+ // end of month
3474
+ while (test()){
3475
+ new_date.setUTCDate(--day);
3476
+ new_date.setUTCMonth(new_month);
3477
+ }
3478
+ return new_date;
3479
+ },
3480
+
3481
+ moveYear: function(date, dir){
3482
+ return this.moveMonth(date, dir*12);
3483
+ },
3484
+
3485
+ dateWithinRange: function(date){
3486
+ return date >= this.startDate && date <= this.endDate;
3487
+ },
3488
+
3489
+ keydown: function(e){
3490
+ if (this.picker.is(':not(:visible)')){
3491
+ if (e.keyCode == 27) // allow escape to hide and re-show picker
3492
+ this.show();
3493
+ return;
3494
+ }
3495
+ var dateChanged = false,
3496
+ dir, day, month,
3497
+ newDate, newViewDate;
3498
+ switch(e.keyCode){
3499
+ case 27: // escape
3500
+ this.hide();
3501
+ e.preventDefault();
3502
+ break;
3503
+ case 37: // left
3504
+ case 39: // right
3505
+ if (!this.keyboardNavigation) break;
3506
+ dir = e.keyCode == 37 ? -1 : 1;
3507
+ if (e.ctrlKey){
3508
+ newDate = this.moveYear(this.date, dir);
3509
+ newViewDate = this.moveYear(this.viewDate, dir);
3510
+ } else if (e.shiftKey){
3511
+ newDate = this.moveMonth(this.date, dir);
3512
+ newViewDate = this.moveMonth(this.viewDate, dir);
3513
+ } else {
3514
+ newDate = new Date(this.date);
3515
+ newDate.setUTCDate(this.date.getUTCDate() + dir);
3516
+ newViewDate = new Date(this.viewDate);
3517
+ newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
3518
+ }
3519
+ if (this.dateWithinRange(newDate)){
3520
+ this.date = newDate;
3521
+ this.viewDate = newViewDate;
3522
+ this.setValue();
3523
+ this.update();
3524
+ e.preventDefault();
3525
+ dateChanged = true;
3526
+ }
3527
+ break;
3528
+ case 38: // up
3529
+ case 40: // down
3530
+ if (!this.keyboardNavigation) break;
3531
+ dir = e.keyCode == 38 ? -1 : 1;
3532
+ if (e.ctrlKey){
3533
+ newDate = this.moveYear(this.date, dir);
3534
+ newViewDate = this.moveYear(this.viewDate, dir);
3535
+ } else if (e.shiftKey){
3536
+ newDate = this.moveMonth(this.date, dir);
3537
+ newViewDate = this.moveMonth(this.viewDate, dir);
3538
+ } else {
3539
+ newDate = new Date(this.date);
3540
+ newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
3541
+ newViewDate = new Date(this.viewDate);
3542
+ newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
3543
+ }
3544
+ if (this.dateWithinRange(newDate)){
3545
+ this.date = newDate;
3546
+ this.viewDate = newViewDate;
3547
+ this.setValue();
3548
+ this.update();
3549
+ e.preventDefault();
3550
+ dateChanged = true;
3551
+ }
3552
+ break;
3553
+ case 13: // enter
3554
+ this.hide();
3555
+ e.preventDefault();
3556
+ break;
3557
+ case 9: // tab
3558
+ this.hide();
3559
+ break;
3560
+ }
3561
+ if (dateChanged){
3562
+ this.element.trigger({
3563
+ type: 'changeDate',
3564
+ date: this.date
3565
+ });
3566
+ var element;
3567
+ if (this.isInput) {
3568
+ element = this.element;
3569
+ } else if (this.component){
3570
+ element = this.element.find('input');
3571
+ }
3572
+ if (element) {
3573
+ element.change();
3574
+ }
3575
+ }
3576
+ },
3577
+
3578
+ showMode: function(dir) {
3579
+ if (dir) {
3580
+ this.viewMode = Math.max(0, Math.min(2, this.viewMode + dir));
3581
+ }
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();
3592
+ this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
3593
+ this.updateNavArrows();
3594
+ }
3595
+ };
3596
+
3597
+ $.fn.datepicker = function ( option ) {
3598
+ var args = Array.apply(null, arguments);
3599
+ args.shift();
3600
+ return this.each(function () {
3601
+ var $this = $(this),
3602
+ data = $this.data('datepicker'),
3603
+ options = typeof option == 'object' && option;
3604
+ if (!data) {
3605
+ $this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options))));
3606
+ }
3607
+ if (typeof option == 'string' && typeof data[option] == 'function') {
3608
+ data[option].apply(data, args);
3609
+ }
3610
+ });
3611
+ };
3612
+
3613
+ $.fn.datepicker.defaults = {
3614
+ };
3615
+ $.fn.datepicker.Constructor = Datepicker;
3616
+ var dates = $.fn.datepicker.dates = {
3617
+ en: {
3618
+ days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
3619
+ daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
3620
+ daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
3621
+ months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
3622
+ monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
3623
+ today: "Today"
3624
+ }
3625
+ }
3626
+
3627
+ var DPGlobal = {
3628
+ modes: [
3629
+ {
3630
+ clsName: 'days',
3631
+ navFnc: 'Month',
3632
+ navStep: 1
3633
+ },
3634
+ {
3635
+ clsName: 'months',
3636
+ navFnc: 'FullYear',
3637
+ navStep: 1
3638
+ },
3639
+ {
3640
+ clsName: 'years',
3641
+ navFnc: 'FullYear',
3642
+ navStep: 10
3643
+ }],
3644
+ isLeapYear: function (year) {
3645
+ return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
3646
+ },
3647
+ getDaysInMonth: function (year, month) {
3648
+ return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
3649
+ },
3650
+ validParts: /dd?|mm?|MM?|yy(?:yy)?/g,
3651
+ nonpunctuation: /[^ -\/:-@\[-`{-~\t\n\r]+/g,
3652
+ parseFormat: function(format){
3653
+ // IE treats \0 as a string end in inputs (truncating the value),
3654
+ // so it's a bad format delimiter, anyway
3655
+ var separators = format.replace(this.validParts, '\0').split('\0'),
3656
+ parts = format.match(this.validParts);
3657
+ if (!separators || !separators.length || !parts || parts.length == 0){
3658
+ throw new Error("Invalid date format.");
3659
+ }
3660
+ return {separators: separators, parts: parts};
3661
+ },
3662
+ parseDate: function(date, format, language) {
3663
+ 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),
3667
+ part, dir;
3668
+ date = new Date();
3669
+ for (var i=0; i<parts.length; i++) {
3670
+ part = part_re.exec(parts[i]);
3671
+ dir = parseInt(part[1]);
3672
+ switch(part[2]){
3673
+ case 'd':
3674
+ date.setUTCDate(date.getUTCDate() + dir);
3675
+ break;
3676
+ case 'm':
3677
+ date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
3678
+ break;
3679
+ case 'w':
3680
+ date.setUTCDate(date.getUTCDate() + dir * 7);
3681
+ break;
3682
+ case 'y':
3683
+ date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
3684
+ break;
3685
+ }
3686
+ }
3687
+ return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
3688
+ }
3689
+ var parts = date && date.match(this.nonpunctuation) || [],
3690
+ date = new Date(),
3691
+ parsed = {},
3692
+ setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
3693
+ setters_map = {
3694
+ yyyy: function(d,v){ return d.setUTCFullYear(v); },
3695
+ yy: function(d,v){ return d.setUTCFullYear(2000+v); },
3696
+ m: function(d,v){
3697
+ v -= 1;
3698
+ while (v<0) v += 12;
3699
+ v %= 12;
3700
+ d.setUTCMonth(v);
3701
+ while (d.getUTCMonth() != v)
3702
+ d.setUTCDate(d.getUTCDate()-1);
3703
+ return d;
3704
+ },
3705
+ d: function(d,v){ return d.setUTCDate(v); }
3706
+ },
3707
+ val, filtered, part;
3708
+ setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
3709
+ setters_map['dd'] = setters_map['d'];
3710
+ 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++) {
3713
+ val = parseInt(parts[i], 10);
3714
+ part = format.parts[i];
3715
+ if (isNaN(val)) {
3716
+ switch(part) {
3717
+ case 'MM':
3718
+ filtered = $(dates[language].months).filter(function(){
3719
+ var m = this.slice(0, parts[i].length),
3720
+ p = parts[i].slice(0, m.length);
3721
+ return m == p;
3722
+ });
3723
+ val = $.inArray(filtered[0], dates[language].months) + 1;
3724
+ break;
3725
+ case 'M':
3726
+ filtered = $(dates[language].monthsShort).filter(function(){
3727
+ var m = this.slice(0, parts[i].length),
3728
+ p = parts[i].slice(0, m.length);
3729
+ return m == p;
3730
+ });
3731
+ val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
3732
+ break;
3733
+ }
3734
+ }
3735
+ parsed[part] = val;
3736
+ }
3737
+ for (var i=0, s; i<setters_order.length; i++){
3738
+ s = setters_order[i];
3739
+ if (s in parsed)
3740
+ setters_map[s](date, parsed[s])
3741
+ }
3742
+ }
3743
+ return date;
3744
+ },
3745
+ formatDate: function(date, format, language){
3746
+ var val = {
3747
+ d: date.getUTCDate(),
3748
+ m: date.getUTCMonth() + 1,
3749
+ M: dates[language].monthsShort[date.getUTCMonth()],
3750
+ MM: dates[language].months[date.getUTCMonth()],
3751
+ yy: date.getUTCFullYear().toString().substring(2),
3752
+ yyyy: date.getUTCFullYear()
3753
+ };
3754
+ val.dd = (val.d < 10 ? '0' : '') + val.d;
3755
+ val.mm = (val.m < 10 ? '0' : '') + val.m;
3756
+ var date = [],
3757
+ seps = $.extend([], format.separators);
3758
+ for (var i=0, cnt = format.parts.length; i < cnt; i++) {
3759
+ if (seps.length)
3760
+ date.push(seps.shift())
3761
+ date.push(val[format.parts[i]]);
3762
+ }
3763
+ return date.join('');
3764
+ },
3765
+ headTemplate: '<thead>'+
3766
+ '<tr>'+
3767
+ '<th class="prev"><i class="icon-arrow-left"/></th>'+
3768
+ '<th colspan="5" class="switch"></th>'+
3769
+ '<th class="next"><i class="icon-arrow-right"/></th>'+
3770
+ '</tr>'+
3771
+ '</thead>',
3772
+ contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
3773
+ footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr></tfoot>'
3774
+ };
3775
+ DPGlobal.template = '<div class="datepicker">'+
3776
+ '<div class="datepicker-days">'+
3777
+ '<table class=" table-condensed">'+
3778
+ DPGlobal.headTemplate+
3779
+ '<tbody></tbody>'+
3780
+ DPGlobal.footTemplate+
3781
+ '</table>'+
3782
+ '</div>'+
3783
+ '<div class="datepicker-months">'+
3784
+ '<table class="table-condensed">'+
3785
+ DPGlobal.headTemplate+
3786
+ DPGlobal.contTemplate+
3787
+ DPGlobal.footTemplate+
3788
+ '</table>'+
3789
+ '</div>'+
3790
+ '<div class="datepicker-years">'+
3791
+ '<table class="table-condensed">'+
3792
+ DPGlobal.headTemplate+
3793
+ DPGlobal.contTemplate+
3794
+ DPGlobal.footTemplate+
3795
+ '</table>'+
3796
+ '</div>'+
3797
+ '</div>';
3798
+
3799
+ $.fn.datepicker.DPGlobal = DPGlobal;
3800
+
3801
+ }( window.jQuery );