x-editable-rails 0.0.1

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