Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
loeasy68
GitHub Repository: loeasy68/loeasy68.github.io
Path: blob/main/website/GAUSS/js/bootstrap-editable.js
2941 views
1
/*! X-editable - v1.5.1
2
* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
3
* http://github.com/vitalets/x-editable
4
* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
5
/**
6
Form with single input element, two buttons and two states: normal/loading.
7
Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown.
8
Editableform is linked with one of input types, e.g. 'text', 'select' etc.
9
10
@class editableform
11
@uses text
12
@uses textarea
13
**/
14
(function ($) {
15
"use strict";
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
//nothing shown after init
24
};
25
26
EditableForm.prototype = {
27
constructor: EditableForm,
28
initInput: function() { //called once
29
//take input from options (as it is created in editable-element)
30
this.input = this.options.input;
31
32
//set initial value
33
//todo: may be add check: typeof str === 'string' ?
34
this.value = this.input.str2value(this.options.value);
35
36
//prerender: get input.$input
37
this.input.prerender();
38
},
39
initTemplate: function() {
40
this.$form = $($.fn.editableform.template);
41
},
42
initButtons: function() {
43
var $btn = this.$form.find('.editable-buttons');
44
$btn.append($.fn.editableform.buttons);
45
if(this.options.showbuttons === 'bottom') {
46
$btn.addClass('editable-buttons-bottom');
47
}
48
},
49
/**
50
Renders editableform
51
52
@method render
53
**/
54
render: function() {
55
//init loader
56
this.$loading = $($.fn.editableform.loading);
57
this.$div.empty().append(this.$loading);
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
//show loading state
68
this.showLoading();
69
70
//flag showing is form now saving value to server.
71
//It is needed to wait when closing form.
72
this.isSaving = false;
73
74
/**
75
Fired when rendering starts
76
@event rendering
77
@param {Object} event event object
78
**/
79
this.$div.triggerHandler('rendering');
80
81
//init input
82
this.initInput();
83
84
//append input to form
85
this.$form.find('div.editable-input').append(this.input.$tpl);
86
87
//append form to container
88
this.$div.append(this.$form);
89
90
//render input
91
$.when(this.input.render())
92
.then($.proxy(function () {
93
//setup input to submit automatically when no buttons shown
94
if(!this.options.showbuttons) {
95
this.input.autosubmit();
96
}
97
98
//attach 'cancel' handler
99
this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
100
101
if(this.input.error) {
102
this.error(this.input.error);
103
this.$form.find('.editable-submit').attr('disabled', true);
104
this.input.$input.attr('disabled', true);
105
//prevent form from submitting
106
this.$form.submit(function(e){ e.preventDefault(); });
107
} else {
108
this.error(false);
109
this.input.$input.removeAttr('disabled');
110
this.$form.find('.editable-submit').removeAttr('disabled');
111
var value = (this.value === null || this.value === undefined || this.value === '') ? this.options.defaultValue : this.value;
112
this.input.value2input(value);
113
//attach submit handler
114
this.$form.submit($.proxy(this.submit, this));
115
}
116
117
/**
118
Fired when form is rendered
119
@event rendered
120
@param {Object} event event object
121
**/
122
this.$div.triggerHandler('rendered');
123
124
this.showForm();
125
126
//call postrender method to perform actions required visibility of form
127
if(this.input.postrender) {
128
this.input.postrender();
129
}
130
}, this));
131
},
132
cancel: function() {
133
/**
134
Fired when form was cancelled by user
135
@event cancel
136
@param {Object} event event object
137
**/
138
this.$div.triggerHandler('cancel');
139
},
140
showLoading: function() {
141
var w, h;
142
if(this.$form) {
143
//set loading size equal to form
144
w = this.$form.outerWidth();
145
h = this.$form.outerHeight();
146
if(w) {
147
this.$loading.width(w);
148
}
149
if(h) {
150
this.$loading.height(h);
151
}
152
this.$form.hide();
153
} else {
154
//stretch loading to fill container width
155
w = this.$loading.parent().width();
156
if(w) {
157
this.$loading.width(w);
158
}
159
}
160
this.$loading.show();
161
},
162
163
showForm: function(activate) {
164
this.$loading.hide();
165
this.$form.show();
166
if(activate !== false) {
167
this.input.activate();
168
}
169
/**
170
Fired when form is shown
171
@event show
172
@param {Object} event event object
173
**/
174
this.$div.triggerHandler('show');
175
},
176
177
error: function(msg) {
178
var $group = this.$form.find('.control-group'),
179
$block = this.$form.find('.editable-error-block'),
180
lines;
181
182
if(msg === false) {
183
$group.removeClass($.fn.editableform.errorGroupClass);
184
$block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
185
} else {
186
//convert newline to <br> for more pretty error display
187
if(msg) {
188
lines = (''+msg).split('\n');
189
for (var i = 0; i < lines.length; i++) {
190
lines[i] = $('<div>').text(lines[i]).html();
191
}
192
msg = lines.join('<br>');
193
}
194
$group.addClass($.fn.editableform.errorGroupClass);
195
$block.addClass($.fn.editableform.errorBlockClass).html(msg).show();
196
}
197
},
198
199
submit: function(e) {
200
e.stopPropagation();
201
e.preventDefault();
202
203
//get new value from input
204
var newValue = this.input.input2value();
205
206
//validation: if validate returns string or truthy value - means error
207
//if returns object like {newValue: '...'} => submitted value is reassigned to it
208
var error = this.validate(newValue);
209
if ($.type(error) === 'object' && error.newValue !== undefined) {
210
newValue = error.newValue;
211
this.input.value2input(newValue);
212
if(typeof error.msg === 'string') {
213
this.error(error.msg);
214
this.showForm();
215
return;
216
}
217
} else if (error) {
218
this.error(error);
219
this.showForm();
220
return;
221
}
222
223
//if value not changed --> trigger 'nochange' event and return
224
/*jslint eqeq: true*/
225
if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) {
226
/*jslint eqeq: false*/
227
/**
228
Fired when value not changed but form is submitted. Requires savenochange = false.
229
@event nochange
230
@param {Object} event event object
231
**/
232
this.$div.triggerHandler('nochange');
233
return;
234
}
235
236
//convert value for submitting to server
237
var submitValue = this.input.value2submit(newValue);
238
239
this.isSaving = true;
240
241
//sending data to server
242
$.when(this.save(submitValue))
243
.done($.proxy(function(response) {
244
this.isSaving = false;
245
246
//run success callback
247
var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null;
248
249
//if success callback returns false --> keep form open and do not activate input
250
if(res === false) {
251
this.error(false);
252
this.showForm(false);
253
return;
254
}
255
256
//if success callback returns string --> keep form open, show error and activate input
257
if(typeof res === 'string') {
258
this.error(res);
259
this.showForm();
260
return;
261
}
262
263
//if success callback returns object like {newValue: <something>} --> use that value instead of submitted
264
//it is usefull if you want to chnage value in url-function
265
if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
266
newValue = res.newValue;
267
}
268
269
//clear error message
270
this.error(false);
271
this.value = newValue;
272
/**
273
Fired when form is submitted
274
@event save
275
@param {Object} event event object
276
@param {Object} params additional params
277
@param {mixed} params.newValue raw new value
278
@param {mixed} params.submitValue submitted value as string
279
@param {Object} params.response ajax response
280
281
@example
282
$('#form-div').on('save'), function(e, params){
283
if(params.newValue === 'username') {...}
284
});
285
**/
286
this.$div.triggerHandler('save', {newValue: newValue, submitValue: submitValue, response: response});
287
}, this))
288
.fail($.proxy(function(xhr) {
289
this.isSaving = false;
290
291
var msg;
292
if(typeof this.options.error === 'function') {
293
msg = this.options.error.call(this.options.scope, xhr, newValue);
294
} else {
295
msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!';
296
}
297
298
this.error(msg);
299
this.showForm();
300
}, this));
301
},
302
303
save: function(submitValue) {
304
//try parse composite pk defined as json string in data-pk
305
this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true);
306
307
var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk,
308
/*
309
send on server in following cases:
310
1. url is function
311
2. url is string AND (pk defined OR send option = always)
312
*/
313
send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))),
314
params;
315
316
if (send) { //send to server
317
this.showLoading();
318
319
//standard params
320
params = {
321
name: this.options.name || '',
322
value: submitValue,
323
pk: pk
324
};
325
326
//additional params
327
if(typeof this.options.params === 'function') {
328
params = this.options.params.call(this.options.scope, params);
329
} else {
330
//try parse json in single quotes (from data-params attribute)
331
this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true);
332
$.extend(params, this.options.params);
333
}
334
335
if(typeof this.options.url === 'function') { //user's function
336
return this.options.url.call(this.options.scope, params);
337
} else {
338
//send ajax to server and return deferred object
339
return $.ajax($.extend({
340
url : this.options.url,
341
data : params,
342
type : 'POST'
343
}, this.options.ajaxOptions));
344
}
345
}
346
},
347
348
validate: function (value) {
349
if (value === undefined) {
350
value = this.value;
351
}
352
if (typeof this.options.validate === 'function') {
353
return this.options.validate.call(this.options.scope, value);
354
}
355
},
356
357
option: function(key, value) {
358
if(key in this.options) {
359
this.options[key] = value;
360
}
361
362
if(key === 'value') {
363
this.setValue(value);
364
}
365
366
//do not pass option to input as it is passed in editable-element
367
},
368
369
setValue: function(value, convertStr) {
370
if(convertStr) {
371
this.value = this.input.str2value(value);
372
} else {
373
this.value = value;
374
}
375
376
//if form is visible, update input
377
if(this.$form && this.$form.is(':visible')) {
378
this.input.value2input(this.value);
379
}
380
}
381
};
382
383
/*
384
Initialize editableform. Applied to jQuery object.
385
386
@method $().editableform(options)
387
@params {Object} options
388
@example
389
var $form = $('&lt;div&gt;').editableform({
390
type: 'text',
391
name: 'username',
392
url: '/post',
393
value: 'vitaliy'
394
});
395
396
//to display form you should call 'render' method
397
$form.editableform('render');
398
*/
399
$.fn.editableform = function (option) {
400
var args = arguments;
401
return this.each(function () {
402
var $this = $(this),
403
data = $this.data('editableform'),
404
options = typeof option === 'object' && option;
405
if (!data) {
406
$this.data('editableform', (data = new EditableForm(this, options)));
407
}
408
409
if (typeof option === 'string') { //call method
410
data[option].apply(data, Array.prototype.slice.call(args, 1));
411
}
412
});
413
};
414
415
//keep link to constructor to allow inheritance
416
$.fn.editableform.Constructor = EditableForm;
417
418
//defaults
419
$.fn.editableform.defaults = {
420
/* see also defaults for input */
421
422
/**
423
Type of input. Can be <code>text|textarea|select|date|checklist</code>
424
425
@property type
426
@type string
427
@default 'text'
428
**/
429
type: 'text',
430
/**
431
Url for submit, e.g. <code>'/post'</code>
432
If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
433
434
@property url
435
@type string|function
436
@default null
437
@example
438
url: function(params) {
439
var d = new $.Deferred;
440
if(params.value === 'abc') {
441
return d.reject('error message'); //returning error via deferred object
442
} else {
443
//async saving data in js model
444
someModel.asyncSaveMethod({
445
...,
446
success: function(){
447
d.resolve();
448
}
449
});
450
return d.promise();
451
}
452
}
453
**/
454
url:null,
455
/**
456
Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value).
457
If defined as <code>function</code> - returned object **overwrites** original ajax data.
458
@example
459
params: function(params) {
460
//originally params contain pk, name and value
461
params.a = 1;
462
return params;
463
}
464
465
@property params
466
@type object|function
467
@default null
468
**/
469
params:null,
470
/**
471
Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute
472
473
@property name
474
@type string
475
@default null
476
**/
477
name: null,
478
/**
479
Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>.
480
Can be calculated dynamically via function.
481
482
@property pk
483
@type string|object|function
484
@default null
485
**/
486
pk: null,
487
/**
488
Initial value. If not defined - will be taken from element's content.
489
For __select__ type should be defined (as it is ID of shown text).
490
491
@property value
492
@type string|object
493
@default null
494
**/
495
value: null,
496
/**
497
Value that will be displayed in input if original field value is empty (`null|undefined|''`).
498
499
@property defaultValue
500
@type string|object
501
@default null
502
@since 1.4.6
503
**/
504
defaultValue: null,
505
/**
506
Strategy for sending data on server. Can be `auto|always|never`.
507
When 'auto' data will be sent on server **only if pk and url defined**, otherwise new value will be stored locally.
508
509
@property send
510
@type string
511
@default 'auto'
512
**/
513
send: 'auto',
514
/**
515
Function for client-side validation. If returns string - means validation not passed and string showed as error.
516
Since 1.5.1 you can modify submitted value by returning object from `validate`:
517
`{newValue: '...'}` or `{newValue: '...', msg: '...'}`
518
519
@property validate
520
@type function
521
@default null
522
@example
523
validate: function(value) {
524
if($.trim(value) == '') {
525
return 'This field is required';
526
}
527
}
528
**/
529
validate: null,
530
/**
531
Success callback. Called when value successfully sent on server and **response status = 200**.
532
Usefull to work with json response. For example, if your backend response can be <code>{success: true}</code>
533
or <code>{success: false, msg: "server error"}</code> you can check it inside this callback.
534
If it returns **string** - means error occured and string is shown as error message.
535
If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user.
536
Otherwise newValue simply rendered into element.
537
538
@property success
539
@type function
540
@default null
541
@example
542
success: function(response, newValue) {
543
if(!response.success) return response.msg;
544
}
545
**/
546
success: null,
547
/**
548
Error callback. Called when request failed (response status != 200).
549
Usefull when you want to parse error response and display a custom message.
550
Must return **string** - the message to be displayed in the error block.
551
552
@property error
553
@type function
554
@default null
555
@since 1.4.4
556
@example
557
error: function(response, newValue) {
558
if(response.status === 500) {
559
return 'Service unavailable. Please try later.';
560
} else {
561
return response.responseText;
562
}
563
}
564
**/
565
error: null,
566
/**
567
Additional options for submit ajax request.
568
List of values: http://api.jquery.com/jQuery.ajax
569
570
@property ajaxOptions
571
@type object
572
@default null
573
@since 1.1.1
574
@example
575
ajaxOptions: {
576
type: 'put',
577
dataType: 'json'
578
}
579
**/
580
ajaxOptions: null,
581
/**
582
Where to show buttons: left(true)|bottom|false
583
Form without buttons is auto-submitted.
584
585
@property showbuttons
586
@type boolean|string
587
@default true
588
@since 1.1.1
589
**/
590
showbuttons: true,
591
/**
592
Scope for callback methods (success, validate).
593
If <code>null</code> means editableform instance itself.
594
595
@property scope
596
@type DOMElement|object
597
@default null
598
@since 1.2.0
599
@private
600
**/
601
scope: null,
602
/**
603
Whether to save or cancel value when it was not changed but form was submitted
604
605
@property savenochange
606
@type boolean
607
@default false
608
@since 1.2.0
609
**/
610
savenochange: false
611
};
612
613
/*
614
Note: following params could redefined in engine: bootstrap or jqueryui:
615
Classes 'control-group' and 'editable-error-block' must always present!
616
*/
617
$.fn.editableform.template = '<form class="form-inline editableform">'+
618
'<div class="control-group">' +
619
'<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
620
'<div class="editable-error-block"></div>' +
621
'</div>' +
622
'</form>';
623
624
//loading div
625
$.fn.editableform.loading = '<div class="editableform-loading"></div>';
626
627
//buttons
628
$.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
629
'<button type="button" class="editable-cancel">cancel</button>';
630
631
//error class attached to control-group
632
$.fn.editableform.errorGroupClass = null;
633
634
//error class attached to editable-error-block
635
$.fn.editableform.errorBlockClass = 'editable-error';
636
637
//engine
638
$.fn.editableform.engine = 'jquery';
639
}(window.jQuery));
640
641
/**
642
* EditableForm utilites
643
*/
644
(function ($) {
645
"use strict";
646
647
//utils
648
$.fn.editableutils = {
649
/**
650
* classic JS inheritance function
651
*/
652
inherit: function (Child, Parent) {
653
var F = function() { };
654
F.prototype = Parent.prototype;
655
Child.prototype = new F();
656
Child.prototype.constructor = Child;
657
Child.superclass = Parent.prototype;
658
},
659
660
/**
661
* set caret position in input
662
* see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
663
*/
664
setCursorPosition: function(elem, pos) {
665
if (elem.setSelectionRange) {
666
elem.setSelectionRange(pos, pos);
667
} else if (elem.createTextRange) {
668
var range = elem.createTextRange();
669
range.collapse(true);
670
range.moveEnd('character', pos);
671
range.moveStart('character', pos);
672
range.select();
673
}
674
},
675
676
/**
677
* function to parse JSON in *single* quotes. (jquery automatically parse only double quotes)
678
* That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}">
679
* safe = true --> means no exception will be thrown
680
* for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
681
*/
682
tryParseJson: function(s, safe) {
683
if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
684
if (safe) {
685
try {
686
/*jslint evil: true*/
687
s = (new Function('return ' + s))();
688
/*jslint evil: false*/
689
} catch (e) {} finally {
690
return s;
691
}
692
} else {
693
/*jslint evil: true*/
694
s = (new Function('return ' + s))();
695
/*jslint evil: false*/
696
}
697
}
698
return s;
699
},
700
701
/**
702
* slice object by specified keys
703
*/
704
sliceObj: function(obj, keys, caseSensitive /* default: false */) {
705
var key, keyLower, newObj = {};
706
707
if (!$.isArray(keys) || !keys.length) {
708
return newObj;
709
}
710
711
for (var i = 0; i < keys.length; i++) {
712
key = keys[i];
713
if (obj.hasOwnProperty(key)) {
714
newObj[key] = obj[key];
715
}
716
717
if(caseSensitive === true) {
718
continue;
719
}
720
721
//when getting data-* attributes via $.data() it's converted to lowercase.
722
//details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
723
//workaround is code below.
724
keyLower = key.toLowerCase();
725
if (obj.hasOwnProperty(keyLower)) {
726
newObj[key] = obj[keyLower];
727
}
728
}
729
730
return newObj;
731
},
732
733
/*
734
exclude complex objects from $.data() before pass to config
735
*/
736
getConfigData: function($element) {
737
var data = {};
738
$.each($element.data(), function(k, v) {
739
if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) {
740
data[k] = v;
741
}
742
});
743
return data;
744
},
745
746
/*
747
returns keys of object
748
*/
749
objectKeys: function(o) {
750
if (Object.keys) {
751
return Object.keys(o);
752
} else {
753
if (o !== Object(o)) {
754
throw new TypeError('Object.keys called on a non-object');
755
}
756
var k=[], p;
757
for (p in o) {
758
if (Object.prototype.hasOwnProperty.call(o,p)) {
759
k.push(p);
760
}
761
}
762
return k;
763
}
764
765
},
766
767
/**
768
method to escape html.
769
**/
770
escape: function(str) {
771
return $('<div>').text(str).html();
772
},
773
774
/*
775
returns array items from sourceData having value property equal or inArray of 'value'
776
*/
777
itemsByValue: function(value, sourceData, valueProp) {
778
if(!sourceData || value === null) {
779
return [];
780
}
781
782
if (typeof(valueProp) !== "function") {
783
var idKey = valueProp || 'value';
784
valueProp = function (e) { return e[idKey]; };
785
}
786
787
var isValArray = $.isArray(value),
788
result = [],
789
that = this;
790
791
$.each(sourceData, function(i, o) {
792
if(o.children) {
793
result = result.concat(that.itemsByValue(value, o.children, valueProp));
794
} else {
795
/*jslint eqeq: true*/
796
if(isValArray) {
797
if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? valueProp(o) : o); }).length) {
798
result.push(o);
799
}
800
} else {
801
var itemValue = (o && (typeof o === 'object')) ? valueProp(o) : o;
802
if(value == itemValue) {
803
result.push(o);
804
}
805
}
806
/*jslint eqeq: false*/
807
}
808
});
809
810
return result;
811
},
812
813
/*
814
Returns input by options: type, mode.
815
*/
816
createInput: function(options) {
817
var TypeConstructor, typeOptions, input,
818
type = options.type;
819
820
//`date` is some kind of virtual type that is transformed to one of exact types
821
//depending on mode and core lib
822
if(type === 'date') {
823
//inline
824
if(options.mode === 'inline') {
825
if($.fn.editabletypes.datefield) {
826
type = 'datefield';
827
} else if($.fn.editabletypes.dateuifield) {
828
type = 'dateuifield';
829
}
830
//popup
831
} else {
832
if($.fn.editabletypes.date) {
833
type = 'date';
834
} else if($.fn.editabletypes.dateui) {
835
type = 'dateui';
836
}
837
}
838
839
//if type still `date` and not exist in types, replace with `combodate` that is base input
840
if(type === 'date' && !$.fn.editabletypes.date) {
841
type = 'combodate';
842
}
843
}
844
845
//`datetime` should be datetimefield in 'inline' mode
846
if(type === 'datetime' && options.mode === 'inline') {
847
type = 'datetimefield';
848
}
849
850
//change wysihtml5 to textarea for jquery UI and plain versions
851
if(type === 'wysihtml5' && !$.fn.editabletypes[type]) {
852
type = 'textarea';
853
}
854
855
//create input of specified type. Input will be used for converting value, not in form
856
if(typeof $.fn.editabletypes[type] === 'function') {
857
TypeConstructor = $.fn.editabletypes[type];
858
typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults));
859
input = new TypeConstructor(typeOptions);
860
return input;
861
} else {
862
$.error('Unknown type: '+ type);
863
return false;
864
}
865
},
866
867
//see http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr
868
supportsTransitions: function () {
869
var b = document.body || document.documentElement,
870
s = b.style,
871
p = 'transition',
872
v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
873
874
if(typeof s[p] === 'string') {
875
return true;
876
}
877
878
// Tests for vendor specific prop
879
p = p.charAt(0).toUpperCase() + p.substr(1);
880
for(var i=0; i<v.length; i++) {
881
if(typeof s[v[i] + p] === 'string') {
882
return true;
883
}
884
}
885
return false;
886
}
887
888
};
889
}(window.jQuery));
890
891
/**
892
Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
893
This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
894
Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br>
895
Applied as jQuery method.
896
897
@class editableContainer
898
@uses editableform
899
**/
900
(function ($) {
901
"use strict";
902
903
var Popup = function (element, options) {
904
this.init(element, options);
905
};
906
907
var Inline = function (element, options) {
908
this.init(element, options);
909
};
910
911
//methods
912
Popup.prototype = {
913
containerName: null, //method to call container on element
914
containerDataName: null, //object name in element's .data()
915
innerCss: null, //tbd in child class
916
containerClass: 'editable-container editable-popup', //css class applied to container element
917
defaults: {}, //container itself defaults
918
919
init: function(element, options) {
920
this.$element = $(element);
921
//since 1.4.1 container do not use data-* directly as they already merged into options.
922
this.options = $.extend({}, $.fn.editableContainer.defaults, options);
923
this.splitOptions();
924
925
//set scope of form callbacks to element
926
this.formOptions.scope = this.$element[0];
927
928
this.initContainer();
929
930
//flag to hide container, when saving value will finish
931
this.delayedHide = false;
932
933
//bind 'destroyed' listener to destroy container when element is removed from dom
934
this.$element.on('destroyed', $.proxy(function(){
935
this.destroy();
936
}, this));
937
938
//attach document handler to close containers on click / escape
939
if(!$(document).data('editable-handlers-attached')) {
940
//close all on escape
941
$(document).on('keyup.editable', function (e) {
942
if (e.which === 27) {
943
$('.editable-open').editableContainer('hide');
944
//todo: return focus on element
945
}
946
});
947
948
//close containers when click outside
949
//(mousedown could be better than click, it closes everything also on drag drop)
950
$(document).on('click.editable', function(e) {
951
var $target = $(e.target), i,
952
exclude_classes = ['.editable-container',
953
'.ui-datepicker-header',
954
'.datepicker', //in inline mode datepicker is rendered into body
955
'.modal-backdrop',
956
'.bootstrap-wysihtml5-insert-image-modal',
957
'.bootstrap-wysihtml5-insert-link-modal'
958
];
959
960
//check if element is detached. It occurs when clicking in bootstrap datepicker
961
if (!$.contains(document.documentElement, e.target)) {
962
return;
963
}
964
965
//for some reason FF 20 generates extra event (click) in select2 widget with e.target = document
966
//we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199
967
//Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec
968
if($target.is(document)) {
969
return;
970
}
971
972
//if click inside one of exclude classes --> no nothing
973
for(i=0; i<exclude_classes.length; i++) {
974
if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) {
975
return;
976
}
977
}
978
979
//close all open containers (except one - target)
980
Popup.prototype.closeOthers(e.target);
981
});
982
983
$(document).data('editable-handlers-attached', true);
984
}
985
},
986
987
//split options on containerOptions and formOptions
988
splitOptions: function() {
989
this.containerOptions = {};
990
this.formOptions = {};
991
992
if(!$.fn[this.containerName]) {
993
throw new Error(this.containerName + ' not found. Have you included corresponding js file?');
994
}
995
996
//keys defined in container defaults go to container, others go to form
997
for(var k in this.options) {
998
if(k in this.defaults) {
999
this.containerOptions[k] = this.options[k];
1000
} else {
1001
this.formOptions[k] = this.options[k];
1002
}
1003
}
1004
},
1005
1006
/*
1007
Returns jquery object of container
1008
@method tip()
1009
*/
1010
tip: function() {
1011
return this.container() ? this.container().$tip : null;
1012
},
1013
1014
/* returns container object */
1015
container: function() {
1016
var container;
1017
//first, try get it by `containerDataName`
1018
if(this.containerDataName) {
1019
if(container = this.$element.data(this.containerDataName)) {
1020
return container;
1021
}
1022
}
1023
//second, try `containerName`
1024
container = this.$element.data(this.containerName);
1025
return container;
1026
},
1027
1028
/* call native method of underlying container, e.g. this.$element.popover('method') */
1029
call: function() {
1030
this.$element[this.containerName].apply(this.$element, arguments);
1031
},
1032
1033
initContainer: function(){
1034
this.call(this.containerOptions);
1035
},
1036
1037
renderForm: function() {
1038
this.$form
1039
.editableform(this.formOptions)
1040
.on({
1041
save: $.proxy(this.save, this), //click on submit button (value changed)
1042
nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
1043
cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
1044
show: $.proxy(function() {
1045
if(this.delayedHide) {
1046
this.hide(this.delayedHide.reason);
1047
this.delayedHide = false;
1048
} else {
1049
this.setPosition();
1050
}
1051
}, this), //re-position container every time form is shown (occurs each time after loading state)
1052
rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
1053
resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
1054
rendered: $.proxy(function(){
1055
/**
1056
Fired when container is shown and form is rendered (for select will wait for loading dropdown options).
1057
**Note:** Bootstrap popover has own `shown` event that now cannot be separated from x-editable's one.
1058
The workaround is to check `arguments.length` that is always `2` for x-editable.
1059
1060
@event shown
1061
@param {Object} event event object
1062
@example
1063
$('#username').on('shown', function(e, editable) {
1064
editable.input.$input.val('overwriting value of input..');
1065
});
1066
**/
1067
/*
1068
TODO: added second param mainly to distinguish from bootstrap's shown event. It's a hotfix that will be solved in future versions via namespaced events.
1069
*/
1070
this.$element.triggerHandler('shown', $(this.options.scope).data('editable'));
1071
}, this)
1072
})
1073
.editableform('render');
1074
},
1075
1076
/**
1077
Shows container with form
1078
@method show()
1079
@param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1080
**/
1081
/* Note: poshytip owerwrites this method totally! */
1082
show: function (closeAll) {
1083
this.$element.addClass('editable-open');
1084
if(closeAll !== false) {
1085
//close all open containers (except this)
1086
this.closeOthers(this.$element[0]);
1087
}
1088
1089
//show container itself
1090
this.innerShow();
1091
this.tip().addClass(this.containerClass);
1092
1093
/*
1094
Currently, form is re-rendered on every show.
1095
The main reason is that we dont know, what will container do with content when closed:
1096
remove(), detach() or just hide() - it depends on container.
1097
1098
Detaching form itself before hide and re-insert before show is good solution,
1099
but visually it looks ugly --> container changes size before hide.
1100
*/
1101
1102
//if form already exist - delete previous data
1103
if(this.$form) {
1104
//todo: destroy prev data!
1105
//this.$form.destroy();
1106
}
1107
1108
this.$form = $('<div>');
1109
1110
//insert form into container body
1111
if(this.tip().is(this.innerCss)) {
1112
//for inline container
1113
this.tip().append(this.$form);
1114
} else {
1115
this.tip().find(this.innerCss).append(this.$form);
1116
}
1117
1118
//render form
1119
this.renderForm();
1120
},
1121
1122
/**
1123
Hides container with form
1124
@method hide()
1125
@param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
1126
**/
1127
hide: function(reason) {
1128
if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
1129
return;
1130
}
1131
1132
//if form is saving value, schedule hide
1133
if(this.$form.data('editableform').isSaving) {
1134
this.delayedHide = {reason: reason};
1135
return;
1136
} else {
1137
this.delayedHide = false;
1138
}
1139
1140
this.$element.removeClass('editable-open');
1141
this.innerHide();
1142
1143
/**
1144
Fired when container was hidden. It occurs on both save or cancel.
1145
**Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one.
1146
The workaround is to check `arguments.length` that is always `2` for x-editable.
1147
1148
@event hidden
1149
@param {object} event event object
1150
@param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|manual</code>
1151
@example
1152
$('#username').on('hidden', function(e, reason) {
1153
if(reason === 'save' || reason === 'cancel') {
1154
//auto-open next editable
1155
$(this).closest('tr').next().find('.editable').editable('show');
1156
}
1157
});
1158
**/
1159
this.$element.triggerHandler('hidden', reason || 'manual');
1160
},
1161
1162
/* internal show method. To be overwritten in child classes */
1163
innerShow: function () {
1164
1165
},
1166
1167
/* internal hide method. To be overwritten in child classes */
1168
innerHide: function () {
1169
1170
},
1171
1172
/**
1173
Toggles container visibility (show / hide)
1174
@method toggle()
1175
@param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1176
**/
1177
toggle: function(closeAll) {
1178
if(this.container() && this.tip() && this.tip().is(':visible')) {
1179
this.hide();
1180
} else {
1181
this.show(closeAll);
1182
}
1183
},
1184
1185
/*
1186
Updates the position of container when content changed.
1187
@method setPosition()
1188
*/
1189
setPosition: function() {
1190
//tbd in child class
1191
},
1192
1193
save: function(e, params) {
1194
/**
1195
Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
1196
1197
@event save
1198
@param {Object} event event object
1199
@param {Object} params additional params
1200
@param {mixed} params.newValue submitted value
1201
@param {Object} params.response ajax response
1202
@example
1203
$('#username').on('save', function(e, params) {
1204
//assuming server response: '{success: true}'
1205
var pk = $(this).data('editableContainer').options.pk;
1206
if(params.response && params.response.success) {
1207
alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
1208
} else {
1209
alert('error!');
1210
}
1211
});
1212
**/
1213
this.$element.triggerHandler('save', params);
1214
1215
//hide must be after trigger, as saving value may require methods of plugin, applied to input
1216
this.hide('save');
1217
},
1218
1219
/**
1220
Sets new option
1221
1222
@method option(key, value)
1223
@param {string} key
1224
@param {mixed} value
1225
**/
1226
option: function(key, value) {
1227
this.options[key] = value;
1228
if(key in this.containerOptions) {
1229
this.containerOptions[key] = value;
1230
this.setContainerOption(key, value);
1231
} else {
1232
this.formOptions[key] = value;
1233
if(this.$form) {
1234
this.$form.editableform('option', key, value);
1235
}
1236
}
1237
},
1238
1239
setContainerOption: function(key, value) {
1240
this.call('option', key, value);
1241
},
1242
1243
/**
1244
Destroys the container instance
1245
@method destroy()
1246
**/
1247
destroy: function() {
1248
this.hide();
1249
this.innerDestroy();
1250
this.$element.off('destroyed');
1251
this.$element.removeData('editableContainer');
1252
},
1253
1254
/* to be overwritten in child classes */
1255
innerDestroy: function() {
1256
1257
},
1258
1259
/*
1260
Closes other containers except one related to passed element.
1261
Other containers can be cancelled or submitted (depends on onblur option)
1262
*/
1263
closeOthers: function(element) {
1264
$('.editable-open').each(function(i, el){
1265
//do nothing with passed element and it's children
1266
if(el === element || $(el).find(element).length) {
1267
return;
1268
}
1269
1270
//otherwise cancel or submit all open containers
1271
var $el = $(el),
1272
ec = $el.data('editableContainer');
1273
1274
if(!ec) {
1275
return;
1276
}
1277
1278
if(ec.options.onblur === 'cancel') {
1279
$el.data('editableContainer').hide('onblur');
1280
} else if(ec.options.onblur === 'submit') {
1281
$el.data('editableContainer').tip().find('form').submit();
1282
}
1283
});
1284
1285
},
1286
1287
/**
1288
Activates input of visible container (e.g. set focus)
1289
@method activate()
1290
**/
1291
activate: function() {
1292
if(this.tip && this.tip().is(':visible') && this.$form) {
1293
this.$form.data('editableform').input.activate();
1294
}
1295
}
1296
1297
};
1298
1299
/**
1300
jQuery method to initialize editableContainer.
1301
1302
@method $().editableContainer(options)
1303
@params {Object} options
1304
@example
1305
$('#edit').editableContainer({
1306
type: 'text',
1307
url: '/post',
1308
pk: 1,
1309
value: 'hello'
1310
});
1311
**/
1312
$.fn.editableContainer = function (option) {
1313
var args = arguments;
1314
return this.each(function () {
1315
var $this = $(this),
1316
dataKey = 'editableContainer',
1317
data = $this.data(dataKey),
1318
options = typeof option === 'object' && option,
1319
Constructor = (options.mode === 'inline') ? Inline : Popup;
1320
1321
if (!data) {
1322
$this.data(dataKey, (data = new Constructor(this, options)));
1323
}
1324
1325
if (typeof option === 'string') { //call method
1326
data[option].apply(data, Array.prototype.slice.call(args, 1));
1327
}
1328
});
1329
};
1330
1331
//store constructors
1332
$.fn.editableContainer.Popup = Popup;
1333
$.fn.editableContainer.Inline = Inline;
1334
1335
//defaults
1336
$.fn.editableContainer.defaults = {
1337
/**
1338
Initial value of form input
1339
1340
@property value
1341
@type mixed
1342
@default null
1343
@private
1344
**/
1345
value: null,
1346
/**
1347
Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inline container.
1348
1349
@property placement
1350
@type string
1351
@default 'top'
1352
**/
1353
placement: 'top',
1354
/**
1355
Whether to hide container on save/cancel.
1356
1357
@property autohide
1358
@type boolean
1359
@default true
1360
@private
1361
**/
1362
autohide: true,
1363
/**
1364
Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>.
1365
Setting <code>ignore</code> allows to have several containers open.
1366
1367
@property onblur
1368
@type string
1369
@default 'cancel'
1370
@since 1.1.1
1371
**/
1372
onblur: 'cancel',
1373
1374
/**
1375
Animation speed (inline mode only)
1376
@property anim
1377
@type string
1378
@default false
1379
**/
1380
anim: false,
1381
1382
/**
1383
Mode of editable, can be `popup` or `inline`
1384
1385
@property mode
1386
@type string
1387
@default 'popup'
1388
@since 1.4.0
1389
**/
1390
mode: 'popup'
1391
};
1392
1393
/*
1394
* workaround to have 'destroyed' event to destroy popover when element is destroyed
1395
* see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
1396
*/
1397
jQuery.event.special.destroyed = {
1398
remove: function(o) {
1399
if (o.handler) {
1400
o.handler();
1401
}
1402
}
1403
};
1404
1405
}(window.jQuery));
1406
1407
/**
1408
* Editable Inline
1409
* ---------------------
1410
*/
1411
(function ($) {
1412
"use strict";
1413
1414
//copy prototype from EditableContainer
1415
//extend methods
1416
$.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, {
1417
containerName: 'editableform',
1418
innerCss: '.editable-inline',
1419
containerClass: 'editable-container editable-inline', //css class applied to container element
1420
1421
initContainer: function(){
1422
//container is <span> element
1423
this.$tip = $('<span></span>');
1424
1425
//convert anim to miliseconds (int)
1426
if(!this.options.anim) {
1427
this.options.anim = 0;
1428
}
1429
},
1430
1431
splitOptions: function() {
1432
//all options are passed to form
1433
this.containerOptions = {};
1434
this.formOptions = this.options;
1435
},
1436
1437
tip: function() {
1438
return this.$tip;
1439
},
1440
1441
innerShow: function () {
1442
this.$element.hide();
1443
this.tip().insertAfter(this.$element).show();
1444
},
1445
1446
innerHide: function () {
1447
this.$tip.hide(this.options.anim, $.proxy(function() {
1448
this.$element.show();
1449
this.innerDestroy();
1450
}, this));
1451
},
1452
1453
innerDestroy: function() {
1454
if(this.tip()) {
1455
this.tip().empty().remove();
1456
}
1457
}
1458
});
1459
1460
}(window.jQuery));
1461
/**
1462
Makes editable any HTML element on the page. Applied as jQuery method.
1463
1464
@class editable
1465
@uses editableContainer
1466
**/
1467
(function ($) {
1468
"use strict";
1469
1470
var Editable = function (element, options) {
1471
this.$element = $(element);
1472
//data-* has more priority over js options: because dynamically created elements may change data-*
1473
this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element));
1474
if(this.options.selector) {
1475
this.initLive();
1476
} else {
1477
this.init();
1478
}
1479
1480
//check for transition support
1481
if(this.options.highlight && !$.fn.editableutils.supportsTransitions()) {
1482
this.options.highlight = false;
1483
}
1484
};
1485
1486
Editable.prototype = {
1487
constructor: Editable,
1488
init: function () {
1489
var isValueByText = false,
1490
doAutotext, finalize;
1491
1492
//name
1493
this.options.name = this.options.name || this.$element.attr('id');
1494
1495
//create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select)
1496
//also we set scope option to have access to element inside input specific callbacks (e. g. source as function)
1497
this.options.scope = this.$element[0];
1498
this.input = $.fn.editableutils.createInput(this.options);
1499
if(!this.input) {
1500
return;
1501
}
1502
1503
//set value from settings or by element's text
1504
if (this.options.value === undefined || this.options.value === null) {
1505
this.value = this.input.html2value($.trim(this.$element.html()));
1506
isValueByText = true;
1507
} else {
1508
/*
1509
value can be string when received from 'data-value' attribute
1510
for complext objects value can be set as json string in data-value attribute,
1511
e.g. data-value="{city: 'Moscow', street: 'Lenina'}"
1512
*/
1513
this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true);
1514
if(typeof this.options.value === 'string') {
1515
this.value = this.input.str2value(this.options.value);
1516
} else {
1517
this.value = this.options.value;
1518
}
1519
}
1520
1521
//add 'editable' class to every editable element
1522
this.$element.addClass('editable');
1523
1524
//specifically for "textarea" add class .editable-pre-wrapped to keep linebreaks
1525
if(this.input.type === 'textarea') {
1526
this.$element.addClass('editable-pre-wrapped');
1527
}
1528
1529
//attach handler activating editable. In disabled mode it just prevent default action (useful for links)
1530
if(this.options.toggle !== 'manual') {
1531
this.$element.addClass('editable-click');
1532
this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
1533
//prevent following link if editable enabled
1534
if(!this.options.disabled) {
1535
e.preventDefault();
1536
}
1537
1538
//stop propagation not required because in document click handler it checks event target
1539
//e.stopPropagation();
1540
1541
if(this.options.toggle === 'mouseenter') {
1542
//for hover only show container
1543
this.show();
1544
} else {
1545
//when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
1546
var closeAll = (this.options.toggle !== 'click');
1547
this.toggle(closeAll);
1548
}
1549
}, this));
1550
} else {
1551
this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
1552
}
1553
1554
//if display is function it's far more convinient to have autotext = always to render correctly on init
1555
//see https://github.com/vitalets/x-editable-yii/issues/34
1556
if(typeof this.options.display === 'function') {
1557
this.options.autotext = 'always';
1558
}
1559
1560
//check conditions for autotext:
1561
switch(this.options.autotext) {
1562
case 'always':
1563
doAutotext = true;
1564
break;
1565
case 'auto':
1566
//if element text is empty and value is defined and value not generated by text --> run autotext
1567
doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText;
1568
break;
1569
default:
1570
doAutotext = false;
1571
}
1572
1573
//depending on autotext run render() or just finilize init
1574
$.when(doAutotext ? this.render() : true).then($.proxy(function() {
1575
if(this.options.disabled) {
1576
this.disable();
1577
} else {
1578
this.enable();
1579
}
1580
/**
1581
Fired when element was initialized by `$().editable()` method.
1582
Please note that you should setup `init` handler **before** applying `editable`.
1583
1584
@event init
1585
@param {Object} event event object
1586
@param {Object} editable editable instance (as here it cannot accessed via data('editable'))
1587
@since 1.2.0
1588
@example
1589
$('#username').on('init', function(e, editable) {
1590
alert('initialized ' + editable.options.name);
1591
});
1592
$('#username').editable();
1593
**/
1594
this.$element.triggerHandler('init', this);
1595
}, this));
1596
},
1597
1598
/*
1599
Initializes parent element for live editables
1600
*/
1601
initLive: function() {
1602
//store selector
1603
var selector = this.options.selector;
1604
//modify options for child elements
1605
this.options.selector = false;
1606
this.options.autotext = 'never';
1607
//listen toggle events
1608
this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){
1609
var $target = $(e.target);
1610
if(!$target.data('editable')) {
1611
//if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user)
1612
//see https://github.com/vitalets/x-editable/issues/137
1613
if($target.hasClass(this.options.emptyclass)) {
1614
$target.empty();
1615
}
1616
$target.editable(this.options).trigger(e);
1617
}
1618
}, this));
1619
},
1620
1621
/*
1622
Renders value into element's text.
1623
Can call custom display method from options.
1624
Can return deferred object.
1625
@method render()
1626
@param {mixed} response server response (if exist) to pass into display function
1627
*/
1628
render: function(response) {
1629
//do not display anything
1630
if(this.options.display === false) {
1631
return;
1632
}
1633
1634
//if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded
1635
if(this.input.value2htmlFinal) {
1636
return this.input.value2html(this.value, this.$element[0], this.options.display, response);
1637
//if display method defined --> use it
1638
} else if(typeof this.options.display === 'function') {
1639
return this.options.display.call(this.$element[0], this.value, response);
1640
//else use input's original value2html() method
1641
} else {
1642
return this.input.value2html(this.value, this.$element[0]);
1643
}
1644
},
1645
1646
/**
1647
Enables editable
1648
@method enable()
1649
**/
1650
enable: function() {
1651
this.options.disabled = false;
1652
this.$element.removeClass('editable-disabled');
1653
this.handleEmpty(this.isEmpty);
1654
if(this.options.toggle !== 'manual') {
1655
if(this.$element.attr('tabindex') === '-1') {
1656
this.$element.removeAttr('tabindex');
1657
}
1658
}
1659
},
1660
1661
/**
1662
Disables editable
1663
@method disable()
1664
**/
1665
disable: function() {
1666
this.options.disabled = true;
1667
this.hide();
1668
this.$element.addClass('editable-disabled');
1669
this.handleEmpty(this.isEmpty);
1670
//do not stop focus on this element
1671
this.$element.attr('tabindex', -1);
1672
},
1673
1674
/**
1675
Toggles enabled / disabled state of editable element
1676
@method toggleDisabled()
1677
**/
1678
toggleDisabled: function() {
1679
if(this.options.disabled) {
1680
this.enable();
1681
} else {
1682
this.disable();
1683
}
1684
},
1685
1686
/**
1687
Sets new option
1688
1689
@method option(key, value)
1690
@param {string|object} key option name or object with several options
1691
@param {mixed} value option new value
1692
@example
1693
$('.editable').editable('option', 'pk', 2);
1694
**/
1695
option: function(key, value) {
1696
//set option(s) by object
1697
if(key && typeof key === 'object') {
1698
$.each(key, $.proxy(function(k, v){
1699
this.option($.trim(k), v);
1700
}, this));
1701
return;
1702
}
1703
1704
//set option by string
1705
this.options[key] = value;
1706
1707
//disabled
1708
if(key === 'disabled') {
1709
return value ? this.disable() : this.enable();
1710
}
1711
1712
//value
1713
if(key === 'value') {
1714
this.setValue(value);
1715
}
1716
1717
//transfer new option to container!
1718
if(this.container) {
1719
this.container.option(key, value);
1720
}
1721
1722
//pass option to input directly (as it points to the same in form)
1723
if(this.input.option) {
1724
this.input.option(key, value);
1725
}
1726
1727
},
1728
1729
/*
1730
* set emptytext if element is empty
1731
*/
1732
handleEmpty: function (isEmpty) {
1733
//do not handle empty if we do not display anything
1734
if(this.options.display === false) {
1735
return;
1736
}
1737
1738
/*
1739
isEmpty may be set directly as param of method.
1740
It is required when we enable/disable field and can't rely on content
1741
as node content is text: "Empty" that is not empty %)
1742
*/
1743
if(isEmpty !== undefined) {
1744
this.isEmpty = isEmpty;
1745
} else {
1746
//detect empty
1747
//for some inputs we need more smart check
1748
//e.g. wysihtml5 may have <br>, <p></p>, <img>
1749
if(typeof(this.input.isEmpty) === 'function') {
1750
this.isEmpty = this.input.isEmpty(this.$element);
1751
} else {
1752
this.isEmpty = $.trim(this.$element.html()) === '';
1753
}
1754
}
1755
1756
//emptytext shown only for enabled
1757
if(!this.options.disabled) {
1758
if (this.isEmpty) {
1759
this.$element.html(this.options.emptytext);
1760
if(this.options.emptyclass) {
1761
this.$element.addClass(this.options.emptyclass);
1762
}
1763
} else if(this.options.emptyclass) {
1764
this.$element.removeClass(this.options.emptyclass);
1765
}
1766
} else {
1767
//below required if element disable property was changed
1768
if(this.isEmpty) {
1769
this.$element.empty();
1770
if(this.options.emptyclass) {
1771
this.$element.removeClass(this.options.emptyclass);
1772
}
1773
}
1774
}
1775
},
1776
1777
/**
1778
Shows container with form
1779
@method show()
1780
@param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1781
**/
1782
show: function (closeAll) {
1783
if(this.options.disabled) {
1784
return;
1785
}
1786
1787
//init editableContainer: popover, tooltip, inline, etc..
1788
if(!this.container) {
1789
var containerOptions = $.extend({}, this.options, {
1790
value: this.value,
1791
input: this.input //pass input to form (as it is already created)
1792
});
1793
this.$element.editableContainer(containerOptions);
1794
//listen `save` event
1795
this.$element.on("save.internal", $.proxy(this.save, this));
1796
this.container = this.$element.data('editableContainer');
1797
} else if(this.container.tip().is(':visible')) {
1798
return;
1799
}
1800
1801
//show container
1802
this.container.show(closeAll);
1803
},
1804
1805
/**
1806
Hides container with form
1807
@method hide()
1808
**/
1809
hide: function () {
1810
if(this.container) {
1811
this.container.hide();
1812
}
1813
},
1814
1815
/**
1816
Toggles container visibility (show / hide)
1817
@method toggle()
1818
@param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1819
**/
1820
toggle: function(closeAll) {
1821
if(this.container && this.container.tip().is(':visible')) {
1822
this.hide();
1823
} else {
1824
this.show(closeAll);
1825
}
1826
},
1827
1828
/*
1829
* called when form was submitted
1830
*/
1831
save: function(e, params) {
1832
//mark element with unsaved class if needed
1833
if(this.options.unsavedclass) {
1834
/*
1835
Add unsaved css to element if:
1836
- url is not user's function
1837
- value was not sent to server
1838
- params.response === undefined, that means data was not sent
1839
- value changed
1840
*/
1841
var sent = false;
1842
sent = sent || typeof this.options.url === 'function';
1843
sent = sent || this.options.display === false;
1844
sent = sent || params.response !== undefined;
1845
sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue));
1846
1847
if(sent) {
1848
this.$element.removeClass(this.options.unsavedclass);
1849
} else {
1850
this.$element.addClass(this.options.unsavedclass);
1851
}
1852
}
1853
1854
//highlight when saving
1855
if(this.options.highlight) {
1856
var $e = this.$element,
1857
bgColor = $e.css('background-color');
1858
1859
$e.css('background-color', this.options.highlight);
1860
setTimeout(function(){
1861
if(bgColor === 'transparent') {
1862
bgColor = '';
1863
}
1864
$e.css('background-color', bgColor);
1865
$e.addClass('editable-bg-transition');
1866
setTimeout(function(){
1867
$e.removeClass('editable-bg-transition');
1868
}, 1700);
1869
}, 10);
1870
}
1871
1872
//set new value
1873
this.setValue(params.newValue, false, params.response);
1874
1875
/**
1876
Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
1877
1878
@event save
1879
@param {Object} event event object
1880
@param {Object} params additional params
1881
@param {mixed} params.newValue submitted value
1882
@param {Object} params.response ajax response
1883
@example
1884
$('#username').on('save', function(e, params) {
1885
alert('Saved value: ' + params.newValue);
1886
});
1887
**/
1888
//event itself is triggered by editableContainer. Description here is only for documentation
1889
},
1890
1891
validate: function () {
1892
if (typeof this.options.validate === 'function') {
1893
return this.options.validate.call(this, this.value);
1894
}
1895
},
1896
1897
/**
1898
Sets new value of editable
1899
@method setValue(value, convertStr)
1900
@param {mixed} value new value
1901
@param {boolean} convertStr whether to convert value from string to internal format
1902
**/
1903
setValue: function(value, convertStr, response) {
1904
if(convertStr) {
1905
this.value = this.input.str2value(value);
1906
} else {
1907
this.value = value;
1908
}
1909
if(this.container) {
1910
this.container.option('value', this.value);
1911
}
1912
$.when(this.render(response))
1913
.then($.proxy(function() {
1914
this.handleEmpty();
1915
}, this));
1916
},
1917
1918
/**
1919
Activates input of visible container (e.g. set focus)
1920
@method activate()
1921
**/
1922
activate: function() {
1923
if(this.container) {
1924
this.container.activate();
1925
}
1926
},
1927
1928
/**
1929
Removes editable feature from element
1930
@method destroy()
1931
**/
1932
destroy: function() {
1933
this.disable();
1934
1935
if(this.container) {
1936
this.container.destroy();
1937
}
1938
1939
this.input.destroy();
1940
1941
if(this.options.toggle !== 'manual') {
1942
this.$element.removeClass('editable-click');
1943
this.$element.off(this.options.toggle + '.editable');
1944
}
1945
1946
this.$element.off("save.internal");
1947
1948
this.$element.removeClass('editable editable-open editable-disabled');
1949
this.$element.removeData('editable');
1950
}
1951
};
1952
1953
/* EDITABLE PLUGIN DEFINITION
1954
* ======================= */
1955
1956
/**
1957
jQuery method to initialize editable element.
1958
1959
@method $().editable(options)
1960
@params {Object} options
1961
@example
1962
$('#username').editable({
1963
type: 'text',
1964
url: '/post',
1965
pk: 1
1966
});
1967
**/
1968
$.fn.editable = function (option) {
1969
//special API methods returning non-jquery object
1970
var result = {}, args = arguments, datakey = 'editable';
1971
switch (option) {
1972
/**
1973
Runs client-side validation for all matched editables
1974
1975
@method validate()
1976
@returns {Object} validation errors map
1977
@example
1978
$('#username, #fullname').editable('validate');
1979
// possible result:
1980
{
1981
username: "username is required",
1982
fullname: "fullname should be minimum 3 letters length"
1983
}
1984
**/
1985
case 'validate':
1986
this.each(function () {
1987
var $this = $(this), data = $this.data(datakey), error;
1988
if (data && (error = data.validate())) {
1989
result[data.options.name] = error;
1990
}
1991
});
1992
return result;
1993
1994
/**
1995
Returns current values of editable elements.
1996
Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements.
1997
If value of some editable is `null` or `undefined` it is excluded from result object.
1998
When param `isSingle` is set to **true** - it is supposed you have single element and will return value of editable instead of object.
1999
2000
@method getValue()
2001
@param {bool} isSingle whether to return just value of single element
2002
@returns {Object} object of element names and values
2003
@example
2004
$('#username, #fullname').editable('getValue');
2005
//result:
2006
{
2007
username: "superuser",
2008
fullname: "John"
2009
}
2010
//isSingle = true
2011
$('#username').editable('getValue', true);
2012
//result "superuser"
2013
**/
2014
case 'getValue':
2015
if(arguments.length === 2 && arguments[1] === true) { //isSingle = true
2016
result = this.eq(0).data(datakey).value;
2017
} else {
2018
this.each(function () {
2019
var $this = $(this), data = $this.data(datakey);
2020
if (data && data.value !== undefined && data.value !== null) {
2021
result[data.options.name] = data.input.value2submit(data.value);
2022
}
2023
});
2024
}
2025
return result;
2026
2027
/**
2028
This method collects values from several editable elements and submit them all to server.
2029
Internally it runs client-side validation for all fields and submits only in case of success.
2030
See <a href="#newrecord">creating new records</a> for details.
2031
Since 1.5.1 `submit` can be applied to single element to send data programmatically. In that case
2032
`url`, `success` and `error` is taken from initial options and you can just call `$('#username').editable('submit')`.
2033
2034
@method submit(options)
2035
@param {object} options
2036
@param {object} options.url url to submit data
2037
@param {object} options.data additional data to submit
2038
@param {object} options.ajaxOptions additional ajax options
2039
@param {function} options.error(obj) error handler
2040
@param {function} options.success(obj,config) success handler
2041
@returns {Object} jQuery object
2042
**/
2043
case 'submit': //collects value, validate and submit to server for creating new record
2044
var config = arguments[1] || {},
2045
$elems = this,
2046
errors = this.editable('validate');
2047
2048
// validation ok
2049
if($.isEmptyObject(errors)) {
2050
var ajaxOptions = {};
2051
2052
// for single element use url, success etc from options
2053
if($elems.length === 1) {
2054
var editable = $elems.data('editable');
2055
//standard params
2056
var params = {
2057
name: editable.options.name || '',
2058
value: editable.input.value2submit(editable.value),
2059
pk: (typeof editable.options.pk === 'function') ?
2060
editable.options.pk.call(editable.options.scope) :
2061
editable.options.pk
2062
};
2063
2064
//additional params
2065
if(typeof editable.options.params === 'function') {
2066
params = editable.options.params.call(editable.options.scope, params);
2067
} else {
2068
//try parse json in single quotes (from data-params attribute)
2069
editable.options.params = $.fn.editableutils.tryParseJson(editable.options.params, true);
2070
$.extend(params, editable.options.params);
2071
}
2072
2073
ajaxOptions = {
2074
url: editable.options.url,
2075
data: params,
2076
type: 'POST'
2077
};
2078
2079
// use success / error from options
2080
config.success = config.success || editable.options.success;
2081
config.error = config.error || editable.options.error;
2082
2083
// multiple elements
2084
} else {
2085
var values = this.editable('getValue');
2086
2087
ajaxOptions = {
2088
url: config.url,
2089
data: values,
2090
type: 'POST'
2091
};
2092
}
2093
2094
// ajax success callabck (response 200 OK)
2095
ajaxOptions.success = typeof config.success === 'function' ? function(response) {
2096
config.success.call($elems, response, config);
2097
} : $.noop;
2098
2099
// ajax error callabck
2100
ajaxOptions.error = typeof config.error === 'function' ? function() {
2101
config.error.apply($elems, arguments);
2102
} : $.noop;
2103
2104
// extend ajaxOptions
2105
if(config.ajaxOptions) {
2106
$.extend(ajaxOptions, config.ajaxOptions);
2107
}
2108
2109
// extra data
2110
if(config.data) {
2111
$.extend(ajaxOptions.data, config.data);
2112
}
2113
2114
// perform ajax request
2115
$.ajax(ajaxOptions);
2116
} else { //client-side validation error
2117
if(typeof config.error === 'function') {
2118
config.error.call($elems, errors);
2119
}
2120
}
2121
return this;
2122
}
2123
2124
//return jquery object
2125
return this.each(function () {
2126
var $this = $(this),
2127
data = $this.data(datakey),
2128
options = typeof option === 'object' && option;
2129
2130
//for delegated targets do not store `editable` object for element
2131
//it's allows several different selectors.
2132
//see: https://github.com/vitalets/x-editable/issues/312
2133
if(options && options.selector) {
2134
data = new Editable(this, options);
2135
return;
2136
}
2137
2138
if (!data) {
2139
$this.data(datakey, (data = new Editable(this, options)));
2140
}
2141
2142
if (typeof option === 'string') { //call method
2143
data[option].apply(data, Array.prototype.slice.call(args, 1));
2144
}
2145
});
2146
};
2147
2148
2149
$.fn.editable.defaults = {
2150
/**
2151
Type of input. Can be <code>text|textarea|select|date|checklist</code> and more
2152
2153
@property type
2154
@type string
2155
@default 'text'
2156
**/
2157
type: 'text',
2158
/**
2159
Sets disabled state of editable
2160
2161
@property disabled
2162
@type boolean
2163
@default false
2164
**/
2165
disabled: false,
2166
/**
2167
How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>.
2168
When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
2169
**Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element,
2170
you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document.
2171
2172
@example
2173
$('#edit-button').click(function(e) {
2174
e.stopPropagation();
2175
$('#username').editable('toggle');
2176
});
2177
2178
@property toggle
2179
@type string
2180
@default 'click'
2181
**/
2182
toggle: 'click',
2183
/**
2184
Text shown when element is empty.
2185
2186
@property emptytext
2187
@type string
2188
@default 'Empty'
2189
**/
2190
emptytext: 'Empty',
2191
/**
2192
Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date.
2193
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>.
2194
<code>auto</code> - text will be automatically set only if element is empty.
2195
<code>always|never</code> - always(never) try to set element's text.
2196
2197
@property autotext
2198
@type string
2199
@default 'auto'
2200
**/
2201
autotext: 'auto',
2202
/**
2203
Initial value of input. If not set, taken from element's text.
2204
Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option).
2205
For example, to display currency sign:
2206
@example
2207
<a id="price" data-type="text" data-value="100"></a>
2208
<script>
2209
$('#price').editable({
2210
...
2211
display: function(value) {
2212
$(this).text(value + '$');
2213
}
2214
})
2215
</script>
2216
2217
@property value
2218
@type mixed
2219
@default element's text
2220
**/
2221
value: null,
2222
/**
2223
Callback to perform custom displaying of value in element's text.
2224
If `null`, default input's display used.
2225
If `false`, no displaying methods will be called, element's text will never change.
2226
Runs under element's scope.
2227
_**Parameters:**_
2228
2229
* `value` current value to be displayed
2230
* `response` server response (if display called after ajax submit), since 1.4.0
2231
2232
For _inputs with source_ (select, checklist) parameters are different:
2233
2234
* `value` current value to be displayed
2235
* `sourceData` array of items for current input (e.g. dropdown items)
2236
* `response` server response (if display called after ajax submit), since 1.4.0
2237
2238
To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`.
2239
2240
@property display
2241
@type function|boolean
2242
@default null
2243
@since 1.2.0
2244
@example
2245
display: function(value, sourceData) {
2246
//display checklist as comma-separated values
2247
var html = [],
2248
checked = $.fn.editableutils.itemsByValue(value, sourceData);
2249
2250
if(checked.length) {
2251
$.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
2252
$(this).html(html.join(', '));
2253
} else {
2254
$(this).empty();
2255
}
2256
}
2257
**/
2258
display: null,
2259
/**
2260
Css class applied when editable text is empty.
2261
2262
@property emptyclass
2263
@type string
2264
@since 1.4.1
2265
@default editable-empty
2266
**/
2267
emptyclass: 'editable-empty',
2268
/**
2269
Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`).
2270
You may set it to `null` if you work with editables locally and submit them together.
2271
2272
@property unsavedclass
2273
@type string
2274
@since 1.4.1
2275
@default editable-unsaved
2276
**/
2277
unsavedclass: 'editable-unsaved',
2278
/**
2279
If selector is provided, editable will be delegated to the specified targets.
2280
Usefull for dynamically generated DOM elements.
2281
**Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options,
2282
as they actually become editable only after first click.
2283
You should manually set class `editable-click` to these elements.
2284
Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element:
2285
2286
@property selector
2287
@type string
2288
@since 1.4.1
2289
@default null
2290
@example
2291
<div id="user">
2292
<!-- empty -->
2293
<a href="#" data-name="username" data-type="text" class="editable-click editable-empty" data-value="" title="Username">Empty</a>
2294
<!-- non-empty -->
2295
<a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" class="editable-click" title="Group">Operator</a>
2296
</div>
2297
2298
<script>
2299
$('#user').editable({
2300
selector: 'a',
2301
url: '/post',
2302
pk: 1
2303
});
2304
</script>
2305
**/
2306
selector: null,
2307
/**
2308
Color used to highlight element after update. Implemented via CSS3 transition, works in modern browsers.
2309
2310
@property highlight
2311
@type string|boolean
2312
@since 1.4.5
2313
@default #FFFF80
2314
**/
2315
highlight: '#FFFF80'
2316
};
2317
2318
}(window.jQuery));
2319
2320
/**
2321
AbstractInput - base class for all editable inputs.
2322
It defines interface to be implemented by any input type.
2323
To create your own input you can inherit from this class.
2324
2325
@class abstractinput
2326
**/
2327
(function ($) {
2328
"use strict";
2329
2330
//types
2331
$.fn.editabletypes = {};
2332
2333
var AbstractInput = function () { };
2334
2335
AbstractInput.prototype = {
2336
/**
2337
Initializes input
2338
2339
@method init()
2340
**/
2341
init: function(type, options, defaults) {
2342
this.type = type;
2343
this.options = $.extend({}, defaults, options);
2344
},
2345
2346
/*
2347
this method called before render to init $tpl that is inserted in DOM
2348
*/
2349
prerender: function() {
2350
this.$tpl = $(this.options.tpl); //whole tpl as jquery object
2351
this.$input = this.$tpl; //control itself, can be changed in render method
2352
this.$clear = null; //clear button
2353
this.error = null; //error message, if input cannot be rendered
2354
},
2355
2356
/**
2357
Renders input from tpl. Can return jQuery deferred object.
2358
Can be overwritten in child objects
2359
2360
@method render()
2361
**/
2362
render: function() {
2363
2364
},
2365
2366
/**
2367
Sets element's html by value.
2368
2369
@method value2html(value, element)
2370
@param {mixed} value
2371
@param {DOMElement} element
2372
**/
2373
value2html: function(value, element) {
2374
$(element)[this.options.escape ? 'text' : 'html']($.trim(value));
2375
},
2376
2377
/**
2378
Converts element's html to value
2379
2380
@method html2value(html)
2381
@param {string} html
2382
@returns {mixed}
2383
**/
2384
html2value: function(html) {
2385
return $('<div>').html(html).text();
2386
},
2387
2388
/**
2389
Converts value to string (for internal compare). For submitting to server used value2submit().
2390
2391
@method value2str(value)
2392
@param {mixed} value
2393
@returns {string}
2394
**/
2395
value2str: function(value) {
2396
return value;
2397
},
2398
2399
/**
2400
Converts string received from server into value. Usually from `data-value` attribute.
2401
2402
@method str2value(str)
2403
@param {string} str
2404
@returns {mixed}
2405
**/
2406
str2value: function(str) {
2407
return str;
2408
},
2409
2410
/**
2411
Converts value for submitting to server. Result can be string or object.
2412
2413
@method value2submit(value)
2414
@param {mixed} value
2415
@returns {mixed}
2416
**/
2417
value2submit: function(value) {
2418
return value;
2419
},
2420
2421
/**
2422
Sets value of input.
2423
2424
@method value2input(value)
2425
@param {mixed} value
2426
**/
2427
value2input: function(value) {
2428
this.$input.val(value);
2429
},
2430
2431
/**
2432
Returns value of input. Value can be object (e.g. datepicker)
2433
2434
@method input2value()
2435
**/
2436
input2value: function() {
2437
return this.$input.val();
2438
},
2439
2440
/**
2441
Activates input. For text it sets focus.
2442
2443
@method activate()
2444
**/
2445
activate: function() {
2446
if(this.$input.is(':visible')) {
2447
this.$input.focus();
2448
}
2449
},
2450
2451
/**
2452
Creates input.
2453
2454
@method clear()
2455
**/
2456
clear: function() {
2457
this.$input.val(null);
2458
},
2459
2460
/**
2461
method to escape html.
2462
**/
2463
escape: function(str) {
2464
return $('<div>').text(str).html();
2465
},
2466
2467
/**
2468
attach handler to automatically submit form when value changed (useful when buttons not shown)
2469
**/
2470
autosubmit: function() {
2471
2472
},
2473
2474
/**
2475
Additional actions when destroying element
2476
**/
2477
destroy: function() {
2478
},
2479
2480
// -------- helper functions --------
2481
setClass: function() {
2482
if(this.options.inputclass) {
2483
this.$input.addClass(this.options.inputclass);
2484
}
2485
},
2486
2487
setAttr: function(attr) {
2488
if (this.options[attr] !== undefined && this.options[attr] !== null) {
2489
this.$input.attr(attr, this.options[attr]);
2490
}
2491
},
2492
2493
option: function(key, value) {
2494
this.options[key] = value;
2495
}
2496
2497
};
2498
2499
AbstractInput.defaults = {
2500
/**
2501
HTML template of input. Normally you should not change it.
2502
2503
@property tpl
2504
@type string
2505
@default ''
2506
**/
2507
tpl: '',
2508
/**
2509
CSS class automatically applied to input
2510
2511
@property inputclass
2512
@type string
2513
@default null
2514
**/
2515
inputclass: null,
2516
2517
/**
2518
If `true` - html will be escaped in content of element via $.text() method.
2519
If `false` - html will not be escaped, $.html() used.
2520
When you use own `display` function, this option obviosly has no effect.
2521
2522
@property escape
2523
@type boolean
2524
@since 1.5.0
2525
@default true
2526
**/
2527
escape: true,
2528
2529
//scope for external methods (e.g. source defined as function)
2530
//for internal use only
2531
scope: null,
2532
2533
//need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults)
2534
showbuttons: true
2535
};
2536
2537
$.extend($.fn.editabletypes, {abstractinput: AbstractInput});
2538
2539
}(window.jQuery));
2540
2541
/**
2542
List - abstract class for inputs that have source option loaded from js array or via ajax
2543
2544
@class list
2545
@extends abstractinput
2546
**/
2547
(function ($) {
2548
"use strict";
2549
2550
var List = function (options) {
2551
2552
};
2553
2554
$.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);
2555
2556
$.extend(List.prototype, {
2557
render: function () {
2558
var deferred = $.Deferred();
2559
2560
this.error = null;
2561
this.onSourceReady(function () {
2562
this.renderList();
2563
deferred.resolve();
2564
}, function () {
2565
this.error = this.options.sourceError;
2566
deferred.resolve();
2567
});
2568
2569
return deferred.promise();
2570
},
2571
2572
html2value: function (html) {
2573
return null; //can't set value by text
2574
},
2575
2576
value2html: function (value, element, display, response) {
2577
var deferred = $.Deferred(),
2578
success = function () {
2579
if(typeof display === 'function') {
2580
//custom display method
2581
display.call(element, value, this.sourceData, response);
2582
} else {
2583
this.value2htmlFinal(value, element);
2584
}
2585
deferred.resolve();
2586
};
2587
2588
//for null value just call success without loading source
2589
if(value === null) {
2590
success.call(this);
2591
} else {
2592
this.onSourceReady(success, function () { deferred.resolve(); });
2593
}
2594
2595
return deferred.promise();
2596
},
2597
2598
// ------------- additional functions ------------
2599
2600
onSourceReady: function (success, error) {
2601
//run source if it function
2602
var source;
2603
if ($.isFunction(this.options.source)) {
2604
source = this.options.source.call(this.options.scope);
2605
this.sourceData = null;
2606
//note: if function returns the same source as URL - sourceData will be taken from cahce and no extra request performed
2607
} else {
2608
source = this.options.source;
2609
}
2610
2611
//if allready loaded just call success
2612
if(this.options.sourceCache && $.isArray(this.sourceData)) {
2613
success.call(this);
2614
return;
2615
}
2616
2617
//try parse json in single quotes (for double quotes jquery does automatically)
2618
try {
2619
source = $.fn.editableutils.tryParseJson(source, false);
2620
} catch (e) {
2621
error.call(this);
2622
return;
2623
}
2624
2625
//loading from url
2626
if (typeof source === 'string') {
2627
//try to get sourceData from cache
2628
if(this.options.sourceCache) {
2629
var cacheID = source,
2630
cache;
2631
2632
if (!$(document).data(cacheID)) {
2633
$(document).data(cacheID, {});
2634
}
2635
cache = $(document).data(cacheID);
2636
2637
//check for cached data
2638
if (cache.loading === false && cache.sourceData) { //take source from cache
2639
this.sourceData = cache.sourceData;
2640
this.doPrepend();
2641
success.call(this);
2642
return;
2643
} else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
2644
cache.callbacks.push($.proxy(function () {
2645
this.sourceData = cache.sourceData;
2646
this.doPrepend();
2647
success.call(this);
2648
}, this));
2649
2650
//also collecting error callbacks
2651
cache.err_callbacks.push($.proxy(error, this));
2652
return;
2653
} else { //no cache yet, activate it
2654
cache.loading = true;
2655
cache.callbacks = [];
2656
cache.err_callbacks = [];
2657
}
2658
}
2659
2660
//ajaxOptions for source. Can be overwritten bt options.sourceOptions
2661
var ajaxOptions = $.extend({
2662
url: source,
2663
type: 'get',
2664
cache: false,
2665
dataType: 'json',
2666
success: $.proxy(function (data) {
2667
if(cache) {
2668
cache.loading = false;
2669
}
2670
this.sourceData = this.makeArray(data);
2671
if($.isArray(this.sourceData)) {
2672
if(cache) {
2673
//store result in cache
2674
cache.sourceData = this.sourceData;
2675
//run success callbacks for other fields waiting for this source
2676
$.each(cache.callbacks, function () { this.call(); });
2677
}
2678
this.doPrepend();
2679
success.call(this);
2680
} else {
2681
error.call(this);
2682
if(cache) {
2683
//run error callbacks for other fields waiting for this source
2684
$.each(cache.err_callbacks, function () { this.call(); });
2685
}
2686
}
2687
}, this),
2688
error: $.proxy(function () {
2689
error.call(this);
2690
if(cache) {
2691
cache.loading = false;
2692
//run error callbacks for other fields
2693
$.each(cache.err_callbacks, function () { this.call(); });
2694
}
2695
}, this)
2696
}, this.options.sourceOptions);
2697
2698
//loading sourceData from server
2699
$.ajax(ajaxOptions);
2700
2701
} else { //options as json/array
2702
this.sourceData = this.makeArray(source);
2703
2704
if($.isArray(this.sourceData)) {
2705
this.doPrepend();
2706
success.call(this);
2707
} else {
2708
error.call(this);
2709
}
2710
}
2711
},
2712
2713
doPrepend: function () {
2714
if(this.options.prepend === null || this.options.prepend === undefined) {
2715
return;
2716
}
2717
2718
if(!$.isArray(this.prependData)) {
2719
//run prepend if it is function (once)
2720
if ($.isFunction(this.options.prepend)) {
2721
this.options.prepend = this.options.prepend.call(this.options.scope);
2722
}
2723
2724
//try parse json in single quotes
2725
this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);
2726
2727
//convert prepend from string to object
2728
if (typeof this.options.prepend === 'string') {
2729
this.options.prepend = {'': this.options.prepend};
2730
}
2731
2732
this.prependData = this.makeArray(this.options.prepend);
2733
}
2734
2735
if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
2736
this.sourceData = this.prependData.concat(this.sourceData);
2737
}
2738
},
2739
2740
/*
2741
renders input list
2742
*/
2743
renderList: function() {
2744
// this method should be overwritten in child class
2745
},
2746
2747
/*
2748
set element's html by value
2749
*/
2750
value2htmlFinal: function(value, element) {
2751
// this method should be overwritten in child class
2752
},
2753
2754
/**
2755
* convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
2756
*/
2757
makeArray: function(data) {
2758
var count, obj, result = [], item, iterateItem;
2759
if(!data || typeof data === 'string') {
2760
return null;
2761
}
2762
2763
if($.isArray(data)) { //array
2764
/*
2765
function to iterate inside item of array if item is object.
2766
Caclulates count of keys in item and store in obj.
2767
*/
2768
iterateItem = function (k, v) {
2769
obj = {value: k, text: v};
2770
if(count++ >= 2) {
2771
return false;// exit from `each` if item has more than one key.
2772
}
2773
};
2774
2775
for(var i = 0; i < data.length; i++) {
2776
item = data[i];
2777
if(typeof item === 'object') {
2778
count = 0; //count of keys inside item
2779
$.each(item, iterateItem);
2780
//case: [{val1: 'text1'}, {val2: 'text2} ...]
2781
if(count === 1) {
2782
result.push(obj);
2783
//case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
2784
} else if(count > 1) {
2785
//removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
2786
if(item.children) {
2787
item.children = this.makeArray(item.children);
2788
}
2789
result.push(item);
2790
}
2791
} else {
2792
//case: ['text1', 'text2' ...]
2793
result.push({value: item, text: item});
2794
}
2795
}
2796
} else { //case: {val1: 'text1', val2: 'text2, ...}
2797
$.each(data, function (k, v) {
2798
result.push({value: k, text: v});
2799
});
2800
}
2801
return result;
2802
},
2803
2804
option: function(key, value) {
2805
this.options[key] = value;
2806
if(key === 'source') {
2807
this.sourceData = null;
2808
}
2809
if(key === 'prepend') {
2810
this.prependData = null;
2811
}
2812
}
2813
2814
});
2815
2816
List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2817
/**
2818
Source data for list.
2819
If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
2820
For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
2821
2822
If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.
2823
2824
If **function**, it should return data in format above (since 1.4.0).
2825
2826
Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
2827
`[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`
2828
2829
2830
@property source
2831
@type string | array | object | function
2832
@default null
2833
**/
2834
source: null,
2835
/**
2836
Data automatically prepended to the beginning of dropdown list.
2837
2838
@property prepend
2839
@type string | array | object | function
2840
@default false
2841
**/
2842
prepend: false,
2843
/**
2844
Error message when list cannot be loaded (e.g. ajax error)
2845
2846
@property sourceError
2847
@type string
2848
@default Error when loading list
2849
**/
2850
sourceError: 'Error when loading list',
2851
/**
2852
if <code>true</code> and source is **string url** - results will be cached for fields with the same source.
2853
Usefull for editable column in grid to prevent extra requests.
2854
2855
@property sourceCache
2856
@type boolean
2857
@default true
2858
@since 1.2.0
2859
**/
2860
sourceCache: true,
2861
/**
2862
Additional ajax options to be used in $.ajax() when loading list from server.
2863
Useful to send extra parameters (`data` key) or change request method (`type` key).
2864
2865
@property sourceOptions
2866
@type object|function
2867
@default null
2868
@since 1.5.0
2869
**/
2870
sourceOptions: null
2871
});
2872
2873
$.fn.editabletypes.list = List;
2874
2875
}(window.jQuery));
2876
2877
/**
2878
Text input
2879
2880
@class text
2881
@extends abstractinput
2882
@final
2883
@example
2884
<a href="#" id="username" data-type="text" data-pk="1">awesome</a>
2885
<script>
2886
$(function(){
2887
$('#username').editable({
2888
url: '/post',
2889
title: 'Enter username'
2890
});
2891
});
2892
</script>
2893
**/
2894
(function ($) {
2895
"use strict";
2896
2897
var Text = function (options) {
2898
this.init('text', options, Text.defaults);
2899
};
2900
2901
$.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
2902
2903
$.extend(Text.prototype, {
2904
render: function() {
2905
this.renderClear();
2906
this.setClass();
2907
this.setAttr('placeholder');
2908
},
2909
2910
activate: function() {
2911
if(this.$input.is(':visible')) {
2912
this.$input.focus();
2913
$.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2914
if(this.toggleClear) {
2915
this.toggleClear();
2916
}
2917
}
2918
},
2919
2920
//render clear button
2921
renderClear: function() {
2922
if (this.options.clear) {
2923
this.$clear = $('<span class="editable-clear-x"></span>');
2924
this.$input.after(this.$clear)
2925
.css('padding-right', 24)
2926
.keyup($.proxy(function(e) {
2927
//arrows, enter, tab, etc
2928
if(~$.inArray(e.keyCode, [40,38,9,13,27])) {
2929
return;
2930
}
2931
2932
clearTimeout(this.t);
2933
var that = this;
2934
this.t = setTimeout(function() {
2935
that.toggleClear(e);
2936
}, 100);
2937
2938
}, this))
2939
.parent().css('position', 'relative');
2940
2941
this.$clear.click($.proxy(this.clear, this));
2942
}
2943
},
2944
2945
postrender: function() {
2946
/*
2947
//now `clear` is positioned via css
2948
if(this.$clear) {
2949
//can position clear button only here, when form is shown and height can be calculated
2950
// var h = this.$input.outerHeight(true) || 20,
2951
var h = this.$clear.parent().height(),
2952
delta = (h - this.$clear.height()) / 2;
2953
2954
//this.$clear.css({bottom: delta, right: delta});
2955
}
2956
*/
2957
},
2958
2959
//show / hide clear button
2960
toggleClear: function(e) {
2961
if(!this.$clear) {
2962
return;
2963
}
2964
2965
var len = this.$input.val().length,
2966
visible = this.$clear.is(':visible');
2967
2968
if(len && !visible) {
2969
this.$clear.show();
2970
}
2971
2972
if(!len && visible) {
2973
this.$clear.hide();
2974
}
2975
},
2976
2977
clear: function() {
2978
this.$clear.hide();
2979
this.$input.val('').focus();
2980
}
2981
});
2982
2983
Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2984
/**
2985
@property tpl
2986
@default <input type="text">
2987
**/
2988
tpl: '<input type="text">',
2989
/**
2990
Placeholder attribute of input. Shown when input is empty.
2991
2992
@property placeholder
2993
@type string
2994
@default null
2995
**/
2996
placeholder: null,
2997
2998
/**
2999
Whether to show `clear` button
3000
3001
@property clear
3002
@type boolean
3003
@default true
3004
**/
3005
clear: true
3006
});
3007
3008
$.fn.editabletypes.text = Text;
3009
3010
}(window.jQuery));
3011
3012
/**
3013
Textarea input
3014
3015
@class textarea
3016
@extends abstractinput
3017
@final
3018
@example
3019
<a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a>
3020
<script>
3021
$(function(){
3022
$('#comments').editable({
3023
url: '/post',
3024
title: 'Enter comments',
3025
rows: 10
3026
});
3027
});
3028
</script>
3029
**/
3030
(function ($) {
3031
"use strict";
3032
3033
var Textarea = function (options) {
3034
this.init('textarea', options, Textarea.defaults);
3035
};
3036
3037
$.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput);
3038
3039
$.extend(Textarea.prototype, {
3040
render: function () {
3041
this.setClass();
3042
this.setAttr('placeholder');
3043
this.setAttr('rows');
3044
3045
//ctrl + enter
3046
this.$input.keydown(function (e) {
3047
if (e.ctrlKey && e.which === 13) {
3048
$(this).closest('form').submit();
3049
}
3050
});
3051
},
3052
3053
//using `white-space: pre-wrap` solves \n <--> BR conversion very elegant!
3054
/*
3055
value2html: function(value, element) {
3056
var html = '', lines;
3057
if(value) {
3058
lines = value.split("\n");
3059
for (var i = 0; i < lines.length; i++) {
3060
lines[i] = $('<div>').text(lines[i]).html();
3061
}
3062
html = lines.join('<br>');
3063
}
3064
$(element).html(html);
3065
},
3066
3067
html2value: function(html) {
3068
if(!html) {
3069
return '';
3070
}
3071
3072
var regex = new RegExp(String.fromCharCode(10), 'g');
3073
var lines = html.split(/<br\s*\/?>/i);
3074
for (var i = 0; i < lines.length; i++) {
3075
var text = $('<div>').html(lines[i]).text();
3076
3077
// Remove newline characters (\n) to avoid them being converted by value2html() method
3078
// thus adding extra <br> tags
3079
text = text.replace(regex, '');
3080
3081
lines[i] = text;
3082
}
3083
return lines.join("\n");
3084
},
3085
*/
3086
activate: function() {
3087
$.fn.editabletypes.text.prototype.activate.call(this);
3088
}
3089
});
3090
3091
Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3092
/**
3093
@property tpl
3094
@default <textarea></textarea>
3095
**/
3096
tpl:'<textarea></textarea>',
3097
/**
3098
@property inputclass
3099
@default input-large
3100
**/
3101
inputclass: 'input-large',
3102
/**
3103
Placeholder attribute of input. Shown when input is empty.
3104
3105
@property placeholder
3106
@type string
3107
@default null
3108
**/
3109
placeholder: null,
3110
/**
3111
Number of rows in textarea
3112
3113
@property rows
3114
@type integer
3115
@default 7
3116
**/
3117
rows: 7
3118
});
3119
3120
$.fn.editabletypes.textarea = Textarea;
3121
3122
}(window.jQuery));
3123
3124
/**
3125
Select (dropdown)
3126
3127
@class select
3128
@extends list
3129
@final
3130
@example
3131
<a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-title="Select status"></a>
3132
<script>
3133
$(function(){
3134
$('#status').editable({
3135
value: 2,
3136
source: [
3137
{value: 1, text: 'Active'},
3138
{value: 2, text: 'Blocked'},
3139
{value: 3, text: 'Deleted'}
3140
]
3141
});
3142
});
3143
</script>
3144
**/
3145
(function ($) {
3146
"use strict";
3147
3148
var Select = function (options) {
3149
this.init('select', options, Select.defaults);
3150
};
3151
3152
$.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
3153
3154
$.extend(Select.prototype, {
3155
renderList: function() {
3156
this.$input.empty();
3157
3158
var fillItems = function($el, data) {
3159
var attr;
3160
if($.isArray(data)) {
3161
for(var i=0; i<data.length; i++) {
3162
attr = {};
3163
if(data[i].children) {
3164
attr.label = data[i].text;
3165
$el.append(fillItems($('<optgroup>', attr), data[i].children));
3166
} else {
3167
attr.value = data[i].value;
3168
if(data[i].disabled) {
3169
attr.disabled = true;
3170
}
3171
$el.append($('<option>', attr).text(data[i].text));
3172
}
3173
}
3174
}
3175
return $el;
3176
};
3177
3178
fillItems(this.$input, this.sourceData);
3179
3180
this.setClass();
3181
3182
//enter submit
3183
this.$input.on('keydown.editable', function (e) {
3184
if (e.which === 13) {
3185
$(this).closest('form').submit();
3186
}
3187
});
3188
},
3189
3190
value2htmlFinal: function(value, element) {
3191
var text = '',
3192
items = $.fn.editableutils.itemsByValue(value, this.sourceData);
3193
3194
if(items.length) {
3195
text = items[0].text;
3196
}
3197
3198
//$(element).text(text);
3199
$.fn.editabletypes.abstractinput.prototype.value2html.call(this, text, element);
3200
},
3201
3202
autosubmit: function() {
3203
this.$input.off('keydown.editable').on('change.editable', function(){
3204
$(this).closest('form').submit();
3205
});
3206
}
3207
});
3208
3209
Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
3210
/**
3211
@property tpl
3212
@default <select></select>
3213
**/
3214
tpl:'<select></select>'
3215
});
3216
3217
$.fn.editabletypes.select = Select;
3218
3219
}(window.jQuery));
3220
3221
/**
3222
List of checkboxes.
3223
Internally value stored as javascript array of values.
3224
3225
@class checklist
3226
@extends list
3227
@final
3228
@example
3229
<a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-title="Select options"></a>
3230
<script>
3231
$(function(){
3232
$('#options').editable({
3233
value: [2, 3],
3234
source: [
3235
{value: 1, text: 'option1'},
3236
{value: 2, text: 'option2'},
3237
{value: 3, text: 'option3'}
3238
]
3239
});
3240
});
3241
</script>
3242
**/
3243
(function ($) {
3244
"use strict";
3245
3246
var Checklist = function (options) {
3247
this.init('checklist', options, Checklist.defaults);
3248
};
3249
3250
$.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list);
3251
3252
$.extend(Checklist.prototype, {
3253
renderList: function() {
3254
var $label, $div;
3255
3256
this.$tpl.empty();
3257
3258
if(!$.isArray(this.sourceData)) {
3259
return;
3260
}
3261
3262
for(var i=0; i<this.sourceData.length; i++) {
3263
$label = $('<label>').append($('<input>', {
3264
type: 'checkbox',
3265
value: this.sourceData[i].value
3266
}))
3267
.append($('<span>').text(' '+this.sourceData[i].text));
3268
3269
$('<div>').append($label).appendTo(this.$tpl);
3270
}
3271
3272
this.$input = this.$tpl.find('input[type="checkbox"]');
3273
this.setClass();
3274
},
3275
3276
value2str: function(value) {
3277
return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : '';
3278
},
3279
3280
//parse separated string
3281
str2value: function(str) {
3282
var reg, value = null;
3283
if(typeof str === 'string' && str.length) {
3284
reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*');
3285
value = str.split(reg);
3286
} else if($.isArray(str)) {
3287
value = str;
3288
} else {
3289
value = [str];
3290
}
3291
return value;
3292
},
3293
3294
//set checked on required checkboxes
3295
value2input: function(value) {
3296
this.$input.prop('checked', false);
3297
if($.isArray(value) && value.length) {
3298
this.$input.each(function(i, el) {
3299
var $el = $(el);
3300
// cannot use $.inArray as it performs strict comparison
3301
$.each(value, function(j, val){
3302
/*jslint eqeq: true*/
3303
if($el.val() == val) {
3304
/*jslint eqeq: false*/
3305
$el.prop('checked', true);
3306
}
3307
});
3308
});
3309
}
3310
},
3311
3312
input2value: function() {
3313
var checked = [];
3314
this.$input.filter(':checked').each(function(i, el) {
3315
checked.push($(el).val());
3316
});
3317
return checked;
3318
},
3319
3320
//collect text of checked boxes
3321
value2htmlFinal: function(value, element) {
3322
var html = [],
3323
checked = $.fn.editableutils.itemsByValue(value, this.sourceData),
3324
escape = this.options.escape;
3325
3326
if(checked.length) {
3327
$.each(checked, function(i, v) {
3328
var text = escape ? $.fn.editableutils.escape(v.text) : v.text;
3329
html.push(text);
3330
});
3331
$(element).html(html.join('<br>'));
3332
} else {
3333
$(element).empty();
3334
}
3335
},
3336
3337
activate: function() {
3338
this.$input.first().focus();
3339
},
3340
3341
autosubmit: function() {
3342
this.$input.on('keydown', function(e){
3343
if (e.which === 13) {
3344
$(this).closest('form').submit();
3345
}
3346
});
3347
}
3348
});
3349
3350
Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
3351
/**
3352
@property tpl
3353
@default <div></div>
3354
**/
3355
tpl:'<div class="editable-checklist"></div>',
3356
3357
/**
3358
@property inputclass
3359
@type string
3360
@default null
3361
**/
3362
inputclass: null,
3363
3364
/**
3365
Separator of values when reading from `data-value` attribute
3366
3367
@property separator
3368
@type string
3369
@default ','
3370
**/
3371
separator: ','
3372
});
3373
3374
$.fn.editabletypes.checklist = Checklist;
3375
3376
}(window.jQuery));
3377
3378
/**
3379
HTML5 input types.
3380
Following types are supported:
3381
3382
* password
3383
* email
3384
* url
3385
* tel
3386
* number
3387
* range
3388
* time
3389
3390
Learn more about html5 inputs:
3391
http://www.w3.org/wiki/HTML5_form_additions
3392
To check browser compatibility please see:
3393
https://developer.mozilla.org/en-US/docs/HTML/Element/Input
3394
3395
@class html5types
3396
@extends text
3397
@final
3398
@since 1.3.0
3399
@example
3400
<a href="#" id="email" data-type="email" data-pk="1">[email protected]</a>
3401
<script>
3402
$(function(){
3403
$('#email').editable({
3404
url: '/post',
3405
title: 'Enter email'
3406
});
3407
});
3408
</script>
3409
**/
3410
3411
/**
3412
@property tpl
3413
@default depends on type
3414
**/
3415
3416
/*
3417
Password
3418
*/
3419
(function ($) {
3420
"use strict";
3421
3422
var Password = function (options) {
3423
this.init('password', options, Password.defaults);
3424
};
3425
$.fn.editableutils.inherit(Password, $.fn.editabletypes.text);
3426
$.extend(Password.prototype, {
3427
//do not display password, show '[hidden]' instead
3428
value2html: function(value, element) {
3429
if(value) {
3430
$(element).text('[hidden]');
3431
} else {
3432
$(element).empty();
3433
}
3434
},
3435
//as password not displayed, should not set value by html
3436
html2value: function(html) {
3437
return null;
3438
}
3439
});
3440
Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3441
tpl: '<input type="password">'
3442
});
3443
$.fn.editabletypes.password = Password;
3444
}(window.jQuery));
3445
3446
3447
/*
3448
Email
3449
*/
3450
(function ($) {
3451
"use strict";
3452
3453
var Email = function (options) {
3454
this.init('email', options, Email.defaults);
3455
};
3456
$.fn.editableutils.inherit(Email, $.fn.editabletypes.text);
3457
Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3458
tpl: '<input type="email">'
3459
});
3460
$.fn.editabletypes.email = Email;
3461
}(window.jQuery));
3462
3463
3464
/*
3465
Url
3466
*/
3467
(function ($) {
3468
"use strict";
3469
3470
var Url = function (options) {
3471
this.init('url', options, Url.defaults);
3472
};
3473
$.fn.editableutils.inherit(Url, $.fn.editabletypes.text);
3474
Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3475
tpl: '<input type="url">'
3476
});
3477
$.fn.editabletypes.url = Url;
3478
}(window.jQuery));
3479
3480
3481
/*
3482
Tel
3483
*/
3484
(function ($) {
3485
"use strict";
3486
3487
var Tel = function (options) {
3488
this.init('tel', options, Tel.defaults);
3489
};
3490
$.fn.editableutils.inherit(Tel, $.fn.editabletypes.text);
3491
Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3492
tpl: '<input type="tel">'
3493
});
3494
$.fn.editabletypes.tel = Tel;
3495
}(window.jQuery));
3496
3497
3498
/*
3499
Number
3500
*/
3501
(function ($) {
3502
"use strict";
3503
3504
var NumberInput = function (options) {
3505
this.init('number', options, NumberInput.defaults);
3506
};
3507
$.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text);
3508
$.extend(NumberInput.prototype, {
3509
render: function () {
3510
NumberInput.superclass.render.call(this);
3511
this.setAttr('min');
3512
this.setAttr('max');
3513
this.setAttr('step');
3514
},
3515
postrender: function() {
3516
if(this.$clear) {
3517
//increase right ffset for up/down arrows
3518
this.$clear.css({right: 24});
3519
/*
3520
//can position clear button only here, when form is shown and height can be calculated
3521
var h = this.$input.outerHeight(true) || 20,
3522
delta = (h - this.$clear.height()) / 2;
3523
3524
//add 12px to offset right for up/down arrows
3525
this.$clear.css({top: delta, right: delta + 16});
3526
*/
3527
}
3528
}
3529
});
3530
NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3531
tpl: '<input type="number">',
3532
inputclass: 'input-mini',
3533
min: null,
3534
max: null,
3535
step: null
3536
});
3537
$.fn.editabletypes.number = NumberInput;
3538
}(window.jQuery));
3539
3540
3541
/*
3542
Range (inherit from number)
3543
*/
3544
(function ($) {
3545
"use strict";
3546
3547
var Range = function (options) {
3548
this.init('range', options, Range.defaults);
3549
};
3550
$.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
3551
$.extend(Range.prototype, {
3552
render: function () {
3553
this.$input = this.$tpl.filter('input');
3554
3555
this.setClass();
3556
this.setAttr('min');
3557
this.setAttr('max');
3558
this.setAttr('step');
3559
3560
this.$input.on('input', function(){
3561
$(this).siblings('output').text($(this).val());
3562
});
3563
},
3564
activate: function() {
3565
this.$input.focus();
3566
}
3567
});
3568
Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
3569
tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>',
3570
inputclass: 'input-medium'
3571
});
3572
$.fn.editabletypes.range = Range;
3573
}(window.jQuery));
3574
3575
/*
3576
Time
3577
*/
3578
(function ($) {
3579
"use strict";
3580
3581
var Time = function (options) {
3582
this.init('time', options, Time.defaults);
3583
};
3584
//inherit from abstract, as inheritance from text gives selection error.
3585
$.fn.editableutils.inherit(Time, $.fn.editabletypes.abstractinput);
3586
$.extend(Time.prototype, {
3587
render: function() {
3588
this.setClass();
3589
}
3590
});
3591
Time.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3592
tpl: '<input type="time">'
3593
});
3594
$.fn.editabletypes.time = Time;
3595
}(window.jQuery));
3596
3597
/**
3598
Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2.
3599
Please see [original select2 docs](http://ivaynberg.github.com/select2) for detailed description and options.
3600
3601
You should manually download and include select2 distributive:
3602
3603
<link href="select2/select2.css" rel="stylesheet" type="text/css"></link>
3604
<script src="select2/select2.js"></script>
3605
3606
To make it **bootstrap-styled** you can use css from [here](https://github.com/t0m/select2-bootstrap-css):
3607
3608
<link href="select2-bootstrap.css" rel="stylesheet" type="text/css"></link>
3609
3610
**Note:** currently `autotext` feature does not work for select2 with `ajax` remote source.
3611
You need initially put both `data-value` and element's text youself:
3612
3613
<a href="#" data-type="select2" data-value="1">Text1</a>
3614
3615
3616
@class select2
3617
@extends abstractinput
3618
@since 1.4.1
3619
@final
3620
@example
3621
<a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-title="Select country"></a>
3622
<script>
3623
$(function(){
3624
//local source
3625
$('#country').editable({
3626
source: [
3627
{id: 'gb', text: 'Great Britain'},
3628
{id: 'us', text: 'United States'},
3629
{id: 'ru', text: 'Russia'}
3630
],
3631
select2: {
3632
multiple: true
3633
}
3634
});
3635
//remote source (simple)
3636
$('#country').editable({
3637
source: '/getCountries',
3638
select2: {
3639
placeholder: 'Select Country',
3640
minimumInputLength: 1
3641
}
3642
});
3643
//remote source (advanced)
3644
$('#country').editable({
3645
select2: {
3646
placeholder: 'Select Country',
3647
allowClear: true,
3648
minimumInputLength: 3,
3649
id: function (item) {
3650
return item.CountryId;
3651
},
3652
ajax: {
3653
url: '/getCountries',
3654
dataType: 'json',
3655
data: function (term, page) {
3656
return { query: term };
3657
},
3658
results: function (data, page) {
3659
return { results: data };
3660
}
3661
},
3662
formatResult: function (item) {
3663
return item.CountryName;
3664
},
3665
formatSelection: function (item) {
3666
return item.CountryName;
3667
},
3668
initSelection: function (element, callback) {
3669
return $.get('/getCountryById', { query: element.val() }, function (data) {
3670
callback(data);
3671
});
3672
}
3673
}
3674
});
3675
});
3676
</script>
3677
**/
3678
(function ($) {
3679
"use strict";
3680
3681
var Constructor = function (options) {
3682
this.init('select2', options, Constructor.defaults);
3683
3684
options.select2 = options.select2 || {};
3685
3686
this.sourceData = null;
3687
3688
//placeholder
3689
if(options.placeholder) {
3690
options.select2.placeholder = options.placeholder;
3691
}
3692
3693
//if not `tags` mode, use source
3694
if(!options.select2.tags && options.source) {
3695
var source = options.source;
3696
//if source is function, call it (once!)
3697
if ($.isFunction(options.source)) {
3698
source = options.source.call(options.scope);
3699
}
3700
3701
if (typeof source === 'string') {
3702
options.select2.ajax = options.select2.ajax || {};
3703
//some default ajax params
3704
if(!options.select2.ajax.data) {
3705
options.select2.ajax.data = function(term) {return { query:term };};
3706
}
3707
if(!options.select2.ajax.results) {
3708
options.select2.ajax.results = function(data) { return {results:data };};
3709
}
3710
options.select2.ajax.url = source;
3711
} else {
3712
//check format and convert x-editable format to select2 format (if needed)
3713
this.sourceData = this.convertSource(source);
3714
options.select2.data = this.sourceData;
3715
}
3716
}
3717
3718
//overriding objects in config (as by default jQuery extend() is not recursive)
3719
this.options.select2 = $.extend({}, Constructor.defaults.select2, options.select2);
3720
3721
//detect whether it is multi-valued
3722
this.isMultiple = this.options.select2.tags || this.options.select2.multiple;
3723
this.isRemote = ('ajax' in this.options.select2);
3724
3725
//store function returning ID of item
3726
//should be here as used inautotext for local source
3727
this.idFunc = this.options.select2.id;
3728
if (typeof(this.idFunc) !== "function") {
3729
var idKey = this.idFunc || 'id';
3730
this.idFunc = function (e) { return e[idKey]; };
3731
}
3732
3733
//store function that renders text in select2
3734
this.formatSelection = this.options.select2.formatSelection;
3735
if (typeof(this.formatSelection) !== "function") {
3736
this.formatSelection = function (e) { return e.text; };
3737
}
3738
};
3739
3740
$.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
3741
3742
$.extend(Constructor.prototype, {
3743
render: function() {
3744
this.setClass();
3745
3746
//can not apply select2 here as it calls initSelection
3747
//over input that does not have correct value yet.
3748
//apply select2 only in value2input
3749
//this.$input.select2(this.options.select2);
3750
3751
//when data is loaded via ajax, we need to know when it's done to populate listData
3752
if(this.isRemote) {
3753
//listen to loaded event to populate data
3754
this.$input.on('select2-loaded', $.proxy(function(e) {
3755
this.sourceData = e.items.results;
3756
}, this));
3757
}
3758
3759
//trigger resize of editableform to re-position container in multi-valued mode
3760
if(this.isMultiple) {
3761
this.$input.on('change', function() {
3762
$(this).closest('form').parent().triggerHandler('resize');
3763
});
3764
}
3765
},
3766
3767
value2html: function(value, element) {
3768
var text = '', data,
3769
that = this;
3770
3771
if(this.options.select2.tags) { //in tags mode just assign value
3772
data = value;
3773
//data = $.fn.editableutils.itemsByValue(value, this.options.select2.tags, this.idFunc);
3774
} else if(this.sourceData) {
3775
data = $.fn.editableutils.itemsByValue(value, this.sourceData, this.idFunc);
3776
} else {
3777
//can not get list of possible values
3778
//(e.g. autotext for select2 with ajax source)
3779
}
3780
3781
//data may be array (when multiple values allowed)
3782
if($.isArray(data)) {
3783
//collect selected data and show with separator
3784
text = [];
3785
$.each(data, function(k, v){
3786
text.push(v && typeof v === 'object' ? that.formatSelection(v) : v);
3787
});
3788
} else if(data) {
3789
text = that.formatSelection(data);
3790
}
3791
3792
text = $.isArray(text) ? text.join(this.options.viewseparator) : text;
3793
3794
//$(element).text(text);
3795
Constructor.superclass.value2html.call(this, text, element);
3796
},
3797
3798
html2value: function(html) {
3799
return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null;
3800
},
3801
3802
value2input: function(value) {
3803
// if value array => join it anyway
3804
if($.isArray(value)) {
3805
value = value.join(this.getSeparator());
3806
}
3807
3808
//for remote source just set value, text is updated by initSelection
3809
if(!this.$input.data('select2')) {
3810
this.$input.val(value);
3811
this.$input.select2(this.options.select2);
3812
} else {
3813
//second argument needed to separate initial change from user's click (for autosubmit)
3814
this.$input.val(value).trigger('change', true);
3815
3816
//Uncaught Error: cannot call val() if initSelection() is not defined
3817
//this.$input.select2('val', value);
3818
}
3819
3820
// if defined remote source AND no multiple mode AND no user's initSelection provided -->
3821
// we should somehow get text for provided id.
3822
// The solution is to use element's text as text for that id (exclude empty)
3823
if(this.isRemote && !this.isMultiple && !this.options.select2.initSelection) {
3824
// customId and customText are methods to extract `id` and `text` from data object
3825
// we can use this workaround only if user did not define these methods
3826
// otherwise we cant construct data object
3827
var customId = this.options.select2.id,
3828
customText = this.options.select2.formatSelection;
3829
3830
if(!customId && !customText) {
3831
var $el = $(this.options.scope);
3832
if (!$el.data('editable').isEmpty) {
3833
var data = {id: value, text: $el.text()};
3834
this.$input.select2('data', data);
3835
}
3836
}
3837
}
3838
},
3839
3840
input2value: function() {
3841
return this.$input.select2('val');
3842
},
3843
3844
str2value: function(str, separator) {
3845
if(typeof str !== 'string' || !this.isMultiple) {
3846
return str;
3847
}
3848
3849
separator = separator || this.getSeparator();
3850
3851
var val, i, l;
3852
3853
if (str === null || str.length < 1) {
3854
return null;
3855
}
3856
val = str.split(separator);
3857
for (i = 0, l = val.length; i < l; i = i + 1) {
3858
val[i] = $.trim(val[i]);
3859
}
3860
3861
return val;
3862
},
3863
3864
autosubmit: function() {
3865
this.$input.on('change', function(e, isInitial){
3866
if(!isInitial) {
3867
$(this).closest('form').submit();
3868
}
3869
});
3870
},
3871
3872
getSeparator: function() {
3873
return this.options.select2.separator || $.fn.select2.defaults.separator;
3874
},
3875
3876
/*
3877
Converts source from x-editable format: {value: 1, text: "1"} to
3878
select2 format: {id: 1, text: "1"}
3879
*/
3880
convertSource: function(source) {
3881
if($.isArray(source) && source.length && source[0].value !== undefined) {
3882
for(var i = 0; i<source.length; i++) {
3883
if(source[i].value !== undefined) {
3884
source[i].id = source[i].value;
3885
delete source[i].value;
3886
}
3887
}
3888
}
3889
return source;
3890
},
3891
3892
destroy: function() {
3893
if(this.$input.data('select2')) {
3894
this.$input.select2('destroy');
3895
}
3896
}
3897
3898
});
3899
3900
Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3901
/**
3902
@property tpl
3903
@default <input type="hidden">
3904
**/
3905
tpl:'<input type="hidden">',
3906
/**
3907
Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2).
3908
3909
@property select2
3910
@type object
3911
@default null
3912
**/
3913
select2: null,
3914
/**
3915
Placeholder attribute of select
3916
3917
@property placeholder
3918
@type string
3919
@default null
3920
**/
3921
placeholder: null,
3922
/**
3923
Source data for select. It will be assigned to select2 `data` property and kept here just for convenience.
3924
Please note, that format is different from simple `select` input: use 'id' instead of 'value'.
3925
E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`.
3926
3927
@property source
3928
@type array|string|function
3929
@default null
3930
**/
3931
source: null,
3932
/**
3933
Separator used to display tags.
3934
3935
@property viewseparator
3936
@type string
3937
@default ', '
3938
**/
3939
viewseparator: ', '
3940
});
3941
3942
$.fn.editabletypes.select2 = Constructor;
3943
3944
}(window.jQuery));
3945
3946
/**
3947
* Combodate - 1.0.5
3948
* Dropdown date and time picker.
3949
* Converts text input into dropdowns to pick day, month, year, hour, minute and second.
3950
* Uses momentjs as datetime library http://momentjs.com.
3951
* For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang
3952
*
3953
* Confusion at noon and midnight - see http://en.wikipedia.org/wiki/12-hour_clock#Confusion_at_noon_and_midnight
3954
* In combodate:
3955
* 12:00 pm --> 12:00 (24-h format, midday)
3956
* 12:00 am --> 00:00 (24-h format, midnight, start of day)
3957
*
3958
* Differs from momentjs parse rules:
3959
* 00:00 pm, 12:00 pm --> 12:00 (24-h format, day not change)
3960
* 00:00 am, 12:00 am --> 00:00 (24-h format, day not change)
3961
*
3962
*
3963
* Author: Vitaliy Potapov
3964
* Project page: http://github.com/vitalets/combodate
3965
* Copyright (c) 2012 Vitaliy Potapov. Released under MIT License.
3966
**/
3967
(function ($) {
3968
3969
var Combodate = function (element, options) {
3970
this.$element = $(element);
3971
if(!this.$element.is('input')) {
3972
$.error('Combodate should be applied to INPUT element');
3973
return;
3974
}
3975
this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data());
3976
this.init();
3977
};
3978
3979
Combodate.prototype = {
3980
constructor: Combodate,
3981
init: function () {
3982
this.map = {
3983
//key regexp moment.method
3984
day: ['D', 'date'],
3985
month: ['M', 'month'],
3986
year: ['Y', 'year'],
3987
hour: ['[Hh]', 'hours'],
3988
minute: ['m', 'minutes'],
3989
second: ['s', 'seconds'],
3990
ampm: ['[Aa]', '']
3991
};
3992
3993
this.$widget = $('<span class="combodate"></span>').html(this.getTemplate());
3994
3995
this.initCombos();
3996
3997
//update original input on change
3998
this.$widget.on('change', 'select', $.proxy(function(e) {
3999
this.$element.val(this.getValue()).change();
4000
// update days count if month or year changes
4001
if (this.options.smartDays) {
4002
if ($(e.target).is('.month') || $(e.target).is('.year')) {
4003
this.fillCombo('day');
4004
}
4005
}
4006
}, this));
4007
4008
this.$widget.find('select').css('width', 'auto');
4009
4010
// hide original input and insert widget
4011
this.$element.hide().after(this.$widget);
4012
4013
// set initial value
4014
this.setValue(this.$element.val() || this.options.value);
4015
},
4016
4017
/*
4018
Replace tokens in template with <select> elements
4019
*/
4020
getTemplate: function() {
4021
var tpl = this.options.template;
4022
4023
//first pass
4024
$.each(this.map, function(k, v) {
4025
v = v[0];
4026
var r = new RegExp(v+'+'),
4027
token = v.length > 1 ? v.substring(1, 2) : v;
4028
4029
tpl = tpl.replace(r, '{'+token+'}');
4030
});
4031
4032
//replace spaces with &nbsp;
4033
tpl = tpl.replace(/ /g, '&nbsp;');
4034
4035
//second pass
4036
$.each(this.map, function(k, v) {
4037
v = v[0];
4038
var token = v.length > 1 ? v.substring(1, 2) : v;
4039
4040
tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>');
4041
});
4042
4043
return tpl;
4044
},
4045
4046
/*
4047
Initialize combos that presents in template
4048
*/
4049
initCombos: function() {
4050
for (var k in this.map) {
4051
var $c = this.$widget.find('.'+k);
4052
// set properties like this.$day, this.$month etc.
4053
this['$'+k] = $c.length ? $c : null;
4054
// fill with items
4055
this.fillCombo(k);
4056
}
4057
},
4058
4059
/*
4060
Fill combo with items
4061
*/
4062
fillCombo: function(k) {
4063
var $combo = this['$'+k];
4064
if (!$combo) {
4065
return;
4066
}
4067
4068
// define method name to fill items, e.g `fillDays`
4069
var f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1);
4070
var items = this[f]();
4071
var value = $combo.val();
4072
4073
$combo.empty();
4074
for(var i=0; i<items.length; i++) {
4075
$combo.append('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
4076
}
4077
4078
$combo.val(value);
4079
},
4080
4081
/*
4082
Initialize items of combos. Handles `firstItem` option
4083
*/
4084
fillCommon: function(key) {
4085
var values = [],
4086
relTime;
4087
4088
if(this.options.firstItem === 'name') {
4089
//need both to support moment ver < 2 and >= 2
4090
relTime = moment.relativeTime || moment.langData()._relativeTime;
4091
var header = typeof relTime[key] === 'function' ? relTime[key](1, true, key, false) : relTime[key];
4092
//take last entry (see momentjs lang files structure)
4093
header = header.split(' ').reverse()[0];
4094
values.push(['', header]);
4095
} else if(this.options.firstItem === 'empty') {
4096
values.push(['', '']);
4097
}
4098
return values;
4099
},
4100
4101
4102
/*
4103
fill day
4104
*/
4105
fillDay: function() {
4106
var items = this.fillCommon('d'), name, i,
4107
twoDigit = this.options.template.indexOf('DD') !== -1,
4108
daysCount = 31;
4109
4110
// detect days count (depends on month and year)
4111
// originally https://github.com/vitalets/combodate/pull/7
4112
if (this.options.smartDays && this.$month && this.$year) {
4113
var month = parseInt(this.$month.val(), 10);
4114
var year = parseInt(this.$year.val(), 10);
4115
4116
if (!isNaN(month) && !isNaN(year)) {
4117
daysCount = moment([year, month]).daysInMonth();
4118
}
4119
}
4120
4121
for (i = 1; i <= daysCount; i++) {
4122
name = twoDigit ? this.leadZero(i) : i;
4123
items.push([i, name]);
4124
}
4125
return items;
4126
},
4127
4128
/*
4129
fill month
4130
*/
4131
fillMonth: function() {
4132
var items = this.fillCommon('M'), name, i,
4133
longNames = this.options.template.indexOf('MMMM') !== -1,
4134
shortNames = this.options.template.indexOf('MMM') !== -1,
4135
twoDigit = this.options.template.indexOf('MM') !== -1;
4136
4137
for(i=0; i<=11; i++) {
4138
if(longNames) {
4139
//see https://github.com/timrwood/momentjs.com/pull/36
4140
name = moment().date(1).month(i).format('MMMM');
4141
} else if(shortNames) {
4142
name = moment().date(1).month(i).format('MMM');
4143
} else if(twoDigit) {
4144
name = this.leadZero(i+1);
4145
} else {
4146
name = i+1;
4147
}
4148
items.push([i, name]);
4149
}
4150
return items;
4151
},
4152
4153
/*
4154
fill year
4155
*/
4156
fillYear: function() {
4157
var items = [], name, i,
4158
longNames = this.options.template.indexOf('YYYY') !== -1;
4159
4160
for(i=this.options.maxYear; i>=this.options.minYear; i--) {
4161
name = longNames ? i : (i+'').substring(2);
4162
items[this.options.yearDescending ? 'push' : 'unshift']([i, name]);
4163
}
4164
4165
items = this.fillCommon('y').concat(items);
4166
4167
return items;
4168
},
4169
4170
/*
4171
fill hour
4172
*/
4173
fillHour: function() {
4174
var items = this.fillCommon('h'), name, i,
4175
h12 = this.options.template.indexOf('h') !== -1,
4176
h24 = this.options.template.indexOf('H') !== -1,
4177
twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1,
4178
min = h12 ? 1 : 0,
4179
max = h12 ? 12 : 23;
4180
4181
for(i=min; i<=max; i++) {
4182
name = twoDigit ? this.leadZero(i) : i;
4183
items.push([i, name]);
4184
}
4185
return items;
4186
},
4187
4188
/*
4189
fill minute
4190
*/
4191
fillMinute: function() {
4192
var items = this.fillCommon('m'), name, i,
4193
twoDigit = this.options.template.indexOf('mm') !== -1;
4194
4195
for(i=0; i<=59; i+= this.options.minuteStep) {
4196
name = twoDigit ? this.leadZero(i) : i;
4197
items.push([i, name]);
4198
}
4199
return items;
4200
},
4201
4202
/*
4203
fill second
4204
*/
4205
fillSecond: function() {
4206
var items = this.fillCommon('s'), name, i,
4207
twoDigit = this.options.template.indexOf('ss') !== -1;
4208
4209
for(i=0; i<=59; i+= this.options.secondStep) {
4210
name = twoDigit ? this.leadZero(i) : i;
4211
items.push([i, name]);
4212
}
4213
return items;
4214
},
4215
4216
/*
4217
fill ampm
4218
*/
4219
fillAmpm: function() {
4220
var ampmL = this.options.template.indexOf('a') !== -1,
4221
ampmU = this.options.template.indexOf('A') !== -1,
4222
items = [
4223
['am', ampmL ? 'am' : 'AM'],
4224
['pm', ampmL ? 'pm' : 'PM']
4225
];
4226
return items;
4227
},
4228
4229
/*
4230
Returns current date value from combos.
4231
If format not specified - `options.format` used.
4232
If format = `null` - Moment object returned.
4233
*/
4234
getValue: function(format) {
4235
var dt, values = {},
4236
that = this,
4237
notSelected = false;
4238
4239
//getting selected values
4240
$.each(this.map, function(k, v) {
4241
if(k === 'ampm') {
4242
return;
4243
}
4244
var def = k === 'day' ? 1 : 0;
4245
4246
values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def;
4247
4248
if(isNaN(values[k])) {
4249
notSelected = true;
4250
return false;
4251
}
4252
});
4253
4254
//if at least one visible combo not selected - return empty string
4255
if(notSelected) {
4256
return '';
4257
}
4258
4259
//convert hours 12h --> 24h
4260
if(this.$ampm) {
4261
//12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
4262
if(values.hour === 12) {
4263
values.hour = this.$ampm.val() === 'am' ? 0 : 12;
4264
} else {
4265
values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
4266
}
4267
}
4268
4269
dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
4270
4271
//highlight invalid date
4272
this.highlight(dt);
4273
4274
format = format === undefined ? this.options.format : format;
4275
if(format === null) {
4276
return dt.isValid() ? dt : null;
4277
} else {
4278
return dt.isValid() ? dt.format(format) : '';
4279
}
4280
},
4281
4282
setValue: function(value) {
4283
if(!value) {
4284
return;
4285
}
4286
4287
var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value),
4288
that = this,
4289
values = {};
4290
4291
//function to find nearest value in select options
4292
function getNearest($select, value) {
4293
var delta = {};
4294
$select.children('option').each(function(i, opt){
4295
var optValue = $(opt).attr('value'),
4296
distance;
4297
4298
if(optValue === '') return;
4299
distance = Math.abs(optValue - value);
4300
if(typeof delta.distance === 'undefined' || distance < delta.distance) {
4301
delta = {value: optValue, distance: distance};
4302
}
4303
});
4304
return delta.value;
4305
}
4306
4307
if(dt.isValid()) {
4308
//read values from date object
4309
$.each(this.map, function(k, v) {
4310
if(k === 'ampm') {
4311
return;
4312
}
4313
values[k] = dt[v[1]]();
4314
});
4315
4316
if(this.$ampm) {
4317
//12:00 pm --> 12:00 (24-h format, midday), 12:00 am --> 00:00 (24-h format, midnight, start of day)
4318
if(values.hour >= 12) {
4319
values.ampm = 'pm';
4320
if(values.hour > 12) {
4321
values.hour -= 12;
4322
}
4323
} else {
4324
values.ampm = 'am';
4325
if(values.hour === 0) {
4326
values.hour = 12;
4327
}
4328
}
4329
}
4330
4331
$.each(values, function(k, v) {
4332
//call val() for each existing combo, e.g. this.$hour.val()
4333
if(that['$'+k]) {
4334
4335
if(k === 'minute' && that.options.minuteStep > 1 && that.options.roundTime) {
4336
v = getNearest(that['$'+k], v);
4337
}
4338
4339
if(k === 'second' && that.options.secondStep > 1 && that.options.roundTime) {
4340
v = getNearest(that['$'+k], v);
4341
}
4342
4343
that['$'+k].val(v);
4344
}
4345
});
4346
4347
// update days count
4348
if (this.options.smartDays) {
4349
this.fillCombo('day');
4350
}
4351
4352
this.$element.val(dt.format(this.options.format)).change();
4353
}
4354
},
4355
4356
/*
4357
highlight combos if date is invalid
4358
*/
4359
highlight: function(dt) {
4360
if(!dt.isValid()) {
4361
if(this.options.errorClass) {
4362
this.$widget.addClass(this.options.errorClass);
4363
} else {
4364
//store original border color
4365
if(!this.borderColor) {
4366
this.borderColor = this.$widget.find('select').css('border-color');
4367
}
4368
this.$widget.find('select').css('border-color', 'red');
4369
}
4370
} else {
4371
if(this.options.errorClass) {
4372
this.$widget.removeClass(this.options.errorClass);
4373
} else {
4374
this.$widget.find('select').css('border-color', this.borderColor);
4375
}
4376
}
4377
},
4378
4379
leadZero: function(v) {
4380
return v <= 9 ? '0' + v : v;
4381
},
4382
4383
destroy: function() {
4384
this.$widget.remove();
4385
this.$element.removeData('combodate').show();
4386
}
4387
4388
//todo: clear method
4389
};
4390
4391
$.fn.combodate = function ( option ) {
4392
var d, args = Array.apply(null, arguments);
4393
args.shift();
4394
4395
//getValue returns date as string / object (not jQuery object)
4396
if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) {
4397
return d.getValue.apply(d, args);
4398
}
4399
4400
return this.each(function () {
4401
var $this = $(this),
4402
data = $this.data('combodate'),
4403
options = typeof option == 'object' && option;
4404
if (!data) {
4405
$this.data('combodate', (data = new Combodate(this, options)));
4406
}
4407
if (typeof option == 'string' && typeof data[option] == 'function') {
4408
data[option].apply(data, args);
4409
}
4410
});
4411
};
4412
4413
$.fn.combodate.defaults = {
4414
//in this format value stored in original input
4415
format: 'DD-MM-YYYY HH:mm',
4416
//in this format items in dropdowns are displayed
4417
template: 'D / MMM / YYYY H : mm',
4418
//initial value, can be `new Date()`
4419
value: null,
4420
minYear: 1970,
4421
maxYear: 2015,
4422
yearDescending: true,
4423
minuteStep: 5,
4424
secondStep: 1,
4425
firstItem: 'empty', //'name', 'empty', 'none'
4426
errorClass: null,
4427
roundTime: true, // whether to round minutes and seconds if step > 1
4428
smartDays: false // whether days in combo depend on selected month: 31, 30, 28
4429
};
4430
4431
}(window.jQuery));
4432
/**
4433
Combodate input - dropdown date and time picker.
4434
Based on [combodate](http://vitalets.github.com/combodate) plugin (included). To use it you should manually include [momentjs](http://momentjs.com).
4435
4436
<script src="js/moment.min.js"></script>
4437
4438
Allows to input:
4439
4440
* only date
4441
* only time
4442
* both date and time
4443
4444
Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker.
4445
Internally value stored as `momentjs` object.
4446
4447
@class combodate
4448
@extends abstractinput
4449
@final
4450
@since 1.4.0
4451
@example
4452
<a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-title="Select date"></a>
4453
<script>
4454
$(function(){
4455
$('#dob').editable({
4456
format: 'YYYY-MM-DD',
4457
viewformat: 'DD.MM.YYYY',
4458
template: 'D / MMMM / YYYY',
4459
combodate: {
4460
minYear: 2000,
4461
maxYear: 2015,
4462
minuteStep: 1
4463
}
4464
}
4465
});
4466
});
4467
</script>
4468
**/
4469
4470
/*global moment*/
4471
4472
(function ($) {
4473
"use strict";
4474
4475
var Constructor = function (options) {
4476
this.init('combodate', options, Constructor.defaults);
4477
4478
//by default viewformat equals to format
4479
if(!this.options.viewformat) {
4480
this.options.viewformat = this.options.format;
4481
}
4482
4483
//try parse combodate config defined as json string in data-combodate
4484
options.combodate = $.fn.editableutils.tryParseJson(options.combodate, true);
4485
4486
//overriding combodate config (as by default jQuery extend() is not recursive)
4487
this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, {
4488
format: this.options.format,
4489
template: this.options.template
4490
});
4491
};
4492
4493
$.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
4494
4495
$.extend(Constructor.prototype, {
4496
render: function () {
4497
this.$input.combodate(this.options.combodate);
4498
4499
if($.fn.editableform.engine === 'bs3') {
4500
this.$input.siblings().find('select').addClass('form-control');
4501
}
4502
4503
if(this.options.inputclass) {
4504
this.$input.siblings().find('select').addClass(this.options.inputclass);
4505
}
4506
//"clear" link
4507
/*
4508
if(this.options.clear) {
4509
this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
4510
e.preventDefault();
4511
e.stopPropagation();
4512
this.clear();
4513
}, this));
4514
4515
this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
4516
}
4517
*/
4518
},
4519
4520
value2html: function(value, element) {
4521
var text = value ? value.format(this.options.viewformat) : '';
4522
//$(element).text(text);
4523
Constructor.superclass.value2html.call(this, text, element);
4524
},
4525
4526
html2value: function(html) {
4527
return html ? moment(html, this.options.viewformat) : null;
4528
},
4529
4530
value2str: function(value) {
4531
return value ? value.format(this.options.format) : '';
4532
},
4533
4534
str2value: function(str) {
4535
return str ? moment(str, this.options.format) : null;
4536
},
4537
4538
value2submit: function(value) {
4539
return this.value2str(value);
4540
},
4541
4542
value2input: function(value) {
4543
this.$input.combodate('setValue', value);
4544
},
4545
4546
input2value: function() {
4547
return this.$input.combodate('getValue', null);
4548
},
4549
4550
activate: function() {
4551
this.$input.siblings('.combodate').find('select').eq(0).focus();
4552
},
4553
4554
/*
4555
clear: function() {
4556
this.$input.data('datepicker').date = null;
4557
this.$input.find('.active').removeClass('active');
4558
},
4559
*/
4560
4561
autosubmit: function() {
4562
4563
}
4564
4565
});
4566
4567
Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
4568
/**
4569
@property tpl
4570
@default <input type="text">
4571
**/
4572
tpl:'<input type="text">',
4573
/**
4574
@property inputclass
4575
@default null
4576
**/
4577
inputclass: null,
4578
/**
4579
Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
4580
See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format)
4581
4582
@property format
4583
@type string
4584
@default YYYY-MM-DD
4585
**/
4586
format:'YYYY-MM-DD',
4587
/**
4588
Format used for displaying date. Also applied when converting date from element's text on init.
4589
If not specified equals to `format`.
4590
4591
@property viewformat
4592
@type string
4593
@default null
4594
**/
4595
viewformat: null,
4596
/**
4597
Template used for displaying dropdowns.
4598
4599
@property template
4600
@type string
4601
@default D / MMM / YYYY
4602
**/
4603
template: 'D / MMM / YYYY',
4604
/**
4605
Configuration of combodate.
4606
Full list of options: http://vitalets.github.com/combodate/#docs
4607
4608
@property combodate
4609
@type object
4610
@default null
4611
**/
4612
combodate: null
4613
4614
/*
4615
(not implemented yet)
4616
Text shown as clear date button.
4617
If <code>false</code> clear button will not be rendered.
4618
4619
@property clear
4620
@type boolean|string
4621
@default 'x clear'
4622
*/
4623
//clear: '&times; clear'
4624
});
4625
4626
$.fn.editabletypes.combodate = Constructor;
4627
4628
}(window.jQuery));
4629
4630
/*
4631
Editableform based on Twitter Bootstrap 3
4632
*/
4633
(function ($) {
4634
"use strict";
4635
4636
//store parent methods
4637
var pInitInput = $.fn.editableform.Constructor.prototype.initInput;
4638
4639
$.extend($.fn.editableform.Constructor.prototype, {
4640
initTemplate: function() {
4641
this.$form = $($.fn.editableform.template);
4642
this.$form.find('.control-group').addClass('form-group');
4643
this.$form.find('.editable-error-block').addClass('help-block');
4644
},
4645
initInput: function() {
4646
pInitInput.apply(this);
4647
4648
//for bs3 set default class `input-sm` to standard inputs
4649
var emptyInputClass = this.input.options.inputclass === null || this.input.options.inputclass === false;
4650
var defaultClass = 'input-sm';
4651
4652
//bs3 add `form-control` class to standard inputs
4653
var stdtypes = 'text,select,textarea,password,email,url,tel,number,range,time,typeaheadjs'.split(',');
4654
if(~$.inArray(this.input.type, stdtypes)) {
4655
this.input.$input.addClass('form-control');
4656
if(emptyInputClass) {
4657
this.input.options.inputclass = defaultClass;
4658
this.input.$input.addClass(defaultClass);
4659
}
4660
}
4661
4662
//apply bs3 size class also to buttons (to fit size of control)
4663
var $btn = this.$form.find('.editable-buttons');
4664
var classes = emptyInputClass ? [defaultClass] : this.input.options.inputclass.split(' ');
4665
for(var i=0; i<classes.length; i++) {
4666
// `btn-sm` is default now
4667
/*
4668
if(classes[i].toLowerCase() === 'input-sm') {
4669
$btn.find('button').addClass('btn-sm');
4670
}
4671
*/
4672
if(classes[i].toLowerCase() === 'input-lg') {
4673
$btn.find('button').removeClass('btn-sm').addClass('btn-lg');
4674
}
4675
}
4676
}
4677
});
4678
4679
//buttons
4680
$.fn.editableform.buttons =
4681
'<button type="submit" class="btn btn-primary btn-sm editable-submit">'+
4682
'<i class="glyphicon glyphicon-ok"></i>'+
4683
'</button>'+
4684
'<button type="button" class="btn btn-default btn-sm editable-cancel">'+
4685
'<i class="glyphicon glyphicon-remove"></i>'+
4686
'</button>';
4687
4688
//error classes
4689
$.fn.editableform.errorGroupClass = 'has-error';
4690
$.fn.editableform.errorBlockClass = null;
4691
//engine
4692
$.fn.editableform.engine = 'bs3';
4693
}(window.jQuery));
4694
/**
4695
* Editable Popover3 (for Bootstrap 3)
4696
* ---------------------
4697
* requires bootstrap-popover.js
4698
*/
4699
(function ($) {
4700
"use strict";
4701
4702
//extend methods
4703
$.extend($.fn.editableContainer.Popup.prototype, {
4704
containerName: 'popover',
4705
containerDataName: 'bs.popover',
4706
innerCss: '.popover-content',
4707
defaults: $.fn.popover.Constructor.DEFAULTS,
4708
4709
initContainer: function(){
4710
$.extend(this.containerOptions, {
4711
trigger: 'manual',
4712
selector: false,
4713
content: ' ',
4714
template: this.defaults.template
4715
});
4716
4717
//as template property is used in inputs, hide it from popover
4718
var t;
4719
if(this.$element.data('template')) {
4720
t = this.$element.data('template');
4721
this.$element.removeData('template');
4722
}
4723
4724
this.call(this.containerOptions);
4725
4726
if(t) {
4727
//restore data('template')
4728
this.$element.data('template', t);
4729
}
4730
},
4731
4732
/* show */
4733
innerShow: function () {
4734
this.call('show');
4735
},
4736
4737
/* hide */
4738
innerHide: function () {
4739
this.call('hide');
4740
},
4741
4742
/* destroy */
4743
innerDestroy: function() {
4744
this.call('destroy');
4745
},
4746
4747
setContainerOption: function(key, value) {
4748
this.container().options[key] = value;
4749
},
4750
4751
/**
4752
* move popover to new position. This function mainly copied from bootstrap-popover.
4753
*/
4754
/*jshint laxcomma: true, eqeqeq: false*/
4755
setPosition: function () {
4756
4757
(function() {
4758
/*
4759
var $tip = this.tip()
4760
, inside
4761
, pos
4762
, actualWidth
4763
, actualHeight
4764
, placement
4765
, tp
4766
, tpt
4767
, tpb
4768
, tpl
4769
, tpr;
4770
4771
placement = typeof this.options.placement === 'function' ?
4772
this.options.placement.call(this, $tip[0], this.$element[0]) :
4773
this.options.placement;
4774
4775
inside = /in/.test(placement);
4776
4777
$tip
4778
// .detach()
4779
//vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover
4780
.removeClass('top right bottom left')
4781
.css({ top: 0, left: 0, display: 'block' });
4782
// .insertAfter(this.$element);
4783
4784
pos = this.getPosition(inside);
4785
4786
actualWidth = $tip[0].offsetWidth;
4787
actualHeight = $tip[0].offsetHeight;
4788
4789
placement = inside ? placement.split(' ')[1] : placement;
4790
4791
tpb = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2};
4792
tpt = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2};
4793
tpl = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth};
4794
tpr = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width};
4795
4796
switch (placement) {
4797
case 'bottom':
4798
if ((tpb.top + actualHeight) > ($(window).scrollTop() + $(window).height())) {
4799
if (tpt.top > $(window).scrollTop()) {
4800
placement = 'top';
4801
} else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
4802
placement = 'right';
4803
} else if (tpl.left > $(window).scrollLeft()) {
4804
placement = 'left';
4805
} else {
4806
placement = 'right';
4807
}
4808
}
4809
break;
4810
case 'top':
4811
if (tpt.top < $(window).scrollTop()) {
4812
if ((tpb.top + actualHeight) < ($(window).scrollTop() + $(window).height())) {
4813
placement = 'bottom';
4814
} else if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
4815
placement = 'right';
4816
} else if (tpl.left > $(window).scrollLeft()) {
4817
placement = 'left';
4818
} else {
4819
placement = 'right';
4820
}
4821
}
4822
break;
4823
case 'left':
4824
if (tpl.left < $(window).scrollLeft()) {
4825
if ((tpr.left + actualWidth) < ($(window).scrollLeft() + $(window).width())) {
4826
placement = 'right';
4827
} else if (tpt.top > $(window).scrollTop()) {
4828
placement = 'top';
4829
} else if (tpt.top > $(window).scrollTop()) {
4830
placement = 'bottom';
4831
} else {
4832
placement = 'right';
4833
}
4834
}
4835
break;
4836
case 'right':
4837
if ((tpr.left + actualWidth) > ($(window).scrollLeft() + $(window).width())) {
4838
if (tpl.left > $(window).scrollLeft()) {
4839
placement = 'left';
4840
} else if (tpt.top > $(window).scrollTop()) {
4841
placement = 'top';
4842
} else if (tpt.top > $(window).scrollTop()) {
4843
placement = 'bottom';
4844
}
4845
}
4846
break;
4847
}
4848
4849
switch (placement) {
4850
case 'bottom':
4851
tp = tpb;
4852
break;
4853
case 'top':
4854
tp = tpt;
4855
break;
4856
case 'left':
4857
tp = tpl;
4858
break;
4859
case 'right':
4860
tp = tpr;
4861
break;
4862
}
4863
4864
$tip
4865
.offset(tp)
4866
.addClass(placement)
4867
.addClass('in');
4868
*/
4869
4870
4871
var $tip = this.tip();
4872
4873
var placement = typeof this.options.placement == 'function' ?
4874
this.options.placement.call(this, $tip[0], this.$element[0]) :
4875
this.options.placement;
4876
4877
var autoToken = /\s?auto?\s?/i;
4878
var autoPlace = autoToken.test(placement);
4879
if (autoPlace) {
4880
placement = placement.replace(autoToken, '') || 'top';
4881
}
4882
4883
4884
var pos = this.getPosition();
4885
var actualWidth = $tip[0].offsetWidth;
4886
var actualHeight = $tip[0].offsetHeight;
4887
4888
if (autoPlace) {
4889
var $parent = this.$element.parent();
4890
4891
var orgPlacement = placement;
4892
var docScroll = document.documentElement.scrollTop || document.body.scrollTop;
4893
var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth();
4894
var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight();
4895
var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left;
4896
4897
placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
4898
placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
4899
placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
4900
placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
4901
placement;
4902
4903
$tip
4904
.removeClass(orgPlacement)
4905
.addClass(placement);
4906
}
4907
4908
4909
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight);
4910
4911
this.applyPlacement(calculatedOffset, placement);
4912
4913
4914
}).call(this.container());
4915
/*jshint laxcomma: false, eqeqeq: true*/
4916
}
4917
});
4918
4919
}(window.jQuery));
4920
4921
/* =========================================================
4922
* bootstrap-datepicker.js
4923
* http://www.eyecon.ro/bootstrap-datepicker
4924
* =========================================================
4925
* Copyright 2012 Stefan Petre
4926
* Improvements by Andrew Rowls
4927
*
4928
* Licensed under the Apache License, Version 2.0 (the "License");
4929
* you may not use this file except in compliance with the License.
4930
* You may obtain a copy of the License at
4931
*
4932
* http://www.apache.org/licenses/LICENSE-2.0
4933
*
4934
* Unless required by applicable law or agreed to in writing, software
4935
* distributed under the License is distributed on an "AS IS" BASIS,
4936
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4937
* See the License for the specific language governing permissions and
4938
* limitations under the License.
4939
* ========================================================= */
4940
4941
(function( $ ) {
4942
4943
function UTCDate(){
4944
return new Date(Date.UTC.apply(Date, arguments));
4945
}
4946
function UTCToday(){
4947
var today = new Date();
4948
return UTCDate(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
4949
}
4950
4951
// Picker object
4952
4953
var Datepicker = function(element, options) {
4954
var that = this;
4955
4956
this._process_options(options);
4957
4958
this.element = $(element);
4959
this.isInline = false;
4960
this.isInput = this.element.is('input');
4961
this.component = this.element.is('.date') ? this.element.find('.add-on, .btn') : false;
4962
this.hasInput = this.component && this.element.find('input').length;
4963
if(this.component && this.component.length === 0)
4964
this.component = false;
4965
4966
this.picker = $(DPGlobal.template);
4967
this._buildEvents();
4968
this._attachEvents();
4969
4970
if(this.isInline) {
4971
this.picker.addClass('datepicker-inline').appendTo(this.element);
4972
} else {
4973
this.picker.addClass('datepicker-dropdown dropdown-menu');
4974
}
4975
4976
if (this.o.rtl){
4977
this.picker.addClass('datepicker-rtl');
4978
this.picker.find('.prev i, .next i')
4979
.toggleClass('icon-arrow-left icon-arrow-right');
4980
}
4981
4982
4983
this.viewMode = this.o.startView;
4984
4985
if (this.o.calendarWeeks)
4986
this.picker.find('tfoot th.today')
4987
.attr('colspan', function(i, val){
4988
return parseInt(val) + 1;
4989
});
4990
4991
this._allow_update = false;
4992
4993
this.setStartDate(this.o.startDate);
4994
this.setEndDate(this.o.endDate);
4995
this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled);
4996
4997
this.fillDow();
4998
this.fillMonths();
4999
5000
this._allow_update = true;
5001
5002
this.update();
5003
this.showMode();
5004
5005
if(this.isInline) {
5006
this.show();
5007
}
5008
};
5009
5010
Datepicker.prototype = {
5011
constructor: Datepicker,
5012
5013
_process_options: function(opts){
5014
// Store raw options for reference
5015
this._o = $.extend({}, this._o, opts);
5016
// Processed options
5017
var o = this.o = $.extend({}, this._o);
5018
5019
// Check if "de-DE" style date is available, if not language should
5020
// fallback to 2 letter code eg "de"
5021
var lang = o.language;
5022
if (!dates[lang]) {
5023
lang = lang.split('-')[0];
5024
if (!dates[lang])
5025
lang = defaults.language;
5026
}
5027
o.language = lang;
5028
5029
switch(o.startView){
5030
case 2:
5031
case 'decade':
5032
o.startView = 2;
5033
break;
5034
case 1:
5035
case 'year':
5036
o.startView = 1;
5037
break;
5038
default:
5039
o.startView = 0;
5040
}
5041
5042
switch (o.minViewMode) {
5043
case 1:
5044
case 'months':
5045
o.minViewMode = 1;
5046
break;
5047
case 2:
5048
case 'years':
5049
o.minViewMode = 2;
5050
break;
5051
default:
5052
o.minViewMode = 0;
5053
}
5054
5055
o.startView = Math.max(o.startView, o.minViewMode);
5056
5057
o.weekStart %= 7;
5058
o.weekEnd = ((o.weekStart + 6) % 7);
5059
5060
var format = DPGlobal.parseFormat(o.format)
5061
if (o.startDate !== -Infinity) {
5062
o.startDate = DPGlobal.parseDate(o.startDate, format, o.language);
5063
}
5064
if (o.endDate !== Infinity) {
5065
o.endDate = DPGlobal.parseDate(o.endDate, format, o.language);
5066
}
5067
5068
o.daysOfWeekDisabled = o.daysOfWeekDisabled||[];
5069
if (!$.isArray(o.daysOfWeekDisabled))
5070
o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/);
5071
o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function (d) {
5072
return parseInt(d, 10);
5073
});
5074
},
5075
_events: [],
5076
_secondaryEvents: [],
5077
_applyEvents: function(evs){
5078
for (var i=0, el, ev; i<evs.length; i++){
5079
el = evs[i][0];
5080
ev = evs[i][1];
5081
el.on(ev);
5082
}
5083
},
5084
_unapplyEvents: function(evs){
5085
for (var i=0, el, ev; i<evs.length; i++){
5086
el = evs[i][0];
5087
ev = evs[i][1];
5088
el.off(ev);
5089
}
5090
},
5091
_buildEvents: function(){
5092
if (this.isInput) { // single input
5093
this._events = [
5094
[this.element, {
5095
focus: $.proxy(this.show, this),
5096
keyup: $.proxy(this.update, this),
5097
keydown: $.proxy(this.keydown, this)
5098
}]
5099
];
5100
}
5101
else if (this.component && this.hasInput){ // component: input + button
5102
this._events = [
5103
// For components that are not readonly, allow keyboard nav
5104
[this.element.find('input'), {
5105
focus: $.proxy(this.show, this),
5106
keyup: $.proxy(this.update, this),
5107
keydown: $.proxy(this.keydown, this)
5108
}],
5109
[this.component, {
5110
click: $.proxy(this.show, this)
5111
}]
5112
];
5113
}
5114
else if (this.element.is('div')) { // inline datepicker
5115
this.isInline = true;
5116
}
5117
else {
5118
this._events = [
5119
[this.element, {
5120
click: $.proxy(this.show, this)
5121
}]
5122
];
5123
}
5124
5125
this._secondaryEvents = [
5126
[this.picker, {
5127
click: $.proxy(this.click, this)
5128
}],
5129
[$(window), {
5130
resize: $.proxy(this.place, this)
5131
}],
5132
[$(document), {
5133
mousedown: $.proxy(function (e) {
5134
// Clicked outside the datepicker, hide it
5135
if (!(
5136
this.element.is(e.target) ||
5137
this.element.find(e.target).size() ||
5138
this.picker.is(e.target) ||
5139
this.picker.find(e.target).size()
5140
)) {
5141
this.hide();
5142
}
5143
}, this)
5144
}]
5145
];
5146
},
5147
_attachEvents: function(){
5148
this._detachEvents();
5149
this._applyEvents(this._events);
5150
},
5151
_detachEvents: function(){
5152
this._unapplyEvents(this._events);
5153
},
5154
_attachSecondaryEvents: function(){
5155
this._detachSecondaryEvents();
5156
this._applyEvents(this._secondaryEvents);
5157
},
5158
_detachSecondaryEvents: function(){
5159
this._unapplyEvents(this._secondaryEvents);
5160
},
5161
_trigger: function(event, altdate){
5162
var date = altdate || this.date,
5163
local_date = new Date(date.getTime() + (date.getTimezoneOffset()*60000));
5164
5165
this.element.trigger({
5166
type: event,
5167
date: local_date,
5168
format: $.proxy(function(altformat){
5169
var format = altformat || this.o.format;
5170
return DPGlobal.formatDate(date, format, this.o.language);
5171
}, this)
5172
});
5173
},
5174
5175
show: function(e) {
5176
if (!this.isInline)
5177
this.picker.appendTo('body');
5178
this.picker.show();
5179
this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
5180
this.place();
5181
this._attachSecondaryEvents();
5182
if (e) {
5183
e.preventDefault();
5184
}
5185
this._trigger('show');
5186
},
5187
5188
hide: function(e){
5189
if(this.isInline) return;
5190
if (!this.picker.is(':visible')) return;
5191
this.picker.hide().detach();
5192
this._detachSecondaryEvents();
5193
this.viewMode = this.o.startView;
5194
this.showMode();
5195
5196
if (
5197
this.o.forceParse &&
5198
(
5199
this.isInput && this.element.val() ||
5200
this.hasInput && this.element.find('input').val()
5201
)
5202
)
5203
this.setValue();
5204
this._trigger('hide');
5205
},
5206
5207
remove: function() {
5208
this.hide();
5209
this._detachEvents();
5210
this._detachSecondaryEvents();
5211
this.picker.remove();
5212
delete this.element.data().datepicker;
5213
if (!this.isInput) {
5214
delete this.element.data().date;
5215
}
5216
},
5217
5218
getDate: function() {
5219
var d = this.getUTCDate();
5220
return new Date(d.getTime() + (d.getTimezoneOffset()*60000));
5221
},
5222
5223
getUTCDate: function() {
5224
return this.date;
5225
},
5226
5227
setDate: function(d) {
5228
this.setUTCDate(new Date(d.getTime() - (d.getTimezoneOffset()*60000)));
5229
},
5230
5231
setUTCDate: function(d) {
5232
this.date = d;
5233
this.setValue();
5234
},
5235
5236
setValue: function() {
5237
var formatted = this.getFormattedDate();
5238
if (!this.isInput) {
5239
if (this.component){
5240
this.element.find('input').val(formatted);
5241
}
5242
} else {
5243
this.element.val(formatted);
5244
}
5245
},
5246
5247
getFormattedDate: function(format) {
5248
if (format === undefined)
5249
format = this.o.format;
5250
return DPGlobal.formatDate(this.date, format, this.o.language);
5251
},
5252
5253
setStartDate: function(startDate){
5254
this._process_options({startDate: startDate});
5255
this.update();
5256
this.updateNavArrows();
5257
},
5258
5259
setEndDate: function(endDate){
5260
this._process_options({endDate: endDate});
5261
this.update();
5262
this.updateNavArrows();
5263
},
5264
5265
setDaysOfWeekDisabled: function(daysOfWeekDisabled){
5266
this._process_options({daysOfWeekDisabled: daysOfWeekDisabled});
5267
this.update();
5268
this.updateNavArrows();
5269
},
5270
5271
place: function(){
5272
if(this.isInline) return;
5273
var zIndex = parseInt(this.element.parents().filter(function() {
5274
return $(this).css('z-index') != 'auto';
5275
}).first().css('z-index'))+10;
5276
var offset = this.component ? this.component.parent().offset() : this.element.offset();
5277
var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(true);
5278
this.picker.css({
5279
top: offset.top + height,
5280
left: offset.left,
5281
zIndex: zIndex
5282
});
5283
},
5284
5285
_allow_update: true,
5286
update: function(){
5287
if (!this._allow_update) return;
5288
5289
var date, fromArgs = false;
5290
if(arguments && arguments.length && (typeof arguments[0] === 'string' || arguments[0] instanceof Date)) {
5291
date = arguments[0];
5292
fromArgs = true;
5293
} else {
5294
date = this.isInput ? this.element.val() : this.element.data('date') || this.element.find('input').val();
5295
delete this.element.data().date;
5296
}
5297
5298
this.date = DPGlobal.parseDate(date, this.o.format, this.o.language);
5299
5300
if(fromArgs) this.setValue();
5301
5302
if (this.date < this.o.startDate) {
5303
this.viewDate = new Date(this.o.startDate);
5304
} else if (this.date > this.o.endDate) {
5305
this.viewDate = new Date(this.o.endDate);
5306
} else {
5307
this.viewDate = new Date(this.date);
5308
}
5309
this.fill();
5310
},
5311
5312
fillDow: function(){
5313
var dowCnt = this.o.weekStart,
5314
html = '<tr>';
5315
if(this.o.calendarWeeks){
5316
var cell = '<th class="cw">&nbsp;</th>';
5317
html += cell;
5318
this.picker.find('.datepicker-days thead tr:first-child').prepend(cell);
5319
}
5320
while (dowCnt < this.o.weekStart + 7) {
5321
html += '<th class="dow">'+dates[this.o.language].daysMin[(dowCnt++)%7]+'</th>';
5322
}
5323
html += '</tr>';
5324
this.picker.find('.datepicker-days thead').append(html);
5325
},
5326
5327
fillMonths: function(){
5328
var html = '',
5329
i = 0;
5330
while (i < 12) {
5331
html += '<span class="month">'+dates[this.o.language].monthsShort[i++]+'</span>';
5332
}
5333
this.picker.find('.datepicker-months td').html(html);
5334
},
5335
5336
setRange: function(range){
5337
if (!range || !range.length)
5338
delete this.range;
5339
else
5340
this.range = $.map(range, function(d){ return d.valueOf(); });
5341
this.fill();
5342
},
5343
5344
getClassNames: function(date){
5345
var cls = [],
5346
year = this.viewDate.getUTCFullYear(),
5347
month = this.viewDate.getUTCMonth(),
5348
currentDate = this.date.valueOf(),
5349
today = new Date();
5350
if (date.getUTCFullYear() < year || (date.getUTCFullYear() == year && date.getUTCMonth() < month)) {
5351
cls.push('old');
5352
} else if (date.getUTCFullYear() > year || (date.getUTCFullYear() == year && date.getUTCMonth() > month)) {
5353
cls.push('new');
5354
}
5355
// Compare internal UTC date with local today, not UTC today
5356
if (this.o.todayHighlight &&
5357
date.getUTCFullYear() == today.getFullYear() &&
5358
date.getUTCMonth() == today.getMonth() &&
5359
date.getUTCDate() == today.getDate()) {
5360
cls.push('today');
5361
}
5362
if (currentDate && date.valueOf() == currentDate) {
5363
cls.push('active');
5364
}
5365
if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate ||
5366
$.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1) {
5367
cls.push('disabled');
5368
}
5369
if (this.range){
5370
if (date > this.range[0] && date < this.range[this.range.length-1]){
5371
cls.push('range');
5372
}
5373
if ($.inArray(date.valueOf(), this.range) != -1){
5374
cls.push('selected');
5375
}
5376
}
5377
return cls;
5378
},
5379
5380
fill: function() {
5381
var d = new Date(this.viewDate),
5382
year = d.getUTCFullYear(),
5383
month = d.getUTCMonth(),
5384
startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity,
5385
startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity,
5386
endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity,
5387
endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity,
5388
currentDate = this.date && this.date.valueOf(),
5389
tooltip;
5390
this.picker.find('.datepicker-days thead th.datepicker-switch')
5391
.text(dates[this.o.language].months[month]+' '+year);
5392
this.picker.find('tfoot th.today')
5393
.text(dates[this.o.language].today)
5394
.toggle(this.o.todayBtn !== false);
5395
this.picker.find('tfoot th.clear')
5396
.text(dates[this.o.language].clear)
5397
.toggle(this.o.clearBtn !== false);
5398
this.updateNavArrows();
5399
this.fillMonths();
5400
var prevMonth = UTCDate(year, month-1, 28,0,0,0,0),
5401
day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
5402
prevMonth.setUTCDate(day);
5403
prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7);
5404
var nextMonth = new Date(prevMonth);
5405
nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
5406
nextMonth = nextMonth.valueOf();
5407
var html = [];
5408
var clsName;
5409
while(prevMonth.valueOf() < nextMonth) {
5410
if (prevMonth.getUTCDay() == this.o.weekStart) {
5411
html.push('<tr>');
5412
if(this.o.calendarWeeks){
5413
// ISO 8601: First week contains first thursday.
5414
// ISO also states week starts on Monday, but we can be more abstract here.
5415
var
5416
// Start of current week: based on weekstart/current date
5417
ws = new Date(+prevMonth + (this.o.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5),
5418
// Thursday of this week
5419
th = new Date(+ws + (7 + 4 - ws.getUTCDay()) % 7 * 864e5),
5420
// First Thursday of year, year from thursday
5421
yth = new Date(+(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5),
5422
// Calendar week: ms between thursdays, div ms per day, div 7 days
5423
calWeek = (th - yth) / 864e5 / 7 + 1;
5424
html.push('<td class="cw">'+ calWeek +'</td>');
5425
5426
}
5427
}
5428
clsName = this.getClassNames(prevMonth);
5429
clsName.push('day');
5430
5431
var before = this.o.beforeShowDay(prevMonth);
5432
if (before === undefined)
5433
before = {};
5434
else if (typeof(before) === 'boolean')
5435
before = {enabled: before};
5436
else if (typeof(before) === 'string')
5437
before = {classes: before};
5438
if (before.enabled === false)
5439
clsName.push('disabled');
5440
if (before.classes)
5441
clsName = clsName.concat(before.classes.split(/\s+/));
5442
if (before.tooltip)
5443
tooltip = before.tooltip;
5444
5445
clsName = $.unique(clsName);
5446
html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>');
5447
if (prevMonth.getUTCDay() == this.o.weekEnd) {
5448
html.push('</tr>');
5449
}
5450
prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
5451
}
5452
this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
5453
var currentYear = this.date && this.date.getUTCFullYear();
5454
5455
var months = this.picker.find('.datepicker-months')
5456
.find('th:eq(1)')
5457
.text(year)
5458
.end()
5459
.find('span').removeClass('active');
5460
if (currentYear && currentYear == year) {
5461
months.eq(this.date.getUTCMonth()).addClass('active');
5462
}
5463
if (year < startYear || year > endYear) {
5464
months.addClass('disabled');
5465
}
5466
if (year == startYear) {
5467
months.slice(0, startMonth).addClass('disabled');
5468
}
5469
if (year == endYear) {
5470
months.slice(endMonth+1).addClass('disabled');
5471
}
5472
5473
html = '';
5474
year = parseInt(year/10, 10) * 10;
5475
var yearCont = this.picker.find('.datepicker-years')
5476
.find('th:eq(1)')
5477
.text(year + '-' + (year + 9))
5478
.end()
5479
.find('td');
5480
year -= 1;
5481
for (var i = -1; i < 11; i++) {
5482
html += '<span class="year'+(i == -1 ? ' old' : i == 10 ? ' new' : '')+(currentYear == year ? ' active' : '')+(year < startYear || year > endYear ? ' disabled' : '')+'">'+year+'</span>';
5483
year += 1;
5484
}
5485
yearCont.html(html);
5486
},
5487
5488
updateNavArrows: function() {
5489
if (!this._allow_update) return;
5490
5491
var d = new Date(this.viewDate),
5492
year = d.getUTCFullYear(),
5493
month = d.getUTCMonth();
5494
switch (this.viewMode) {
5495
case 0:
5496
if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()) {
5497
this.picker.find('.prev').css({visibility: 'hidden'});
5498
} else {
5499
this.picker.find('.prev').css({visibility: 'visible'});
5500
}
5501
if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()) {
5502
this.picker.find('.next').css({visibility: 'hidden'});
5503
} else {
5504
this.picker.find('.next').css({visibility: 'visible'});
5505
}
5506
break;
5507
case 1:
5508
case 2:
5509
if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()) {
5510
this.picker.find('.prev').css({visibility: 'hidden'});
5511
} else {
5512
this.picker.find('.prev').css({visibility: 'visible'});
5513
}
5514
if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()) {
5515
this.picker.find('.next').css({visibility: 'hidden'});
5516
} else {
5517
this.picker.find('.next').css({visibility: 'visible'});
5518
}
5519
break;
5520
}
5521
},
5522
5523
click: function(e) {
5524
e.preventDefault();
5525
var target = $(e.target).closest('span, td, th');
5526
if (target.length == 1) {
5527
switch(target[0].nodeName.toLowerCase()) {
5528
case 'th':
5529
switch(target[0].className) {
5530
case 'datepicker-switch':
5531
this.showMode(1);
5532
break;
5533
case 'prev':
5534
case 'next':
5535
var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className == 'prev' ? -1 : 1);
5536
switch(this.viewMode){
5537
case 0:
5538
this.viewDate = this.moveMonth(this.viewDate, dir);
5539
break;
5540
case 1:
5541
case 2:
5542
this.viewDate = this.moveYear(this.viewDate, dir);
5543
break;
5544
}
5545
this.fill();
5546
break;
5547
case 'today':
5548
var date = new Date();
5549
date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
5550
5551
this.showMode(-2);
5552
var which = this.o.todayBtn == 'linked' ? null : 'view';
5553
this._setDate(date, which);
5554
break;
5555
case 'clear':
5556
var element;
5557
if (this.isInput)
5558
element = this.element;
5559
else if (this.component)
5560
element = this.element.find('input');
5561
if (element)
5562
element.val("").change();
5563
this._trigger('changeDate');
5564
this.update();
5565
if (this.o.autoclose)
5566
this.hide();
5567
break;
5568
}
5569
break;
5570
case 'span':
5571
if (!target.is('.disabled')) {
5572
this.viewDate.setUTCDate(1);
5573
if (target.is('.month')) {
5574
var day = 1;
5575
var month = target.parent().find('span').index(target);
5576
var year = this.viewDate.getUTCFullYear();
5577
this.viewDate.setUTCMonth(month);
5578
this._trigger('changeMonth', this.viewDate);
5579
if (this.o.minViewMode === 1) {
5580
this._setDate(UTCDate(year, month, day,0,0,0,0));
5581
}
5582
} else {
5583
var year = parseInt(target.text(), 10)||0;
5584
var day = 1;
5585
var month = 0;
5586
this.viewDate.setUTCFullYear(year);
5587
this._trigger('changeYear', this.viewDate);
5588
if (this.o.minViewMode === 2) {
5589
this._setDate(UTCDate(year, month, day,0,0,0,0));
5590
}
5591
}
5592
this.showMode(-1);
5593
this.fill();
5594
}
5595
break;
5596
case 'td':
5597
if (target.is('.day') && !target.is('.disabled')){
5598
var day = parseInt(target.text(), 10)||1;
5599
var year = this.viewDate.getUTCFullYear(),
5600
month = this.viewDate.getUTCMonth();
5601
if (target.is('.old')) {
5602
if (month === 0) {
5603
month = 11;
5604
year -= 1;
5605
} else {
5606
month -= 1;
5607
}
5608
} else if (target.is('.new')) {
5609
if (month == 11) {
5610
month = 0;
5611
year += 1;
5612
} else {
5613
month += 1;
5614
}
5615
}
5616
this._setDate(UTCDate(year, month, day,0,0,0,0));
5617
}
5618
break;
5619
}
5620
}
5621
},
5622
5623
_setDate: function(date, which){
5624
if (!which || which == 'date')
5625
this.date = new Date(date);
5626
if (!which || which == 'view')
5627
this.viewDate = new Date(date);
5628
this.fill();
5629
this.setValue();
5630
this._trigger('changeDate');
5631
var element;
5632
if (this.isInput) {
5633
element = this.element;
5634
} else if (this.component){
5635
element = this.element.find('input');
5636
}
5637
if (element) {
5638
element.change();
5639
if (this.o.autoclose && (!which || which == 'date')) {
5640
this.hide();
5641
}
5642
}
5643
},
5644
5645
moveMonth: function(date, dir){
5646
if (!dir) return date;
5647
var new_date = new Date(date.valueOf()),
5648
day = new_date.getUTCDate(),
5649
month = new_date.getUTCMonth(),
5650
mag = Math.abs(dir),
5651
new_month, test;
5652
dir = dir > 0 ? 1 : -1;
5653
if (mag == 1){
5654
test = dir == -1
5655
// If going back one month, make sure month is not current month
5656
// (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
5657
? function(){ return new_date.getUTCMonth() == month; }
5658
// If going forward one month, make sure month is as expected
5659
// (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
5660
: function(){ return new_date.getUTCMonth() != new_month; };
5661
new_month = month + dir;
5662
new_date.setUTCMonth(new_month);
5663
// Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
5664
if (new_month < 0 || new_month > 11)
5665
new_month = (new_month + 12) % 12;
5666
} else {
5667
// For magnitudes >1, move one month at a time...
5668
for (var i=0; i<mag; i++)
5669
// ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
5670
new_date = this.moveMonth(new_date, dir);
5671
// ...then reset the day, keeping it in the new month
5672
new_month = new_date.getUTCMonth();
5673
new_date.setUTCDate(day);
5674
test = function(){ return new_month != new_date.getUTCMonth(); };
5675
}
5676
// Common date-resetting loop -- if date is beyond end of month, make it
5677
// end of month
5678
while (test()){
5679
new_date.setUTCDate(--day);
5680
new_date.setUTCMonth(new_month);
5681
}
5682
return new_date;
5683
},
5684
5685
moveYear: function(date, dir){
5686
return this.moveMonth(date, dir*12);
5687
},
5688
5689
dateWithinRange: function(date){
5690
return date >= this.o.startDate && date <= this.o.endDate;
5691
},
5692
5693
keydown: function(e){
5694
if (this.picker.is(':not(:visible)')){
5695
if (e.keyCode == 27) // allow escape to hide and re-show picker
5696
this.show();
5697
return;
5698
}
5699
var dateChanged = false,
5700
dir, day, month,
5701
newDate, newViewDate;
5702
switch(e.keyCode){
5703
case 27: // escape
5704
this.hide();
5705
e.preventDefault();
5706
break;
5707
case 37: // left
5708
case 39: // right
5709
if (!this.o.keyboardNavigation) break;
5710
dir = e.keyCode == 37 ? -1 : 1;
5711
if (e.ctrlKey){
5712
newDate = this.moveYear(this.date, dir);
5713
newViewDate = this.moveYear(this.viewDate, dir);
5714
} else if (e.shiftKey){
5715
newDate = this.moveMonth(this.date, dir);
5716
newViewDate = this.moveMonth(this.viewDate, dir);
5717
} else {
5718
newDate = new Date(this.date);
5719
newDate.setUTCDate(this.date.getUTCDate() + dir);
5720
newViewDate = new Date(this.viewDate);
5721
newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir);
5722
}
5723
if (this.dateWithinRange(newDate)){
5724
this.date = newDate;
5725
this.viewDate = newViewDate;
5726
this.setValue();
5727
this.update();
5728
e.preventDefault();
5729
dateChanged = true;
5730
}
5731
break;
5732
case 38: // up
5733
case 40: // down
5734
if (!this.o.keyboardNavigation) break;
5735
dir = e.keyCode == 38 ? -1 : 1;
5736
if (e.ctrlKey){
5737
newDate = this.moveYear(this.date, dir);
5738
newViewDate = this.moveYear(this.viewDate, dir);
5739
} else if (e.shiftKey){
5740
newDate = this.moveMonth(this.date, dir);
5741
newViewDate = this.moveMonth(this.viewDate, dir);
5742
} else {
5743
newDate = new Date(this.date);
5744
newDate.setUTCDate(this.date.getUTCDate() + dir * 7);
5745
newViewDate = new Date(this.viewDate);
5746
newViewDate.setUTCDate(this.viewDate.getUTCDate() + dir * 7);
5747
}
5748
if (this.dateWithinRange(newDate)){
5749
this.date = newDate;
5750
this.viewDate = newViewDate;
5751
this.setValue();
5752
this.update();
5753
e.preventDefault();
5754
dateChanged = true;
5755
}
5756
break;
5757
case 13: // enter
5758
this.hide();
5759
e.preventDefault();
5760
break;
5761
case 9: // tab
5762
this.hide();
5763
break;
5764
}
5765
if (dateChanged){
5766
this._trigger('changeDate');
5767
var element;
5768
if (this.isInput) {
5769
element = this.element;
5770
} else if (this.component){
5771
element = this.element.find('input');
5772
}
5773
if (element) {
5774
element.change();
5775
}
5776
}
5777
},
5778
5779
showMode: function(dir) {
5780
if (dir) {
5781
this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir));
5782
}
5783
/*
5784
vitalets: fixing bug of very special conditions:
5785
jquery 1.7.1 + webkit + show inline datepicker in bootstrap popover.
5786
Method show() does not set display css correctly and datepicker is not shown.
5787
Changed to .css('display', 'block') solve the problem.
5788
See https://github.com/vitalets/x-editable/issues/37
5789
5790
In jquery 1.7.2+ everything works fine.
5791
*/
5792
//this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
5793
this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).css('display', 'block');
5794
this.updateNavArrows();
5795
}
5796
};
5797
5798
var DateRangePicker = function(element, options){
5799
this.element = $(element);
5800
this.inputs = $.map(options.inputs, function(i){ return i.jquery ? i[0] : i; });
5801
delete options.inputs;
5802
5803
$(this.inputs)
5804
.datepicker(options)
5805
.bind('changeDate', $.proxy(this.dateUpdated, this));
5806
5807
this.pickers = $.map(this.inputs, function(i){ return $(i).data('datepicker'); });
5808
this.updateDates();
5809
};
5810
DateRangePicker.prototype = {
5811
updateDates: function(){
5812
this.dates = $.map(this.pickers, function(i){ return i.date; });
5813
this.updateRanges();
5814
},
5815
updateRanges: function(){
5816
var range = $.map(this.dates, function(d){ return d.valueOf(); });
5817
$.each(this.pickers, function(i, p){
5818
p.setRange(range);
5819
});
5820
},
5821
dateUpdated: function(e){
5822
var dp = $(e.target).data('datepicker'),
5823
new_date = dp.getUTCDate(),
5824
i = $.inArray(e.target, this.inputs),
5825
l = this.inputs.length;
5826
if (i == -1) return;
5827
5828
if (new_date < this.dates[i]){
5829
// Date being moved earlier/left
5830
while (i>=0 && new_date < this.dates[i]){
5831
this.pickers[i--].setUTCDate(new_date);
5832
}
5833
}
5834
else if (new_date > this.dates[i]){
5835
// Date being moved later/right
5836
while (i<l && new_date > this.dates[i]){
5837
this.pickers[i++].setUTCDate(new_date);
5838
}
5839
}
5840
this.updateDates();
5841
},
5842
remove: function(){
5843
$.map(this.pickers, function(p){ p.remove(); });
5844
delete this.element.data().datepicker;
5845
}
5846
};
5847
5848
function opts_from_el(el, prefix){
5849
// Derive options from element data-attrs
5850
var data = $(el).data(),
5851
out = {}, inkey,
5852
replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])'),
5853
prefix = new RegExp('^' + prefix.toLowerCase());
5854
for (var key in data)
5855
if (prefix.test(key)){
5856
inkey = key.replace(replace, function(_,a){ return a.toLowerCase(); });
5857
out[inkey] = data[key];
5858
}
5859
return out;
5860
}
5861
5862
function opts_from_locale(lang){
5863
// Derive options from locale plugins
5864
var out = {};
5865
// Check if "de-DE" style date is available, if not language should
5866
// fallback to 2 letter code eg "de"
5867
if (!dates[lang]) {
5868
lang = lang.split('-')[0]
5869
if (!dates[lang])
5870
return;
5871
}
5872
var d = dates[lang];
5873
$.each(locale_opts, function(i,k){
5874
if (k in d)
5875
out[k] = d[k];
5876
});
5877
return out;
5878
}
5879
5880
var old = $.fn.datepicker;
5881
var datepicker = $.fn.datepicker = function ( option ) {
5882
var args = Array.apply(null, arguments);
5883
args.shift();
5884
var internal_return,
5885
this_return;
5886
this.each(function () {
5887
var $this = $(this),
5888
data = $this.data('datepicker'),
5889
options = typeof option == 'object' && option;
5890
if (!data) {
5891
var elopts = opts_from_el(this, 'date'),
5892
// Preliminary otions
5893
xopts = $.extend({}, defaults, elopts, options),
5894
locopts = opts_from_locale(xopts.language),
5895
// Options priority: js args, data-attrs, locales, defaults
5896
opts = $.extend({}, defaults, locopts, elopts, options);
5897
if ($this.is('.input-daterange') || opts.inputs){
5898
var ropts = {
5899
inputs: opts.inputs || $this.find('input').toArray()
5900
};
5901
$this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts))));
5902
}
5903
else{
5904
$this.data('datepicker', (data = new Datepicker(this, opts)));
5905
}
5906
}
5907
if (typeof option == 'string' && typeof data[option] == 'function') {
5908
internal_return = data[option].apply(data, args);
5909
if (internal_return !== undefined)
5910
return false;
5911
}
5912
});
5913
if (internal_return !== undefined)
5914
return internal_return;
5915
else
5916
return this;
5917
};
5918
5919
var defaults = $.fn.datepicker.defaults = {
5920
autoclose: false,
5921
beforeShowDay: $.noop,
5922
calendarWeeks: false,
5923
clearBtn: false,
5924
daysOfWeekDisabled: [],
5925
endDate: Infinity,
5926
forceParse: true,
5927
format: 'mm/dd/yyyy',
5928
keyboardNavigation: true,
5929
language: 'en',
5930
minViewMode: 0,
5931
rtl: false,
5932
startDate: -Infinity,
5933
startView: 0,
5934
todayBtn: false,
5935
todayHighlight: false,
5936
weekStart: 0
5937
};
5938
var locale_opts = $.fn.datepicker.locale_opts = [
5939
'format',
5940
'rtl',
5941
'weekStart'
5942
];
5943
$.fn.datepicker.Constructor = Datepicker;
5944
var dates = $.fn.datepicker.dates = {
5945
en: {
5946
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
5947
daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
5948
daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
5949
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
5950
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
5951
today: "Today",
5952
clear: "Clear"
5953
}
5954
};
5955
5956
var DPGlobal = {
5957
modes: [
5958
{
5959
clsName: 'days',
5960
navFnc: 'Month',
5961
navStep: 1
5962
},
5963
{
5964
clsName: 'months',
5965
navFnc: 'FullYear',
5966
navStep: 1
5967
},
5968
{
5969
clsName: 'years',
5970
navFnc: 'FullYear',
5971
navStep: 10
5972
}],
5973
isLeapYear: function (year) {
5974
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
5975
},
5976
getDaysInMonth: function (year, month) {
5977
return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
5978
},
5979
validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
5980
nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
5981
parseFormat: function(format){
5982
// IE treats \0 as a string end in inputs (truncating the value),
5983
// so it's a bad format delimiter, anyway
5984
var separators = format.replace(this.validParts, '\0').split('\0'),
5985
parts = format.match(this.validParts);
5986
if (!separators || !separators.length || !parts || parts.length === 0){
5987
throw new Error("Invalid date format.");
5988
}
5989
return {separators: separators, parts: parts};
5990
},
5991
parseDate: function(date, format, language) {
5992
if (date instanceof Date) return date;
5993
if (typeof format === 'string')
5994
format = DPGlobal.parseFormat(format);
5995
if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)) {
5996
var part_re = /([\-+]\d+)([dmwy])/,
5997
parts = date.match(/([\-+]\d+)([dmwy])/g),
5998
part, dir;
5999
date = new Date();
6000
for (var i=0; i<parts.length; i++) {
6001
part = part_re.exec(parts[i]);
6002
dir = parseInt(part[1]);
6003
switch(part[2]){
6004
case 'd':
6005
date.setUTCDate(date.getUTCDate() + dir);
6006
break;
6007
case 'm':
6008
date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
6009
break;
6010
case 'w':
6011
date.setUTCDate(date.getUTCDate() + dir * 7);
6012
break;
6013
case 'y':
6014
date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
6015
break;
6016
}
6017
}
6018
return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
6019
}
6020
var parts = date && date.match(this.nonpunctuation) || [],
6021
date = new Date(),
6022
parsed = {},
6023
setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
6024
setters_map = {
6025
yyyy: function(d,v){ return d.setUTCFullYear(v); },
6026
yy: function(d,v){ return d.setUTCFullYear(2000+v); },
6027
m: function(d,v){
6028
v -= 1;
6029
while (v<0) v += 12;
6030
v %= 12;
6031
d.setUTCMonth(v);
6032
while (d.getUTCMonth() != v)
6033
d.setUTCDate(d.getUTCDate()-1);
6034
return d;
6035
},
6036
d: function(d,v){ return d.setUTCDate(v); }
6037
},
6038
val, filtered, part;
6039
setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
6040
setters_map['dd'] = setters_map['d'];
6041
date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
6042
var fparts = format.parts.slice();
6043
// Remove noop parts
6044
if (parts.length != fparts.length) {
6045
fparts = $(fparts).filter(function(i,p){
6046
return $.inArray(p, setters_order) !== -1;
6047
}).toArray();
6048
}
6049
// Process remainder
6050
if (parts.length == fparts.length) {
6051
for (var i=0, cnt = fparts.length; i < cnt; i++) {
6052
val = parseInt(parts[i], 10);
6053
part = fparts[i];
6054
if (isNaN(val)) {
6055
switch(part) {
6056
case 'MM':
6057
filtered = $(dates[language].months).filter(function(){
6058
var m = this.slice(0, parts[i].length),
6059
p = parts[i].slice(0, m.length);
6060
return m == p;
6061
});
6062
val = $.inArray(filtered[0], dates[language].months) + 1;
6063
break;
6064
case 'M':
6065
filtered = $(dates[language].monthsShort).filter(function(){
6066
var m = this.slice(0, parts[i].length),
6067
p = parts[i].slice(0, m.length);
6068
return m == p;
6069
});
6070
val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
6071
break;
6072
}
6073
}
6074
parsed[part] = val;
6075
}
6076
for (var i=0, s; i<setters_order.length; i++){
6077
s = setters_order[i];
6078
if (s in parsed && !isNaN(parsed[s]))
6079
setters_map[s](date, parsed[s]);
6080
}
6081
}
6082
return date;
6083
},
6084
formatDate: function(date, format, language){
6085
if (typeof format === 'string')
6086
format = DPGlobal.parseFormat(format);
6087
var val = {
6088
d: date.getUTCDate(),
6089
D: dates[language].daysShort[date.getUTCDay()],
6090
DD: dates[language].days[date.getUTCDay()],
6091
m: date.getUTCMonth() + 1,
6092
M: dates[language].monthsShort[date.getUTCMonth()],
6093
MM: dates[language].months[date.getUTCMonth()],
6094
yy: date.getUTCFullYear().toString().substring(2),
6095
yyyy: date.getUTCFullYear()
6096
};
6097
val.dd = (val.d < 10 ? '0' : '') + val.d;
6098
val.mm = (val.m < 10 ? '0' : '') + val.m;
6099
var date = [],
6100
seps = $.extend([], format.separators);
6101
for (var i=0, cnt = format.parts.length; i <= cnt; i++) {
6102
if (seps.length)
6103
date.push(seps.shift());
6104
date.push(val[format.parts[i]]);
6105
}
6106
return date.join('');
6107
},
6108
headTemplate: '<thead>'+
6109
'<tr>'+
6110
'<th class="prev"><i class="icon-arrow-left"/></th>'+
6111
'<th colspan="5" class="datepicker-switch"></th>'+
6112
'<th class="next"><i class="icon-arrow-right"/></th>'+
6113
'</tr>'+
6114
'</thead>',
6115
contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
6116
footTemplate: '<tfoot><tr><th colspan="7" class="today"></th></tr><tr><th colspan="7" class="clear"></th></tr></tfoot>'
6117
};
6118
DPGlobal.template = '<div class="datepicker">'+
6119
'<div class="datepicker-days">'+
6120
'<table class=" table-condensed">'+
6121
DPGlobal.headTemplate+
6122
'<tbody></tbody>'+
6123
DPGlobal.footTemplate+
6124
'</table>'+
6125
'</div>'+
6126
'<div class="datepicker-months">'+
6127
'<table class="table-condensed">'+
6128
DPGlobal.headTemplate+
6129
DPGlobal.contTemplate+
6130
DPGlobal.footTemplate+
6131
'</table>'+
6132
'</div>'+
6133
'<div class="datepicker-years">'+
6134
'<table class="table-condensed">'+
6135
DPGlobal.headTemplate+
6136
DPGlobal.contTemplate+
6137
DPGlobal.footTemplate+
6138
'</table>'+
6139
'</div>'+
6140
'</div>';
6141
6142
$.fn.datepicker.DPGlobal = DPGlobal;
6143
6144
6145
/* DATEPICKER NO CONFLICT
6146
* =================== */
6147
6148
$.fn.datepicker.noConflict = function(){
6149
$.fn.datepicker = old;
6150
return this;
6151
};
6152
6153
6154
/* DATEPICKER DATA-API
6155
* ================== */
6156
6157
$(document).on(
6158
'focus.datepicker.data-api click.datepicker.data-api',
6159
'[data-provide="datepicker"]',
6160
function(e){
6161
var $this = $(this);
6162
if ($this.data('datepicker')) return;
6163
e.preventDefault();
6164
// component click requires us to explicitly show it
6165
datepicker.call($this, 'show');
6166
}
6167
);
6168
$(function(){
6169
//$('[data-provide="datepicker-inline"]').datepicker();
6170
//vit: changed to support noConflict()
6171
datepicker.call($('[data-provide="datepicker-inline"]'));
6172
});
6173
6174
}( window.jQuery ));
6175
6176
/**
6177
Bootstrap-datepicker.
6178
Description and examples: https://github.com/eternicode/bootstrap-datepicker.
6179
For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
6180
and set `language` option.
6181
Since 1.4.0 date has different appearance in **popup** and **inline** modes.
6182
6183
@class date
6184
@extends abstractinput
6185
@final
6186
@example
6187
<a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-title="Select date">15/05/1984</a>
6188
<script>
6189
$(function(){
6190
$('#dob').editable({
6191
format: 'yyyy-mm-dd',
6192
viewformat: 'dd/mm/yyyy',
6193
datepicker: {
6194
weekStart: 1
6195
}
6196
}
6197
});
6198
});
6199
</script>
6200
**/
6201
(function ($) {
6202
"use strict";
6203
6204
//store bootstrap-datepicker as bdateicker to exclude conflict with jQuery UI one
6205
$.fn.bdatepicker = $.fn.datepicker.noConflict();
6206
if(!$.fn.datepicker) { //if there were no other datepickers, keep also original name
6207
$.fn.datepicker = $.fn.bdatepicker;
6208
}
6209
6210
var Date = function (options) {
6211
this.init('date', options, Date.defaults);
6212
this.initPicker(options, Date.defaults);
6213
};
6214
6215
$.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput);
6216
6217
$.extend(Date.prototype, {
6218
initPicker: function(options, defaults) {
6219
//'format' is set directly from settings or data-* attributes
6220
6221
//by default viewformat equals to format
6222
if(!this.options.viewformat) {
6223
this.options.viewformat = this.options.format;
6224
}
6225
6226
//try parse datepicker config defined as json string in data-datepicker
6227
options.datepicker = $.fn.editableutils.tryParseJson(options.datepicker, true);
6228
6229
//overriding datepicker config (as by default jQuery extend() is not recursive)
6230
//since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only
6231
this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, {
6232
format: this.options.viewformat
6233
});
6234
6235
//language
6236
this.options.datepicker.language = this.options.datepicker.language || 'en';
6237
6238
//store DPglobal
6239
this.dpg = $.fn.bdatepicker.DPGlobal;
6240
6241
//store parsed formats
6242
this.parsedFormat = this.dpg.parseFormat(this.options.format);
6243
this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
6244
},
6245
6246
render: function () {
6247
this.$input.bdatepicker(this.options.datepicker);
6248
6249
//"clear" link
6250
if(this.options.clear) {
6251
this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
6252
e.preventDefault();
6253
e.stopPropagation();
6254
this.clear();
6255
}, this));
6256
6257
this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
6258
}
6259
},
6260
6261
value2html: function(value, element) {
6262
var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
6263
Date.superclass.value2html.call(this, text, element);
6264
},
6265
6266
html2value: function(html) {
6267
return this.parseDate(html, this.parsedViewFormat);
6268
},
6269
6270
value2str: function(value) {
6271
return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : '';
6272
},
6273
6274
str2value: function(str) {
6275
return this.parseDate(str, this.parsedFormat);
6276
},
6277
6278
value2submit: function(value) {
6279
return this.value2str(value);
6280
},
6281
6282
value2input: function(value) {
6283
this.$input.bdatepicker('update', value);
6284
},
6285
6286
input2value: function() {
6287
return this.$input.data('datepicker').date;
6288
},
6289
6290
activate: function() {
6291
},
6292
6293
clear: function() {
6294
this.$input.data('datepicker').date = null;
6295
this.$input.find('.active').removeClass('active');
6296
if(!this.options.showbuttons) {
6297
this.$input.closest('form').submit();
6298
}
6299
},
6300
6301
autosubmit: function() {
6302
this.$input.on('mouseup', '.day', function(e){
6303
if($(e.currentTarget).is('.old') || $(e.currentTarget).is('.new')) {
6304
return;
6305
}
6306
var $form = $(this).closest('form');
6307
setTimeout(function() {
6308
$form.submit();
6309
}, 200);
6310
});
6311
//changedate is not suitable as it triggered when showing datepicker. see #149
6312
/*
6313
this.$input.on('changeDate', function(e){
6314
var $form = $(this).closest('form');
6315
setTimeout(function() {
6316
$form.submit();
6317
}, 200);
6318
});
6319
*/
6320
},
6321
6322
/*
6323
For incorrect date bootstrap-datepicker returns current date that is not suitable
6324
for datefield.
6325
This function returns null for incorrect date.
6326
*/
6327
parseDate: function(str, format) {
6328
var date = null, formattedBack;
6329
if(str) {
6330
date = this.dpg.parseDate(str, format, this.options.datepicker.language);
6331
if(typeof str === 'string') {
6332
formattedBack = this.dpg.formatDate(date, format, this.options.datepicker.language);
6333
if(str !== formattedBack) {
6334
date = null;
6335
}
6336
}
6337
}
6338
return date;
6339
}
6340
6341
});
6342
6343
Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
6344
/**
6345
@property tpl
6346
@default <div></div>
6347
**/
6348
tpl:'<div class="editable-date well"></div>',
6349
/**
6350
@property inputclass
6351
@default null
6352
**/
6353
inputclass: null,
6354
/**
6355
Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
6356
Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code>
6357
6358
@property format
6359
@type string
6360
@default yyyy-mm-dd
6361
**/
6362
format:'yyyy-mm-dd',
6363
/**
6364
Format used for displaying date. Also applied when converting date from element's text on init.
6365
If not specified equals to <code>format</code>
6366
6367
@property viewformat
6368
@type string
6369
@default null
6370
**/
6371
viewformat: null,
6372
/**
6373
Configuration of datepicker.
6374
Full list of options: http://bootstrap-datepicker.readthedocs.org/en/latest/options.html
6375
6376
@property datepicker
6377
@type object
6378
@default {
6379
weekStart: 0,
6380
startView: 0,
6381
minViewMode: 0,
6382
autoclose: false
6383
}
6384
**/
6385
datepicker:{
6386
weekStart: 0,
6387
startView: 0,
6388
minViewMode: 0,
6389
autoclose: false
6390
},
6391
/**
6392
Text shown as clear date button.
6393
If <code>false</code> clear button will not be rendered.
6394
6395
@property clear
6396
@type boolean|string
6397
@default 'x clear'
6398
**/
6399
clear: '&times; clear'
6400
});
6401
6402
$.fn.editabletypes.date = Date;
6403
6404
}(window.jQuery));
6405
6406
/**
6407
Bootstrap datefield input - modification for inline mode.
6408
Shows normal <input type="text"> and binds popup datepicker.
6409
Automatically shown in inline mode.
6410
6411
@class datefield
6412
@extends date
6413
6414
@since 1.4.0
6415
**/
6416
(function ($) {
6417
"use strict";
6418
6419
var DateField = function (options) {
6420
this.init('datefield', options, DateField.defaults);
6421
this.initPicker(options, DateField.defaults);
6422
};
6423
6424
$.fn.editableutils.inherit(DateField, $.fn.editabletypes.date);
6425
6426
$.extend(DateField.prototype, {
6427
render: function () {
6428
this.$input = this.$tpl.find('input');
6429
this.setClass();
6430
this.setAttr('placeholder');
6431
6432
//bootstrap-datepicker is set `bdateicker` to exclude conflict with jQuery UI one. (in date.js)
6433
this.$tpl.bdatepicker(this.options.datepicker);
6434
6435
//need to disable original event handlers
6436
this.$input.off('focus keydown');
6437
6438
//update value of datepicker
6439
this.$input.keyup($.proxy(function(){
6440
this.$tpl.removeData('date');
6441
this.$tpl.bdatepicker('update');
6442
}, this));
6443
6444
},
6445
6446
value2input: function(value) {
6447
this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '');
6448
this.$tpl.bdatepicker('update');
6449
},
6450
6451
input2value: function() {
6452
return this.html2value(this.$input.val());
6453
},
6454
6455
activate: function() {
6456
$.fn.editabletypes.text.prototype.activate.call(this);
6457
},
6458
6459
autosubmit: function() {
6460
//reset autosubmit to empty
6461
}
6462
});
6463
6464
DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, {
6465
/**
6466
@property tpl
6467
**/
6468
tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',
6469
/**
6470
@property inputclass
6471
@default 'input-small'
6472
**/
6473
inputclass: 'input-small',
6474
6475
/* datepicker config */
6476
datepicker: {
6477
weekStart: 0,
6478
startView: 0,
6479
minViewMode: 0,
6480
autoclose: true
6481
}
6482
});
6483
6484
$.fn.editabletypes.datefield = DateField;
6485
6486
}(window.jQuery));
6487
/**
6488
Bootstrap-datetimepicker.
6489
Based on [smalot bootstrap-datetimepicker plugin](https://github.com/smalot/bootstrap-datetimepicker).
6490
Before usage you should manually include dependent js and css:
6491
6492
<link href="css/datetimepicker.css" rel="stylesheet" type="text/css"></link>
6493
<script src="js/bootstrap-datetimepicker.js"></script>
6494
6495
For **i18n** you should include js file from here: https://github.com/smalot/bootstrap-datetimepicker/tree/master/js/locales
6496
and set `language` option.
6497
6498
@class datetime
6499
@extends abstractinput
6500
@final
6501
@since 1.4.4
6502
@example
6503
<a href="#" id="last_seen" data-type="datetime" data-pk="1" data-url="/post" title="Select date & time">15/03/2013 12:45</a>
6504
<script>
6505
$(function(){
6506
$('#last_seen').editable({
6507
format: 'yyyy-mm-dd hh:ii',
6508
viewformat: 'dd/mm/yyyy hh:ii',
6509
datetimepicker: {
6510
weekStart: 1
6511
}
6512
}
6513
});
6514
});
6515
</script>
6516
**/
6517
(function ($) {
6518
"use strict";
6519
6520
var DateTime = function (options) {
6521
this.init('datetime', options, DateTime.defaults);
6522
this.initPicker(options, DateTime.defaults);
6523
};
6524
6525
$.fn.editableutils.inherit(DateTime, $.fn.editabletypes.abstractinput);
6526
6527
$.extend(DateTime.prototype, {
6528
initPicker: function(options, defaults) {
6529
//'format' is set directly from settings or data-* attributes
6530
6531
//by default viewformat equals to format
6532
if(!this.options.viewformat) {
6533
this.options.viewformat = this.options.format;
6534
}
6535
6536
//try parse datetimepicker config defined as json string in data-datetimepicker
6537
options.datetimepicker = $.fn.editableutils.tryParseJson(options.datetimepicker, true);
6538
6539
//overriding datetimepicker config (as by default jQuery extend() is not recursive)
6540
//since 1.4 datetimepicker internally uses viewformat instead of format. Format is for submit only
6541
this.options.datetimepicker = $.extend({}, defaults.datetimepicker, options.datetimepicker, {
6542
format: this.options.viewformat
6543
});
6544
6545
//language
6546
this.options.datetimepicker.language = this.options.datetimepicker.language || 'en';
6547
6548
//store DPglobal
6549
this.dpg = $.fn.datetimepicker.DPGlobal;
6550
6551
//store parsed formats
6552
this.parsedFormat = this.dpg.parseFormat(this.options.format, this.options.formatType);
6553
this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat, this.options.formatType);
6554
},
6555
6556
render: function () {
6557
this.$input.datetimepicker(this.options.datetimepicker);
6558
6559
//adjust container position when viewMode changes
6560
//see https://github.com/smalot/bootstrap-datetimepicker/pull/80
6561
this.$input.on('changeMode', function(e) {
6562
var f = $(this).closest('form').parent();
6563
//timeout here, otherwise container changes position before form has new size
6564
setTimeout(function(){
6565
f.triggerHandler('resize');
6566
}, 0);
6567
});
6568
6569
//"clear" link
6570
if(this.options.clear) {
6571
this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
6572
e.preventDefault();
6573
e.stopPropagation();
6574
this.clear();
6575
}, this));
6576
6577
this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
6578
}
6579
},
6580
6581
value2html: function(value, element) {
6582
//formatDate works with UTCDate!
6583
var text = value ? this.dpg.formatDate(this.toUTC(value), this.parsedViewFormat, this.options.datetimepicker.language, this.options.formatType) : '';
6584
if(element) {
6585
DateTime.superclass.value2html.call(this, text, element);
6586
} else {
6587
return text;
6588
}
6589
},
6590
6591
html2value: function(html) {
6592
//parseDate return utc date!
6593
var value = this.parseDate(html, this.parsedViewFormat);
6594
return value ? this.fromUTC(value) : null;
6595
},
6596
6597
value2str: function(value) {
6598
//formatDate works with UTCDate!
6599
return value ? this.dpg.formatDate(this.toUTC(value), this.parsedFormat, this.options.datetimepicker.language, this.options.formatType) : '';
6600
},
6601
6602
str2value: function(str) {
6603
//parseDate return utc date!
6604
var value = this.parseDate(str, this.parsedFormat);
6605
return value ? this.fromUTC(value) : null;
6606
},
6607
6608
value2submit: function(value) {
6609
return this.value2str(value);
6610
},
6611
6612
value2input: function(value) {
6613
if(value) {
6614
this.$input.data('datetimepicker').setDate(value);
6615
}
6616
},
6617
6618
input2value: function() {
6619
//date may be cleared, in that case getDate() triggers error
6620
var dt = this.$input.data('datetimepicker');
6621
return dt.date ? dt.getDate() : null;
6622
},
6623
6624
activate: function() {
6625
},
6626
6627
clear: function() {
6628
this.$input.data('datetimepicker').date = null;
6629
this.$input.find('.active').removeClass('active');
6630
if(!this.options.showbuttons) {
6631
this.$input.closest('form').submit();
6632
}
6633
},
6634
6635
autosubmit: function() {
6636
this.$input.on('mouseup', '.minute', function(e){
6637
var $form = $(this).closest('form');
6638
setTimeout(function() {
6639
$form.submit();
6640
}, 200);
6641
});
6642
},
6643
6644
//convert date from local to utc
6645
toUTC: function(value) {
6646
return value ? new Date(value.valueOf() - value.getTimezoneOffset() * 60000) : value;
6647
},
6648
6649
//convert date from utc to local
6650
fromUTC: function(value) {
6651
return value ? new Date(value.valueOf() + value.getTimezoneOffset() * 60000) : value;
6652
},
6653
6654
/*
6655
For incorrect date bootstrap-datetimepicker returns current date that is not suitable
6656
for datetimefield.
6657
This function returns null for incorrect date.
6658
*/
6659
parseDate: function(str, format) {
6660
var date = null, formattedBack;
6661
if(str) {
6662
date = this.dpg.parseDate(str, format, this.options.datetimepicker.language, this.options.formatType);
6663
if(typeof str === 'string') {
6664
formattedBack = this.dpg.formatDate(date, format, this.options.datetimepicker.language, this.options.formatType);
6665
if(str !== formattedBack) {
6666
date = null;
6667
}
6668
}
6669
}
6670
return date;
6671
}
6672
6673
});
6674
6675
DateTime.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
6676
/**
6677
@property tpl
6678
@default <div></div>
6679
**/
6680
tpl:'<div class="editable-date well"></div>',
6681
/**
6682
@property inputclass
6683
@default null
6684
**/
6685
inputclass: null,
6686
/**
6687
Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
6688
Possible tokens are: <code>d, dd, m, mm, yy, yyyy, h, i</code>
6689
6690
@property format
6691
@type string
6692
@default yyyy-mm-dd hh:ii
6693
**/
6694
format:'yyyy-mm-dd hh:ii',
6695
formatType:'standard',
6696
/**
6697
Format used for displaying date. Also applied when converting date from element's text on init.
6698
If not specified equals to <code>format</code>
6699
6700
@property viewformat
6701
@type string
6702
@default null
6703
**/
6704
viewformat: null,
6705
/**
6706
Configuration of datetimepicker.
6707
Full list of options: https://github.com/smalot/bootstrap-datetimepicker
6708
6709
@property datetimepicker
6710
@type object
6711
@default { }
6712
**/
6713
datetimepicker:{
6714
todayHighlight: false,
6715
autoclose: false
6716
},
6717
/**
6718
Text shown as clear date button.
6719
If <code>false</code> clear button will not be rendered.
6720
6721
@property clear
6722
@type boolean|string
6723
@default 'x clear'
6724
**/
6725
clear: '&times; clear'
6726
});
6727
6728
$.fn.editabletypes.datetime = DateTime;
6729
6730
}(window.jQuery));
6731
/**
6732
Bootstrap datetimefield input - datetime input for inline mode.
6733
Shows normal <input type="text"> and binds popup datetimepicker.
6734
Automatically shown in inline mode.
6735
6736
@class datetimefield
6737
@extends datetime
6738
6739
**/
6740
(function ($) {
6741
"use strict";
6742
6743
var DateTimeField = function (options) {
6744
this.init('datetimefield', options, DateTimeField.defaults);
6745
this.initPicker(options, DateTimeField.defaults);
6746
};
6747
6748
$.fn.editableutils.inherit(DateTimeField, $.fn.editabletypes.datetime);
6749
6750
$.extend(DateTimeField.prototype, {
6751
render: function () {
6752
this.$input = this.$tpl.find('input');
6753
this.setClass();
6754
this.setAttr('placeholder');
6755
6756
this.$tpl.datetimepicker(this.options.datetimepicker);
6757
6758
//need to disable original event handlers
6759
this.$input.off('focus keydown');
6760
6761
//update value of datepicker
6762
this.$input.keyup($.proxy(function(){
6763
this.$tpl.removeData('date');
6764
this.$tpl.datetimepicker('update');
6765
}, this));
6766
6767
},
6768
6769
value2input: function(value) {
6770
this.$input.val(this.value2html(value));
6771
this.$tpl.datetimepicker('update');
6772
},
6773
6774
input2value: function() {
6775
return this.html2value(this.$input.val());
6776
},
6777
6778
activate: function() {
6779
$.fn.editabletypes.text.prototype.activate.call(this);
6780
},
6781
6782
autosubmit: function() {
6783
//reset autosubmit to empty
6784
}
6785
});
6786
6787
DateTimeField.defaults = $.extend({}, $.fn.editabletypes.datetime.defaults, {
6788
/**
6789
@property tpl
6790
**/
6791
tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',
6792
/**
6793
@property inputclass
6794
@default 'input-medium'
6795
**/
6796
inputclass: 'input-medium',
6797
6798
/* datetimepicker config */
6799
datetimepicker:{
6800
todayHighlight: false,
6801
autoclose: true
6802
}
6803
});
6804
6805
$.fn.editabletypes.datetimefield = DateTimeField;
6806
6807
}(window.jQuery));
6808