bootstrap-editable-rails 0.0.1

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