ab_admin 0.3.4 → 0.3.5

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.
Files changed (46) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +4 -2
  3. data/app/assets/javascripts/ab_admin/components/google_translate.js.coffee +1 -1
  4. data/app/assets/javascripts/ab_admin/components/init_nested_filelds.js.coffee +2 -2
  5. data/app/assets/javascripts/ab_admin/core/batch_actions.js.coffee +3 -3
  6. data/app/assets/javascripts/ab_admin/core/init.js.coffee +5 -3
  7. data/app/assets/javascripts/ab_admin/core/search_form.js.coffee +2 -2
  8. data/app/assets/javascripts/ab_admin/core/ui_utils.js.coffee +14 -2
  9. data/app/assets/javascripts/ab_admin/core/utils.js.coffee +4 -3
  10. data/app/assets/javascripts/ab_admin/main.js +3 -1
  11. data/app/assets/stylesheets/ab_admin/bootstrap_and_overrides.css.scss +5 -4
  12. data/app/assets/stylesheets/ab_admin/components/_text_styles.css.scss +47 -0
  13. data/app/assets/stylesheets/ab_admin/main.css.scss +1 -1
  14. data/app/controllers/admin/base_controller.rb +1 -1
  15. data/app/views/admin/base/edit.js.erb +1 -1
  16. data/app/views/admin/base/new.js.erb +1 -1
  17. data/app/views/admin/base/update.js.erb +1 -1
  18. data/app/views/admin/fileupload/_container.html.slim +4 -2
  19. data/app/views/admin/fileupload/_file.html.slim +1 -1
  20. data/app/views/admin/fileupload/_ftmpl.html.slim +1 -1
  21. data/app/views/admin/manager/_search_form.html.slim +1 -1
  22. data/app/views/admin/shared/_batch_actions.html.slim +9 -10
  23. data/app/views/layouts/admin/application.html.slim +3 -5
  24. data/config/locales/ru.yml +3 -2
  25. data/features/menu.feature +7 -2
  26. data/features/step_definitions/menu_steps.rb +7 -0
  27. data/lib/ab_admin.rb +3 -3
  28. data/lib/ab_admin/controllers/head_options.rb +2 -2
  29. data/lib/ab_admin/menu_builder.rb +2 -1
  30. data/lib/ab_admin/models/attachment_file.rb +1 -2
  31. data/lib/ab_admin/models/user.rb +1 -1
  32. data/lib/ab_admin/version.rb +1 -1
  33. data/lib/ab_admin/views/admin_helpers.rb +7 -4
  34. data/lib/ab_admin/views/admin_navigation_helpers.rb +4 -4
  35. data/lib/ab_admin/views/form_builder.rb +2 -2
  36. data/lib/generators/template.rb +0 -1
  37. data/spec/ab_admin_spec.rb +2 -2
  38. data/vendor/assets/images/ab_admin/clear.png +0 -0
  39. data/vendor/assets/images/ab_admin/loading.gif +0 -0
  40. data/vendor/assets/javascripts/ab_admin/bootstrap-editable-inline.js +2895 -0
  41. data/vendor/assets/javascripts/ab_admin/bootstrap-editable.js +4523 -0
  42. data/vendor/assets/javascripts/ab_admin/jquery_nested_form.js.coffee +19 -1
  43. data/vendor/assets/javascripts/jquery/jquery-ui.min.js +15 -0
  44. data/vendor/assets/javascripts/jquery/jquery.min.js +4 -0
  45. data/vendor/assets/stylesheets/ab_admin/bootstrap-editable.scss +461 -0
  46. metadata +12 -4
@@ -0,0 +1,4523 @@
1
+ // !!! removed datepicker
2
+ /*! X-editable - v1.4.1
3
+ * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
4
+ * http://github.com/vitalets/x-editable
5
+ * Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */
6
+
7
+ /**
8
+ Form with single input element, two buttons and two states: normal/loading.
9
+ Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown.
10
+ Editableform is linked with one of input types, e.g. 'text', 'select' etc.
11
+
12
+ @class editableform
13
+ @uses text
14
+ @uses textarea
15
+ **/
16
+ (function ($) {
17
+
18
+ var EditableForm = function (div, options) {
19
+ this.options = $.extend({}, $.fn.editableform.defaults, options);
20
+ this.$div = $(div); //div, containing form. Not form tag. Not editable-element.
21
+ if(!this.options.scope) {
22
+ this.options.scope = this;
23
+ }
24
+ //nothing shown after init
25
+ };
26
+
27
+ EditableForm.prototype = {
28
+ constructor: EditableForm,
29
+ initInput: function() { //called once
30
+ //take input from options (as it is created in editable-element)
31
+ this.input = this.options.input;
32
+
33
+ //set initial value
34
+ //todo: may be add check: typeof str === 'string' ?
35
+ this.value = this.input.str2value(this.options.value);
36
+ },
37
+ initTemplate: function() {
38
+ this.$form = $($.fn.editableform.template);
39
+ },
40
+ initButtons: function() {
41
+ this.$form.find('.editable-buttons').append($.fn.editableform.buttons);
42
+ },
43
+ /**
44
+ Renders editableform
45
+
46
+ @method render
47
+ **/
48
+ render: function() {
49
+ //init loader
50
+ this.$loading = $($.fn.editableform.loading);
51
+ this.$div.empty().append(this.$loading);
52
+
53
+ //init form template and buttons
54
+ this.initTemplate();
55
+ if(this.options.showbuttons) {
56
+ this.initButtons();
57
+ } else {
58
+ this.$form.find('.editable-buttons').remove();
59
+ }
60
+
61
+ //show loading state
62
+ this.showLoading();
63
+
64
+ /**
65
+ Fired when rendering starts
66
+ @event rendering
67
+ @param {Object} event event object
68
+ **/
69
+ this.$div.triggerHandler('rendering');
70
+
71
+ //init input
72
+ this.initInput();
73
+
74
+ //append input to form
75
+ this.input.prerender();
76
+ this.$form.find('div.editable-input').append(this.input.$tpl);
77
+
78
+ //append form to container
79
+ this.$div.append(this.$form);
80
+
81
+ //render input
82
+ $.when(this.input.render())
83
+ .then($.proxy(function () {
84
+ //setup input to submit automatically when no buttons shown
85
+ if(!this.options.showbuttons) {
86
+ this.input.autosubmit();
87
+ }
88
+
89
+ //attach 'cancel' handler
90
+ this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
91
+
92
+ if(this.input.error) {
93
+ this.error(this.input.error);
94
+ this.$form.find('.editable-submit').attr('disabled', true);
95
+ this.input.$input.attr('disabled', true);
96
+ //prevent form from submitting
97
+ this.$form.submit(function(e){ e.preventDefault(); });
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
+ //attach submit handler
104
+ this.$form.submit($.proxy(this.submit, this));
105
+ }
106
+
107
+ /**
108
+ Fired when form is rendered
109
+ @event rendered
110
+ @param {Object} event event object
111
+ **/
112
+ this.$div.triggerHandler('rendered');
113
+
114
+ this.showForm();
115
+
116
+ //call postrender method to perform actions required visibility of form
117
+ if(this.input.postrender) {
118
+ this.input.postrender();
119
+ }
120
+ }, this));
121
+ },
122
+ cancel: function() {
123
+ /**
124
+ Fired when form was cancelled by user
125
+ @event cancel
126
+ @param {Object} event event object
127
+ **/
128
+ this.$div.triggerHandler('cancel');
129
+ },
130
+ showLoading: function() {
131
+ var w, h;
132
+ if(this.$form) {
133
+ //set loading size equal to form
134
+ w = this.$form.outerWidth();
135
+ h = this.$form.outerHeight();
136
+ if(w) {
137
+ this.$loading.width(w);
138
+ }
139
+ if(h) {
140
+ this.$loading.height(h);
141
+ }
142
+ this.$form.hide();
143
+ } else {
144
+ //stretch loading to fill container width
145
+ w = this.$loading.parent().width();
146
+ if(w) {
147
+ this.$loading.width(w);
148
+ }
149
+ }
150
+ this.$loading.show();
151
+ },
152
+
153
+ showForm: function(activate) {
154
+ this.$loading.hide();
155
+ this.$form.show();
156
+ if(activate !== false) {
157
+ this.input.activate();
158
+ }
159
+ /**
160
+ Fired when form is shown
161
+ @event show
162
+ @param {Object} event event object
163
+ **/
164
+ this.$div.triggerHandler('show');
165
+ },
166
+
167
+ error: function(msg) {
168
+ var $group = this.$form.find('.control-group'),
169
+ $block = this.$form.find('.editable-error-block'),
170
+ lines;
171
+
172
+ if(msg === false) {
173
+ $group.removeClass($.fn.editableform.errorGroupClass);
174
+ $block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
175
+ } else {
176
+ //convert newline to <br> for more pretty error display
177
+ if(msg) {
178
+ lines = msg.split("\n");
179
+ for (var i = 0; i < lines.length; i++) {
180
+ lines[i] = $('<div>').text(lines[i]).html();
181
+ }
182
+ msg = lines.join('<br>');
183
+ }
184
+ $group.addClass($.fn.editableform.errorGroupClass);
185
+ $block.addClass($.fn.editableform.errorBlockClass).html(msg).show();
186
+ }
187
+ },
188
+
189
+ submit: function(e) {
190
+ e.stopPropagation();
191
+ e.preventDefault();
192
+
193
+ var error,
194
+ newValue = this.input.input2value(); //get new value from input
195
+
196
+ //validation
197
+ if (error = this.validate(newValue)) {
198
+ this.error(error);
199
+ this.showForm();
200
+ return;
201
+ }
202
+
203
+ //if value not changed --> trigger 'nochange' event and return
204
+ /*jslint eqeq: true*/
205
+ if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) {
206
+ /*jslint eqeq: false*/
207
+ /**
208
+ Fired when value not changed but form is submitted. Requires savenochange = false.
209
+ @event nochange
210
+ @param {Object} event event object
211
+ **/
212
+ this.$div.triggerHandler('nochange');
213
+ return;
214
+ }
215
+
216
+ //sending data to server
217
+ $.when(this.save(newValue))
218
+ .done($.proxy(function(response) {
219
+ //run success callback
220
+ var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null;
221
+
222
+ //if success callback returns false --> keep form open and do not activate input
223
+ if(res === false) {
224
+ this.error(false);
225
+ this.showForm(false);
226
+ return;
227
+ }
228
+
229
+ //if success callback returns string --> keep form open, show error and activate input
230
+ if(typeof res === 'string') {
231
+ this.error(res);
232
+ this.showForm();
233
+ return;
234
+ }
235
+
236
+ //if success callback returns object like {newValue: <something>} --> use that value instead of submitted
237
+ //it is usefull if you want to chnage value in url-function
238
+ if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
239
+ newValue = res.newValue;
240
+ }
241
+
242
+ //clear error message
243
+ this.error(false);
244
+ this.value = newValue;
245
+ /**
246
+ Fired when form is submitted
247
+ @event save
248
+ @param {Object} event event object
249
+ @param {Object} params additional params
250
+ @param {mixed} params.newValue submitted value
251
+ @param {Object} params.response ajax response
252
+
253
+ @example
254
+ $('#form-div').on('save'), function(e, params){
255
+ if(params.newValue === 'username') {...}
256
+ });
257
+ **/
258
+ this.$div.triggerHandler('save', {newValue: newValue, response: response});
259
+ }, this))
260
+ .fail($.proxy(function(xhr) {
261
+ this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!');
262
+ this.showForm();
263
+ }, this));
264
+ },
265
+
266
+ save: function(newValue) {
267
+ //convert value for submitting to server
268
+ var submitValue = this.input.value2submit(newValue);
269
+
270
+ //try parse composite pk defined as json string in data-pk
271
+ this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true);
272
+
273
+ var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk,
274
+ send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))),
275
+ params;
276
+
277
+ if (send) { //send to server
278
+ this.showLoading();
279
+
280
+ //standard params
281
+ params = {
282
+ name: this.options.name || '',
283
+ value: submitValue,
284
+ pk: pk
285
+ };
286
+
287
+ //additional params
288
+ if(typeof this.options.params === 'function') {
289
+ params = this.options.params.call(this.options.scope, params);
290
+ } else {
291
+ //try parse json in single quotes (from data-params attribute)
292
+ this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true);
293
+ $.extend(params, this.options.params);
294
+ }
295
+
296
+ if(typeof this.options.url === 'function') { //user's function
297
+ return this.options.url.call(this.options.scope, params);
298
+ } else {
299
+ //send ajax to server and return deferred object
300
+ return $.ajax($.extend({
301
+ url : this.options.url,
302
+ data : params,
303
+ type : 'POST'
304
+ }, this.options.ajaxOptions));
305
+ }
306
+ }
307
+ },
308
+
309
+ validate: function (value) {
310
+ if (value === undefined) {
311
+ value = this.value;
312
+ }
313
+ if (typeof this.options.validate === 'function') {
314
+ return this.options.validate.call(this.options.scope, value);
315
+ }
316
+ },
317
+
318
+ option: function(key, value) {
319
+ if(key in this.options) {
320
+ this.options[key] = value;
321
+ }
322
+
323
+ if(key === 'value') {
324
+ this.setValue(value);
325
+ }
326
+
327
+ //do not pass option to input as it is passed in editable-element
328
+ },
329
+
330
+ setValue: function(value, convertStr) {
331
+ if(convertStr) {
332
+ this.value = this.input.str2value(value);
333
+ } else {
334
+ this.value = value;
335
+ }
336
+
337
+ //if form is visible, update input
338
+ if(this.$form && this.$form.is(':visible')) {
339
+ this.input.value2input(this.value);
340
+ }
341
+ }
342
+ };
343
+
344
+ /*
345
+ Initialize editableform. Applied to jQuery object.
346
+
347
+ @method $().editableform(options)
348
+ @params {Object} options
349
+ @example
350
+ var $form = $('&lt;div&gt;').editableform({
351
+ type: 'text',
352
+ name: 'username',
353
+ url: '/post',
354
+ value: 'vitaliy'
355
+ });
356
+
357
+ //to display form you should call 'render' method
358
+ $form.editableform('render');
359
+ */
360
+ $.fn.editableform = function (option) {
361
+ var args = arguments;
362
+ return this.each(function () {
363
+ var $this = $(this),
364
+ data = $this.data('editableform'),
365
+ options = typeof option === 'object' && option;
366
+ if (!data) {
367
+ $this.data('editableform', (data = new EditableForm(this, options)));
368
+ }
369
+
370
+ if (typeof option === 'string') { //call method
371
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
372
+ }
373
+ });
374
+ };
375
+
376
+ //keep link to constructor to allow inheritance
377
+ $.fn.editableform.Constructor = EditableForm;
378
+
379
+ //defaults
380
+ $.fn.editableform.defaults = {
381
+ /* see also defaults for input */
382
+
383
+ /**
384
+ Type of input. Can be <code>text|textarea|select|date|checklist</code>
385
+
386
+ @property type
387
+ @type string
388
+ @default 'text'
389
+ **/
390
+ type: 'text',
391
+ /**
392
+ Url for submit, e.g. <code>'/post'</code>
393
+ If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks.
394
+
395
+ @property url
396
+ @type string|function
397
+ @default null
398
+ @example
399
+ url: function(params) {
400
+ var d = new $.Deferred;
401
+ if(params.value === 'abc') {
402
+ return d.reject('error message'); //returning error via deferred object
403
+ } else {
404
+ //async saving data in js model
405
+ someModel.asyncSaveMethod({
406
+ ...,
407
+ success: function(){
408
+ d.resolve();
409
+ }
410
+ });
411
+ return d.promise();
412
+ }
413
+ }
414
+ **/
415
+ url:null,
416
+ /**
417
+ Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value).
418
+ If defined as <code>function</code> - returned object **overwrites** original ajax data.
419
+ @example
420
+ params: function(params) {
421
+ //originally params contain pk, name and value
422
+ params.a = 1;
423
+ return params;
424
+ }
425
+
426
+ @property params
427
+ @type object|function
428
+ @default null
429
+ **/
430
+ params:null,
431
+ /**
432
+ Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute
433
+
434
+ @property name
435
+ @type string
436
+ @default null
437
+ **/
438
+ name: null,
439
+ /**
440
+ Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>.
441
+ Can be calculated dynamically via function.
442
+
443
+ @property pk
444
+ @type string|object|function
445
+ @default null
446
+ **/
447
+ pk: null,
448
+ /**
449
+ Initial value. If not defined - will be taken from element's content.
450
+ For __select__ type should be defined (as it is ID of shown text).
451
+
452
+ @property value
453
+ @type string|object
454
+ @default null
455
+ **/
456
+ value: null,
457
+ /**
458
+ Strategy for sending data on server. Can be <code>auto|always|never</code>.
459
+ When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element.
460
+
461
+ @property send
462
+ @type string
463
+ @default 'auto'
464
+ **/
465
+ send: 'auto',
466
+ /**
467
+ Function for client-side validation. If returns string - means validation not passed and string showed as error.
468
+
469
+ @property validate
470
+ @type function
471
+ @default null
472
+ @example
473
+ validate: function(value) {
474
+ if($.trim(value) == '') {
475
+ return 'This field is required';
476
+ }
477
+ }
478
+ **/
479
+ validate: null,
480
+ /**
481
+ Success callback. Called when value successfully sent on server and **response status = 200**.
482
+ Useful to work with json response. For example, if your backend response can be <code>{success: true}</code>
483
+ or <code>{success: false, msg: "server error"}</code> you can check it inside this callback.
484
+ If it returns **string** - means error occured and string is shown as error message.
485
+ If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user.
486
+ Otherwise newValue simply rendered into element.
487
+
488
+ @property success
489
+ @type function
490
+ @default null
491
+ @example
492
+ success: function(response, newValue) {
493
+ if(!response.success) return response.msg;
494
+ }
495
+ **/
496
+ success: null,
497
+ /**
498
+ Additional options for ajax request.
499
+ List of values: http://api.jquery.com/jQuery.ajax
500
+
501
+ @property ajaxOptions
502
+ @type object
503
+ @default null
504
+ @since 1.1.1
505
+ @example
506
+ ajaxOptions: {
507
+ type: 'put',
508
+ dataType: 'json'
509
+ }
510
+ **/
511
+ ajaxOptions: null,
512
+ /**
513
+ Whether to show buttons or not.
514
+ Form without buttons is auto-submitted.
515
+
516
+ @property showbuttons
517
+ @type boolean
518
+ @default true
519
+ @since 1.1.1
520
+ **/
521
+ showbuttons: true,
522
+ /**
523
+ Scope for callback methods (success, validate).
524
+ If <code>null</code> means editableform instance itself.
525
+
526
+ @property scope
527
+ @type DOMElement|object
528
+ @default null
529
+ @since 1.2.0
530
+ @private
531
+ **/
532
+ scope: null,
533
+ /**
534
+ Whether to save or cancel value when it was not changed but form was submitted
535
+
536
+ @property savenochange
537
+ @type boolean
538
+ @default false
539
+ @since 1.2.0
540
+ **/
541
+ savenochange: false
542
+ };
543
+
544
+ /*
545
+ Note: following params could redefined in engine: bootstrap or jqueryui:
546
+ Classes 'control-group' and 'editable-error-block' must always present!
547
+ */
548
+ $.fn.editableform.template = '<form class="form-inline editableform">'+
549
+ '<div class="control-group">' +
550
+ '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
551
+ '<div class="editable-error-block"></div>' +
552
+ '</div>' +
553
+ '</form>';
554
+
555
+ //loading div
556
+ $.fn.editableform.loading = '<div class="editableform-loading"></div>';
557
+
558
+ //buttons
559
+ $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
560
+ '<button type="button" class="editable-cancel">cancel</button>';
561
+
562
+ //error class attached to control-group
563
+ $.fn.editableform.errorGroupClass = null;
564
+
565
+ //error class attached to editable-error-block
566
+ $.fn.editableform.errorBlockClass = 'editable-error';
567
+ }(window.jQuery));
568
+ /**
569
+ * EditableForm utilites
570
+ */
571
+ (function ($) {
572
+ //utils
573
+ $.fn.editableutils = {
574
+ /**
575
+ * classic JS inheritance function
576
+ */
577
+ inherit: function (Child, Parent) {
578
+ var F = function() { };
579
+ F.prototype = Parent.prototype;
580
+ Child.prototype = new F();
581
+ Child.prototype.constructor = Child;
582
+ Child.superclass = Parent.prototype;
583
+ },
584
+
585
+ /**
586
+ * set caret position in input
587
+ * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
588
+ */
589
+ setCursorPosition: function(elem, pos) {
590
+ if (elem.setSelectionRange) {
591
+ elem.setSelectionRange(pos, pos);
592
+ } else if (elem.createTextRange) {
593
+ var range = elem.createTextRange();
594
+ range.collapse(true);
595
+ range.moveEnd('character', pos);
596
+ range.moveStart('character', pos);
597
+ range.select();
598
+ }
599
+ },
600
+
601
+ /**
602
+ * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes)
603
+ * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}">
604
+ * safe = true --> means no exception will be thrown
605
+ * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
606
+ */
607
+ tryParseJson: function(s, safe) {
608
+ if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
609
+ if (safe) {
610
+ try {
611
+ /*jslint evil: true*/
612
+ s = (new Function('return ' + s))();
613
+ /*jslint evil: false*/
614
+ } catch (e) {} finally {
615
+ return s;
616
+ }
617
+ } else {
618
+ /*jslint evil: true*/
619
+ s = (new Function('return ' + s))();
620
+ /*jslint evil: false*/
621
+ }
622
+ }
623
+ return s;
624
+ },
625
+
626
+ /**
627
+ * slice object by specified keys
628
+ */
629
+ sliceObj: function(obj, keys, caseSensitive /* default: false */) {
630
+ var key, keyLower, newObj = {};
631
+
632
+ if (!$.isArray(keys) || !keys.length) {
633
+ return newObj;
634
+ }
635
+
636
+ for (var i = 0; i < keys.length; i++) {
637
+ key = keys[i];
638
+ if (obj.hasOwnProperty(key)) {
639
+ newObj[key] = obj[key];
640
+ }
641
+
642
+ if(caseSensitive === true) {
643
+ continue;
644
+ }
645
+
646
+ //when getting data-* attributes via $.data() it's converted to lowercase.
647
+ //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
648
+ //workaround is code below.
649
+ keyLower = key.toLowerCase();
650
+ if (obj.hasOwnProperty(keyLower)) {
651
+ newObj[key] = obj[keyLower];
652
+ }
653
+ }
654
+
655
+ return newObj;
656
+ },
657
+
658
+ /*
659
+ exclude complex objects from $.data() before pass to config
660
+ */
661
+ getConfigData: function($element) {
662
+ var data = {};
663
+ $.each($element.data(), function(k, v) {
664
+ if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) {
665
+ data[k] = v;
666
+ }
667
+ });
668
+ return data;
669
+ },
670
+
671
+ /*
672
+ returns keys of object
673
+ */
674
+ objectKeys: function(o) {
675
+ if (Object.keys) {
676
+ return Object.keys(o);
677
+ } else {
678
+ if (o !== Object(o)) {
679
+ throw new TypeError('Object.keys called on a non-object');
680
+ }
681
+ var k=[], p;
682
+ for (p in o) {
683
+ if (Object.prototype.hasOwnProperty.call(o,p)) {
684
+ k.push(p);
685
+ }
686
+ }
687
+ return k;
688
+ }
689
+
690
+ },
691
+
692
+ /**
693
+ method to escape html.
694
+ **/
695
+ escape: function(str) {
696
+ return $('<div>').text(str).html();
697
+ },
698
+
699
+ /*
700
+ returns array items from sourceData having value property equal or inArray of 'value'
701
+ */
702
+ itemsByValue: function(value, sourceData, valueProp) {
703
+ if(!sourceData || value === null) {
704
+ return [];
705
+ }
706
+
707
+ valueProp = valueProp || 'value';
708
+
709
+ var isValArray = $.isArray(value),
710
+ result = [],
711
+ that = this;
712
+
713
+ $.each(sourceData, function(i, o) {
714
+ if(o.children) {
715
+ result = result.concat(that.itemsByValue(value, o.children));
716
+ } else {
717
+ /*jslint eqeq: true*/
718
+ if(isValArray) {
719
+ if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? o[valueProp] : o); }).length) {
720
+ result.push(o);
721
+ }
722
+ } else {
723
+ if(value == (o && typeof o === 'object' ? o[valueProp] : o)) {
724
+ result.push(o);
725
+ }
726
+ }
727
+ /*jslint eqeq: false*/
728
+ }
729
+ });
730
+
731
+ return result;
732
+ },
733
+
734
+ /*
735
+ Returns input by options: type, mode.
736
+ */
737
+ createInput: function(options) {
738
+ var TypeConstructor, typeOptions, input,
739
+ type = options.type;
740
+
741
+ //`date` is some kind of virtual type that is transformed to one of exact types
742
+ //depending on mode and core lib
743
+ if(type === 'date') {
744
+ //inline
745
+ if(options.mode === 'inline') {
746
+ if($.fn.editabletypes.datefield) {
747
+ type = 'datefield';
748
+ } else if($.fn.editabletypes.dateuifield) {
749
+ type = 'dateuifield';
750
+ }
751
+ //popup
752
+ } else {
753
+ if($.fn.editabletypes.date) {
754
+ type = 'date';
755
+ } else if($.fn.editabletypes.dateui) {
756
+ type = 'dateui';
757
+ }
758
+ }
759
+
760
+ //if type still `date` and not exist in types, replace with `combodate` that is base input
761
+ if(type === 'date' && !$.fn.editabletypes.date) {
762
+ type = 'combodate';
763
+ }
764
+ }
765
+
766
+ //change wysihtml5 to textarea for jquery UI and plain versions
767
+ if(type === 'wysihtml5' && !$.fn.editabletypes[type]) {
768
+ type = 'textarea';
769
+ }
770
+
771
+ //create input of specified type. Input will be used for converting value, not in form
772
+ if(typeof $.fn.editabletypes[type] === 'function') {
773
+ TypeConstructor = $.fn.editabletypes[type];
774
+ typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults));
775
+ input = new TypeConstructor(typeOptions);
776
+ return input;
777
+ } else {
778
+ $.error('Unknown type: '+ type);
779
+ return false;
780
+ }
781
+ }
782
+
783
+ };
784
+ }(window.jQuery));
785
+
786
+ /**
787
+ Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
788
+ This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
789
+ Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br>
790
+ Applied as jQuery method.
791
+
792
+ @class editableContainer
793
+ @uses editableform
794
+ **/
795
+ (function ($) {
796
+
797
+ var Popup = function (element, options) {
798
+ this.init(element, options);
799
+ };
800
+
801
+ var Inline = function (element, options) {
802
+ this.init(element, options);
803
+ };
804
+
805
+ //methods
806
+ Popup.prototype = {
807
+ containerName: null, //tbd in child class
808
+ innerCss: null, //tbd in child class
809
+ init: function(element, options) {
810
+ this.$element = $(element);
811
+ //since 1.4.1 container do not use data-* directly as they already merged into options.
812
+ this.options = $.extend({}, $.fn.editableContainer.defaults, options);
813
+ this.splitOptions();
814
+
815
+ //set scope of form callbacks to element
816
+ this.formOptions.scope = this.$element[0];
817
+
818
+ this.initContainer();
819
+
820
+ //bind 'destroyed' listener to destroy container when element is removed from dom
821
+ this.$element.on('destroyed', $.proxy(function(){
822
+ this.destroy();
823
+ }, this));
824
+
825
+ //attach document handler to close containers on click / escape
826
+ if(!$(document).data('editable-handlers-attached')) {
827
+ //close all on escape
828
+ $(document).on('keyup.editable', function (e) {
829
+ if (e.which === 27) {
830
+ $('.editable-open').editableContainer('hide');
831
+ //todo: return focus on element
832
+ }
833
+ });
834
+
835
+ //close containers when click outside
836
+ $(document).on('click.editable', function(e) {
837
+ var $target = $(e.target), i,
838
+ exclude_classes = ['.editable-container',
839
+ '.ui-datepicker-header',
840
+ '.modal-backdrop',
841
+ '.bootstrap-wysihtml5-insert-image-modal',
842
+ '.bootstrap-wysihtml5-insert-link-modal'];
843
+
844
+ //if click inside one of exclude classes --> no nothing
845
+ for(i=0; i<exclude_classes.length; i++) {
846
+ if($target.is(exclude_classes[i]) || $target.parents(exclude_classes[i]).length) {
847
+ return;
848
+ }
849
+ }
850
+
851
+ //close all open containers (except one - target)
852
+ Popup.prototype.closeOthers(e.target);
853
+ });
854
+
855
+ $(document).data('editable-handlers-attached', true);
856
+ }
857
+ },
858
+
859
+ //split options on containerOptions and formOptions
860
+ splitOptions: function() {
861
+ this.containerOptions = {};
862
+ this.formOptions = {};
863
+ var cDef = $.fn[this.containerName].defaults;
864
+ //keys defined in container defaults go to container, others go to form
865
+ for(var k in this.options) {
866
+ if(k in cDef) {
867
+ this.containerOptions[k] = this.options[k];
868
+ } else {
869
+ this.formOptions[k] = this.options[k];
870
+ }
871
+ }
872
+ },
873
+
874
+ /*
875
+ Returns jquery object of container
876
+ @method tip()
877
+ */
878
+ tip: function() {
879
+ return this.container() ? this.container().$tip : null;
880
+ },
881
+
882
+ /* returns container object */
883
+ container: function() {
884
+ return this.$element.data(this.containerName);
885
+ },
886
+
887
+ call: function() {
888
+ this.$element[this.containerName].apply(this.$element, arguments);
889
+ },
890
+
891
+ initContainer: function(){
892
+ this.call(this.containerOptions);
893
+ },
894
+
895
+ renderForm: function() {
896
+ this.$form
897
+ .editableform(this.formOptions)
898
+ .on({
899
+ save: $.proxy(this.save, this), //click on submit button (value changed)
900
+ nochange: $.proxy(function(){ this.hide('nochange'); }, this), //click on submit button (value NOT changed)
901
+ cancel: $.proxy(function(){ this.hide('cancel'); }, this), //click on calcel button
902
+ show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state)
903
+ rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
904
+ resize: $.proxy(this.setPosition, this), //this allows to re-position container when form size is changed
905
+ rendered: $.proxy(function(){
906
+ /**
907
+ Fired when container is shown and form is rendered (for select will wait for loading dropdown options)
908
+
909
+ @event shown
910
+ @param {Object} event event object
911
+ @example
912
+ $('#username').on('shown', function() {
913
+ var $tip = $(this).data('editableContainer').tip();
914
+ $tip.find('input').val('overwriting value of input..');
915
+ });
916
+ **/
917
+ this.$element.triggerHandler('shown');
918
+ }, this)
919
+ })
920
+ .editableform('render');
921
+ },
922
+
923
+ /**
924
+ Shows container with form
925
+ @method show()
926
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
927
+ **/
928
+ /* Note: poshytip owerwrites this method totally! */
929
+ show: function (closeAll) {
930
+ this.$element.addClass('editable-open');
931
+ if(closeAll !== false) {
932
+ //close all open containers (except this)
933
+ this.closeOthers(this.$element[0]);
934
+ }
935
+
936
+ //show container itself
937
+ this.innerShow();
938
+ this.tip().addClass('editable-container');
939
+
940
+ /*
941
+ Currently, form is re-rendered on every show.
942
+ The main reason is that we dont know, what container will do with content when closed:
943
+ remove(), detach() or just hide().
944
+
945
+ Detaching form itself before hide and re-insert before show is good solution,
946
+ but visually it looks ugly, as container changes size before hide.
947
+ */
948
+
949
+ //if form already exist - delete previous data
950
+ if(this.$form) {
951
+ //todo: destroy prev data!
952
+ //this.$form.destroy();
953
+ }
954
+
955
+ this.$form = $('<div>');
956
+
957
+ //insert form into container body
958
+ if(this.tip().is(this.innerCss)) {
959
+ //for inline container
960
+ this.tip().append(this.$form);
961
+ } else {
962
+ this.tip().find(this.innerCss).append(this.$form);
963
+ }
964
+
965
+ //render form
966
+ this.renderForm();
967
+ },
968
+
969
+ /**
970
+ Hides container with form
971
+ @method hide()
972
+ @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
973
+ **/
974
+ hide: function(reason) {
975
+ if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
976
+ return;
977
+ }
978
+
979
+ this.$element.removeClass('editable-open');
980
+ this.innerHide();
981
+
982
+ /**
983
+ Fired when container was hidden. It occurs on both save or cancel.
984
+
985
+ @event hidden
986
+ @param {object} event event object
987
+ @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
988
+ @example
989
+ $('#username').on('hidden', function(e, reason) {
990
+ if(reason === 'save' || reason === 'cancel') {
991
+ //auto-open next editable
992
+ $(this).closest('tr').next().find('.editable').editable('show');
993
+ }
994
+ });
995
+ **/
996
+ this.$element.triggerHandler('hidden', reason);
997
+ },
998
+
999
+ /* internal show method. To be overwritten in child classes */
1000
+ innerShow: function () {
1001
+
1002
+ },
1003
+
1004
+ /* internal hide method. To be overwritten in child classes */
1005
+ innerHide: function () {
1006
+
1007
+ },
1008
+
1009
+ /**
1010
+ Toggles container visibility (show / hide)
1011
+ @method toggle()
1012
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1013
+ **/
1014
+ toggle: function(closeAll) {
1015
+ if(this.container() && this.tip() && this.tip().is(':visible')) {
1016
+ this.hide();
1017
+ } else {
1018
+ this.show(closeAll);
1019
+ }
1020
+ },
1021
+
1022
+ /*
1023
+ Updates the position of container when content changed.
1024
+ @method setPosition()
1025
+ */
1026
+ setPosition: function() {
1027
+ //tbd in child class
1028
+ },
1029
+
1030
+ save: function(e, params) {
1031
+ /**
1032
+ Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
1033
+
1034
+ @event save
1035
+ @param {Object} event event object
1036
+ @param {Object} params additional params
1037
+ @param {mixed} params.newValue submitted value
1038
+ @param {Object} params.response ajax response
1039
+ @example
1040
+ $('#username').on('save', function(e, params) {
1041
+ //assuming server response: '{success: true}'
1042
+ var pk = $(this).data('editableContainer').options.pk;
1043
+ if(params.response && params.response.success) {
1044
+ alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
1045
+ } else {
1046
+ alert('error!');
1047
+ }
1048
+ });
1049
+ **/
1050
+ this.$element.triggerHandler('save', params);
1051
+
1052
+ //hide must be after trigger, as saving value may require methods od plugin, applied to input
1053
+ this.hide('save');
1054
+ },
1055
+
1056
+ /**
1057
+ Sets new option
1058
+
1059
+ @method option(key, value)
1060
+ @param {string} key
1061
+ @param {mixed} value
1062
+ **/
1063
+ option: function(key, value) {
1064
+ this.options[key] = value;
1065
+ if(key in this.containerOptions) {
1066
+ this.containerOptions[key] = value;
1067
+ this.setContainerOption(key, value);
1068
+ } else {
1069
+ this.formOptions[key] = value;
1070
+ if(this.$form) {
1071
+ this.$form.editableform('option', key, value);
1072
+ }
1073
+ }
1074
+ },
1075
+
1076
+ setContainerOption: function(key, value) {
1077
+ this.call('option', key, value);
1078
+ },
1079
+
1080
+ /**
1081
+ Destroys the container instance
1082
+ @method destroy()
1083
+ **/
1084
+ destroy: function() {
1085
+ this.hide();
1086
+ this.innerDestroy();
1087
+ this.$element.off('destroyed');
1088
+ this.$element.removeData('editableContainer');
1089
+ },
1090
+
1091
+ /* to be overwritten in child classes */
1092
+ innerDestroy: function() {
1093
+
1094
+ },
1095
+
1096
+ /*
1097
+ Closes other containers except one related to passed element.
1098
+ Other containers can be cancelled or submitted (depends on onblur option)
1099
+ */
1100
+ closeOthers: function(element) {
1101
+ $('.editable-open').each(function(i, el){
1102
+ //do nothing with passed element and it's children
1103
+ if(el === element || $(el).find(element).length) {
1104
+ return;
1105
+ }
1106
+
1107
+ //otherwise cancel or submit all open containers
1108
+ var $el = $(el),
1109
+ ec = $el.data('editableContainer');
1110
+
1111
+ if(!ec) {
1112
+ return;
1113
+ }
1114
+
1115
+ if(ec.options.onblur === 'cancel') {
1116
+ $el.data('editableContainer').hide('onblur');
1117
+ } else if(ec.options.onblur === 'submit') {
1118
+ $el.data('editableContainer').tip().find('form').submit();
1119
+ }
1120
+ });
1121
+
1122
+ },
1123
+
1124
+ /**
1125
+ Activates input of visible container (e.g. set focus)
1126
+ @method activate()
1127
+ **/
1128
+ activate: function() {
1129
+ if(this.tip && this.tip().is(':visible') && this.$form) {
1130
+ this.$form.data('editableform').input.activate();
1131
+ }
1132
+ }
1133
+
1134
+ };
1135
+
1136
+ /**
1137
+ jQuery method to initialize editableContainer.
1138
+
1139
+ @method $().editableContainer(options)
1140
+ @params {Object} options
1141
+ @example
1142
+ $('#edit').editableContainer({
1143
+ type: 'text',
1144
+ url: '/post',
1145
+ pk: 1,
1146
+ value: 'hello'
1147
+ });
1148
+ **/
1149
+ $.fn.editableContainer = function (option) {
1150
+ var args = arguments;
1151
+ return this.each(function () {
1152
+ var $this = $(this),
1153
+ dataKey = 'editableContainer',
1154
+ data = $this.data(dataKey),
1155
+ options = typeof option === 'object' && option,
1156
+ Constructor = (options.mode === 'inline') ? Inline : Popup;
1157
+
1158
+ if (!data) {
1159
+ $this.data(dataKey, (data = new Constructor(this, options)));
1160
+ }
1161
+
1162
+ if (typeof option === 'string') { //call method
1163
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
1164
+ }
1165
+ });
1166
+ };
1167
+
1168
+ //store constructors
1169
+ $.fn.editableContainer.Popup = Popup;
1170
+ $.fn.editableContainer.Inline = Inline;
1171
+
1172
+ //defaults
1173
+ $.fn.editableContainer.defaults = {
1174
+ /**
1175
+ Initial value of form input
1176
+
1177
+ @property value
1178
+ @type mixed
1179
+ @default null
1180
+ @private
1181
+ **/
1182
+ value: null,
1183
+ /**
1184
+ Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inline container.
1185
+
1186
+ @property placement
1187
+ @type string
1188
+ @default 'top'
1189
+ **/
1190
+ placement: 'top',
1191
+ /**
1192
+ Whether to hide container on save/cancel.
1193
+
1194
+ @property autohide
1195
+ @type boolean
1196
+ @default true
1197
+ @private
1198
+ **/
1199
+ autohide: true,
1200
+ /**
1201
+ Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>.
1202
+ Setting <code>ignore</code> allows to have several containers open.
1203
+
1204
+ @property onblur
1205
+ @type string
1206
+ @default 'cancel'
1207
+ @since 1.1.1
1208
+ **/
1209
+ onblur: 'cancel',
1210
+
1211
+ /**
1212
+ Animation speed (inline mode)
1213
+ @property anim
1214
+ @type string
1215
+ @default 'fast'
1216
+ **/
1217
+ anim: 'fast',
1218
+
1219
+ /**
1220
+ Mode of editable, can be `popup` or `inline`
1221
+
1222
+ @property mode
1223
+ @type string
1224
+ @default 'popup'
1225
+ @since 1.4.0
1226
+ **/
1227
+ mode: 'popup'
1228
+ };
1229
+
1230
+ /*
1231
+ * workaround to have 'destroyed' event to destroy popover when element is destroyed
1232
+ * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
1233
+ */
1234
+ jQuery.event.special.destroyed = {
1235
+ remove: function(o) {
1236
+ if (o.handler) {
1237
+ o.handler();
1238
+ }
1239
+ }
1240
+ };
1241
+
1242
+ }(window.jQuery));
1243
+
1244
+ /**
1245
+ * Editable Inline
1246
+ * ---------------------
1247
+ */
1248
+ (function ($) {
1249
+
1250
+ //copy prototype from EditableContainer
1251
+ //extend methods
1252
+ $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, {
1253
+ containerName: 'editableform',
1254
+ innerCss: '.editable-inline',
1255
+
1256
+ initContainer: function(){
1257
+ //container is <span> element
1258
+ this.$tip = $('<span></span>').addClass('editable-inline');
1259
+
1260
+ //convert anim to miliseconds (int)
1261
+ if(!this.options.anim) {
1262
+ this.options.anim = 0;
1263
+ }
1264
+ },
1265
+
1266
+ splitOptions: function() {
1267
+ //all options are passed to form
1268
+ this.containerOptions = {};
1269
+ this.formOptions = this.options;
1270
+ },
1271
+
1272
+ tip: function() {
1273
+ return this.$tip;
1274
+ },
1275
+
1276
+ innerShow: function () {
1277
+ this.$element.hide();
1278
+ this.tip().insertAfter(this.$element).show();
1279
+ },
1280
+
1281
+ innerHide: function () {
1282
+ this.$tip.hide(this.options.anim, $.proxy(function() {
1283
+ this.$element.show();
1284
+ this.innerDestroy();
1285
+ }, this));
1286
+ },
1287
+
1288
+ innerDestroy: function() {
1289
+ if(this.tip()) {
1290
+ this.tip().empty().remove();
1291
+ }
1292
+ }
1293
+ });
1294
+
1295
+ }(window.jQuery));
1296
+ /**
1297
+ Makes editable any HTML element on the page. Applied as jQuery method.
1298
+
1299
+ @class editable
1300
+ @uses editableContainer
1301
+ **/
1302
+ (function ($) {
1303
+
1304
+ var Editable = function (element, options) {
1305
+ this.$element = $(element);
1306
+ //data-* has more priority over js options: because dynamically created elements may change data-*
1307
+ this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element));
1308
+ if(this.options.selector) {
1309
+ this.initLive();
1310
+ } else {
1311
+ this.init();
1312
+ }
1313
+ };
1314
+
1315
+ Editable.prototype = {
1316
+ constructor: Editable,
1317
+ init: function () {
1318
+ var isValueByText = false,
1319
+ doAutotext, finalize;
1320
+
1321
+ //name
1322
+ this.options.name = this.options.name || this.$element.attr('id');
1323
+
1324
+ //create input of specified type. Input will be used for converting value, not in form
1325
+ this.input = $.fn.editableutils.createInput(this.options);
1326
+ if(!this.input) {
1327
+ return;
1328
+ }
1329
+
1330
+ //set value from settings or by element's text
1331
+ if (this.options.value === undefined || this.options.value === null) {
1332
+ this.value = this.input.html2value($.trim(this.$element.html()));
1333
+ isValueByText = true;
1334
+ } else {
1335
+ /*
1336
+ value can be string when received from 'data-value' attribute
1337
+ for complext objects value can be set as json string in data-value attribute,
1338
+ e.g. data-value="{city: 'Moscow', street: 'Lenina'}"
1339
+ */
1340
+ this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true);
1341
+ if(typeof this.options.value === 'string') {
1342
+ this.value = this.input.str2value(this.options.value);
1343
+ } else {
1344
+ this.value = this.options.value;
1345
+ }
1346
+ }
1347
+
1348
+ //add 'editable' class to every editable element
1349
+ this.$element.addClass('editable');
1350
+
1351
+ //attach handler activating editable. In disabled mode it just prevent default action (useful for links)
1352
+ if(this.options.toggle !== 'manual') {
1353
+ this.$element.addClass('editable-click');
1354
+ this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
1355
+ //prevent following link
1356
+ e.preventDefault();
1357
+
1358
+ //stop propagation not required because in document click handler it checks event target
1359
+ //e.stopPropagation();
1360
+
1361
+ if(this.options.toggle === 'mouseenter') {
1362
+ //for hover only show container
1363
+ this.show();
1364
+ } else {
1365
+ //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
1366
+ var closeAll = (this.options.toggle !== 'click');
1367
+ this.toggle(closeAll);
1368
+ }
1369
+ }, this));
1370
+ } else {
1371
+ this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
1372
+ }
1373
+
1374
+ //check conditions for autotext:
1375
+ //if value was generated by text or value is empty, no sense to run autotext
1376
+ doAutotext = !isValueByText && this.value !== null && this.value !== undefined;
1377
+ doAutotext &= (this.options.autotext === 'always') || (this.options.autotext === 'auto' && !this.$element.text().length);
1378
+ $.when(doAutotext ? this.render() : true).then($.proxy(function() {
1379
+ if(this.options.disabled) {
1380
+ this.disable();
1381
+ } else {
1382
+ this.enable();
1383
+ }
1384
+ /**
1385
+ Fired when element was initialized by editable method.
1386
+
1387
+ @event init
1388
+ @param {Object} event event object
1389
+ @param {Object} editable editable instance (as here it cannot accessed via data('editable'))
1390
+ @since 1.2.0
1391
+ @example
1392
+ $('#username').on('init', function(e, editable) {
1393
+ alert('initialized ' + editable.options.name);
1394
+ });
1395
+ **/
1396
+ this.$element.triggerHandler('init', this);
1397
+ }, this));
1398
+ },
1399
+
1400
+ /*
1401
+ Initializes parent element for live editables
1402
+ */
1403
+ initLive: function() {
1404
+ //store selector
1405
+ var selector = this.options.selector;
1406
+ //modify options for child elements
1407
+ this.options.selector = false;
1408
+ this.options.autotext = 'never';
1409
+ //listen toggle events
1410
+ this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){
1411
+ var $target = $(e.target);
1412
+ if(!$target.data('editable')) {
1413
+ $target.editable(this.options).trigger(e);
1414
+ }
1415
+ }, this));
1416
+ },
1417
+
1418
+ /*
1419
+ Renders value into element's text.
1420
+ Can call custom display method from options.
1421
+ Can return deferred object.
1422
+ @method render()
1423
+ @param {mixed} response server response (if exist) to pass into display function
1424
+ */
1425
+ render: function(response) {
1426
+ //do not display anything
1427
+ if(this.options.display === false) {
1428
+ return;
1429
+ }
1430
+
1431
+ //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded
1432
+ if(this.input.value2htmlFinal) {
1433
+ return this.input.value2html(this.value, this.$element[0], this.options.display, response);
1434
+ //if display method defined --> use it
1435
+ } else if(typeof this.options.display === 'function') {
1436
+ return this.options.display.call(this.$element[0], this.value, response);
1437
+ //else use input's original value2html() method
1438
+ } else {
1439
+ return this.input.value2html(this.value, this.$element[0]);
1440
+ }
1441
+ },
1442
+
1443
+ /**
1444
+ Enables editable
1445
+ @method enable()
1446
+ **/
1447
+ enable: function() {
1448
+ this.options.disabled = false;
1449
+ this.$element.removeClass('editable-disabled');
1450
+ this.handleEmpty(this.isEmpty);
1451
+ if(this.options.toggle !== 'manual') {
1452
+ if(this.$element.attr('tabindex') === '-1') {
1453
+ this.$element.removeAttr('tabindex');
1454
+ }
1455
+ }
1456
+ },
1457
+
1458
+ /**
1459
+ Disables editable
1460
+ @method disable()
1461
+ **/
1462
+ disable: function() {
1463
+ this.options.disabled = true;
1464
+ this.hide();
1465
+ this.$element.addClass('editable-disabled');
1466
+ this.handleEmpty(this.isEmpty);
1467
+ //do not stop focus on this element
1468
+ this.$element.attr('tabindex', -1);
1469
+ },
1470
+
1471
+ /**
1472
+ Toggles enabled / disabled state of editable element
1473
+ @method toggleDisabled()
1474
+ **/
1475
+ toggleDisabled: function() {
1476
+ if(this.options.disabled) {
1477
+ this.enable();
1478
+ } else {
1479
+ this.disable();
1480
+ }
1481
+ },
1482
+
1483
+ /**
1484
+ Sets new option
1485
+
1486
+ @method option(key, value)
1487
+ @param {string|object} key option name or object with several options
1488
+ @param {mixed} value option new value
1489
+ @example
1490
+ $('.editable').editable('option', 'pk', 2);
1491
+ **/
1492
+ option: function(key, value) {
1493
+ //set option(s) by object
1494
+ if(key && typeof key === 'object') {
1495
+ $.each(key, $.proxy(function(k, v){
1496
+ this.option($.trim(k), v);
1497
+ }, this));
1498
+ return;
1499
+ }
1500
+
1501
+ //set option by string
1502
+ this.options[key] = value;
1503
+
1504
+ //disabled
1505
+ if(key === 'disabled') {
1506
+ return value ? this.disable() : this.enable();
1507
+ }
1508
+
1509
+ //value
1510
+ if(key === 'value') {
1511
+ this.setValue(value);
1512
+ }
1513
+
1514
+ //transfer new option to container!
1515
+ if(this.container) {
1516
+ this.container.option(key, value);
1517
+ }
1518
+
1519
+ //pass option to input directly (as it points to the same in form)
1520
+ if(this.input.option) {
1521
+ this.input.option(key, value);
1522
+ }
1523
+
1524
+ },
1525
+
1526
+ /*
1527
+ * set emptytext if element is empty
1528
+ */
1529
+ handleEmpty: function (isEmpty) {
1530
+ //do not handle empty if we do not display anything
1531
+ if(this.options.display === false) {
1532
+ return;
1533
+ }
1534
+
1535
+ this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === '';
1536
+
1537
+ //emptytext shown only for enabled
1538
+ if(!this.options.disabled) {
1539
+ if (this.isEmpty) {
1540
+ this.$element.text(this.options.emptytext);
1541
+ if(this.options.emptyclass) {
1542
+ this.$element.addClass(this.options.emptyclass);
1543
+ }
1544
+ } else if(this.options.emptyclass) {
1545
+ this.$element.removeClass(this.options.emptyclass);
1546
+ }
1547
+ } else {
1548
+ //below required if element disable property was changed
1549
+ if(this.isEmpty) {
1550
+ this.$element.empty();
1551
+ if(this.options.emptyclass) {
1552
+ this.$element.removeClass(this.options.emptyclass);
1553
+ }
1554
+ }
1555
+ }
1556
+ },
1557
+
1558
+ /**
1559
+ Shows container with form
1560
+ @method show()
1561
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1562
+ **/
1563
+ show: function (closeAll) {
1564
+ if(this.options.disabled) {
1565
+ return;
1566
+ }
1567
+
1568
+ //init editableContainer: popover, tooltip, inline, etc..
1569
+ if(!this.container) {
1570
+ var containerOptions = $.extend({}, this.options, {
1571
+ value: this.value,
1572
+ input: this.input //pass input to form (as it is already created)
1573
+ });
1574
+ this.$element.editableContainer(containerOptions);
1575
+ //listen `save` event
1576
+ this.$element.on("save.internal", $.proxy(this.save, this));
1577
+ this.container = this.$element.data('editableContainer');
1578
+ } else if(this.container.tip().is(':visible')) {
1579
+ return;
1580
+ }
1581
+
1582
+ //show container
1583
+ this.container.show(closeAll);
1584
+ },
1585
+
1586
+ /**
1587
+ Hides container with form
1588
+ @method hide()
1589
+ **/
1590
+ hide: function () {
1591
+ if(this.container) {
1592
+ this.container.hide();
1593
+ }
1594
+ },
1595
+
1596
+ /**
1597
+ Toggles container visibility (show / hide)
1598
+ @method toggle()
1599
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1600
+ **/
1601
+ toggle: function(closeAll) {
1602
+ if(this.container && this.container.tip().is(':visible')) {
1603
+ this.hide();
1604
+ } else {
1605
+ this.show(closeAll);
1606
+ }
1607
+ },
1608
+
1609
+ /*
1610
+ * called when form was submitted
1611
+ */
1612
+ save: function(e, params) {
1613
+ //mark element with unsaved class if needed
1614
+ if(this.options.unsavedclass) {
1615
+ /*
1616
+ Add unsaved css to element if:
1617
+ - url is not user's function
1618
+ - value was not sent to server
1619
+ - params.response === undefined, that means data was not sent
1620
+ - value changed
1621
+ */
1622
+ var sent = false;
1623
+ sent = sent || typeof this.options.url === 'function';
1624
+ sent = sent || this.options.display === false;
1625
+ sent = sent || params.response !== undefined;
1626
+ sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue));
1627
+
1628
+ if(sent) {
1629
+ this.$element.removeClass(this.options.unsavedclass);
1630
+ } else {
1631
+ this.$element.addClass(this.options.unsavedclass);
1632
+ }
1633
+ }
1634
+
1635
+ //set new value
1636
+ this.setValue(params.newValue, false, params.response);
1637
+
1638
+ /**
1639
+ Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
1640
+
1641
+ @event save
1642
+ @param {Object} event event object
1643
+ @param {Object} params additional params
1644
+ @param {mixed} params.newValue submitted value
1645
+ @param {Object} params.response ajax response
1646
+ @example
1647
+ $('#username').on('save', function(e, params) {
1648
+ alert('Saved value: ' + params.newValue);
1649
+ });
1650
+ **/
1651
+ //event itself is triggered by editableContainer. Description here is only for documentation
1652
+ },
1653
+
1654
+ validate: function () {
1655
+ if (typeof this.options.validate === 'function') {
1656
+ return this.options.validate.call(this, this.value);
1657
+ }
1658
+ },
1659
+
1660
+ /**
1661
+ Sets new value of editable
1662
+ @method setValue(value, convertStr)
1663
+ @param {mixed} value new value
1664
+ @param {boolean} convertStr whether to convert value from string to internal format
1665
+ **/
1666
+ setValue: function(value, convertStr, response) {
1667
+ if(convertStr) {
1668
+ this.value = this.input.str2value(value);
1669
+ } else {
1670
+ this.value = value;
1671
+ }
1672
+ if(this.container) {
1673
+ this.container.option('value', this.value);
1674
+ }
1675
+ $.when(this.render(response))
1676
+ .then($.proxy(function() {
1677
+ this.handleEmpty();
1678
+ }, this));
1679
+ },
1680
+
1681
+ /**
1682
+ Activates input of visible container (e.g. set focus)
1683
+ @method activate()
1684
+ **/
1685
+ activate: function() {
1686
+ if(this.container) {
1687
+ this.container.activate();
1688
+ }
1689
+ },
1690
+
1691
+ /**
1692
+ Removes editable feature from element
1693
+ @method destroy()
1694
+ **/
1695
+ destroy: function() {
1696
+ if(this.container) {
1697
+ this.container.destroy();
1698
+ }
1699
+
1700
+ if(this.options.toggle !== 'manual') {
1701
+ this.$element.removeClass('editable-click');
1702
+ this.$element.off(this.options.toggle + '.editable');
1703
+ }
1704
+
1705
+ this.$element.off("save.internal");
1706
+
1707
+ this.$element.removeClass('editable');
1708
+ this.$element.removeClass('editable-open');
1709
+ this.$element.removeData('editable');
1710
+ }
1711
+ };
1712
+
1713
+ /* EDITABLE PLUGIN DEFINITION
1714
+ * ======================= */
1715
+
1716
+ /**
1717
+ jQuery method to initialize editable element.
1718
+
1719
+ @method $().editable(options)
1720
+ @params {Object} options
1721
+ @example
1722
+ $('#username').editable({
1723
+ type: 'text',
1724
+ url: '/post',
1725
+ pk: 1
1726
+ });
1727
+ **/
1728
+ $.fn.editable = function (option) {
1729
+ //special API methods returning non-jquery object
1730
+ var result = {}, args = arguments, datakey = 'editable';
1731
+ switch (option) {
1732
+ /**
1733
+ Runs client-side validation for all matched editables
1734
+
1735
+ @method validate()
1736
+ @returns {Object} validation errors map
1737
+ @example
1738
+ $('#username, #fullname').editable('validate');
1739
+ // possible result:
1740
+ {
1741
+ username: "username is required",
1742
+ fullname: "fullname should be minimum 3 letters length"
1743
+ }
1744
+ **/
1745
+ case 'validate':
1746
+ this.each(function () {
1747
+ var $this = $(this), data = $this.data(datakey), error;
1748
+ if (data && (error = data.validate())) {
1749
+ result[data.options.name] = error;
1750
+ }
1751
+ });
1752
+ return result;
1753
+
1754
+ /**
1755
+ Returns current values of editable elements. If value is <code>null</code> or <code>undefined</code> it will not be returned
1756
+ @method getValue()
1757
+ @returns {Object} object of element names and values
1758
+ @example
1759
+ $('#username, #fullname').editable('validate');
1760
+ // possible result:
1761
+ {
1762
+ username: "superuser",
1763
+ fullname: "John"
1764
+ }
1765
+ **/
1766
+ case 'getValue':
1767
+ this.each(function () {
1768
+ var $this = $(this), data = $this.data(datakey);
1769
+ if (data && data.value !== undefined && data.value !== null) {
1770
+ result[data.options.name] = data.input.value2submit(data.value);
1771
+ }
1772
+ });
1773
+ return result;
1774
+
1775
+ /**
1776
+ This method collects values from several editable elements and submit them all to server.
1777
+ Internally it runs client-side validation for all fields and submits only in case of success.
1778
+ See <a href="#newrecord">creating new records</a> for details.
1779
+
1780
+ @method submit(options)
1781
+ @param {object} options
1782
+ @param {object} options.url url to submit data
1783
+ @param {object} options.data additional data to submit
1784
+ @param {object} options.ajaxOptions additional ajax options
1785
+ @param {function} options.error(obj) error handler
1786
+ @param {function} options.success(obj,config) success handler
1787
+ @returns {Object} jQuery object
1788
+ **/
1789
+ case 'submit': //collects value, validate and submit to server for creating new record
1790
+ var config = arguments[1] || {},
1791
+ $elems = this,
1792
+ errors = this.editable('validate'),
1793
+ values;
1794
+
1795
+ if($.isEmptyObject(errors)) {
1796
+ values = this.editable('getValue');
1797
+ if(config.data) {
1798
+ $.extend(values, config.data);
1799
+ }
1800
+
1801
+ $.ajax($.extend({
1802
+ url: config.url,
1803
+ data: values,
1804
+ type: 'POST'
1805
+ }, config.ajaxOptions))
1806
+ .success(function(response) {
1807
+ //successful response 200 OK
1808
+ if(typeof config.success === 'function') {
1809
+ config.success.call($elems, response, config);
1810
+ }
1811
+ })
1812
+ .error(function(){ //ajax error
1813
+ if(typeof config.error === 'function') {
1814
+ config.error.apply($elems, arguments);
1815
+ }
1816
+ });
1817
+ } else { //client-side validation error
1818
+ if(typeof config.error === 'function') {
1819
+ config.error.call($elems, errors);
1820
+ }
1821
+ }
1822
+ return this;
1823
+ }
1824
+
1825
+ //return jquery object
1826
+ return this.each(function () {
1827
+ var $this = $(this),
1828
+ data = $this.data(datakey),
1829
+ options = typeof option === 'object' && option;
1830
+
1831
+ if (!data) {
1832
+ $this.data(datakey, (data = new Editable(this, options)));
1833
+ }
1834
+
1835
+ if (typeof option === 'string') { //call method
1836
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
1837
+ }
1838
+ });
1839
+ };
1840
+
1841
+
1842
+ $.fn.editable.defaults = {
1843
+ /**
1844
+ Type of input. Can be <code>text|textarea|select|date|checklist</code> and more
1845
+
1846
+ @property type
1847
+ @type string
1848
+ @default 'text'
1849
+ **/
1850
+ type: 'text',
1851
+ /**
1852
+ Sets disabled state of editable
1853
+
1854
+ @property disabled
1855
+ @type boolean
1856
+ @default false
1857
+ **/
1858
+ disabled: false,
1859
+ /**
1860
+ How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>.
1861
+ When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
1862
+ **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element,
1863
+ you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document.
1864
+
1865
+ @example
1866
+ $('#edit-button').click(function(e) {
1867
+ e.stopPropagation();
1868
+ $('#username').editable('toggle');
1869
+ });
1870
+
1871
+ @property toggle
1872
+ @type string
1873
+ @default 'click'
1874
+ **/
1875
+ toggle: 'click',
1876
+ /**
1877
+ Text shown when element is empty.
1878
+
1879
+ @property emptytext
1880
+ @type string
1881
+ @default 'Empty'
1882
+ **/
1883
+ emptytext: 'Empty',
1884
+ /**
1885
+ Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date.
1886
+ 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>.
1887
+ <code>auto</code> - text will be automatically set only if element is empty.
1888
+ <code>always|never</code> - always(never) try to set element's text.
1889
+
1890
+ @property autotext
1891
+ @type string
1892
+ @default 'auto'
1893
+ **/
1894
+ autotext: 'auto',
1895
+ /**
1896
+ Initial value of input. If not set, taken from element's text.
1897
+
1898
+ @property value
1899
+ @type mixed
1900
+ @default element's text
1901
+ **/
1902
+ value: null,
1903
+ /**
1904
+ Callback to perform custom displaying of value in element's text.
1905
+ If `null`, default input's display used.
1906
+ If `false`, no displaying methods will be called, element's text will never change.
1907
+ Runs under element's scope.
1908
+ _Parameters:_
1909
+
1910
+ * `value` current value to be displayed
1911
+ * `response` server response (if display called after ajax submit), since 1.4.0
1912
+
1913
+ For **inputs with source** (select, checklist) parameters are different:
1914
+
1915
+ * `value` current value to be displayed
1916
+ * `sourceData` array of items for current input (e.g. dropdown items)
1917
+ * `response` server response (if display called after ajax submit), since 1.4.0
1918
+
1919
+ To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`.
1920
+
1921
+ @property display
1922
+ @type function|boolean
1923
+ @default null
1924
+ @since 1.2.0
1925
+ @example
1926
+ display: function(value, sourceData) {
1927
+ //display checklist as comma-separated values
1928
+ var html = [],
1929
+ checked = $.fn.editableutils.itemsByValue(value, sourceData);
1930
+
1931
+ if(checked.length) {
1932
+ $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
1933
+ $(this).html(html.join(', '));
1934
+ } else {
1935
+ $(this).empty();
1936
+ }
1937
+ }
1938
+ **/
1939
+ display: null,
1940
+ /**
1941
+ Css class applied when editable text is empty.
1942
+
1943
+ @property emptyclass
1944
+ @type string
1945
+ @since 1.4.1
1946
+ @default editable-empty
1947
+ **/
1948
+ emptyclass: 'editable-empty',
1949
+ /**
1950
+ Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`).
1951
+ You may set it to `null` if you work with editables locally and submit them together.
1952
+
1953
+ @property unsavedclass
1954
+ @type string
1955
+ @since 1.4.1
1956
+ @default editable-unsaved
1957
+ **/
1958
+ unsavedclass: 'editable-unsaved',
1959
+ /**
1960
+ If a css selector is provided, editable will be delegated to the specified targets.
1961
+ Usefull for dynamically generated DOM elements.
1962
+ **Please note**, that delegated targets can't use `emptytext` and `autotext` options,
1963
+ as they are initialized after first click.
1964
+
1965
+ @property selector
1966
+ @type string
1967
+ @since 1.4.1
1968
+ @default null
1969
+ @example
1970
+ <div id="user">
1971
+ <a href="#" data-name="username" data-type="text" title="Username">awesome</a>
1972
+ <a href="#" data-name="group" data-type="select" data-source="/groups" data-value="1" title="Group">Operator</a>
1973
+ </div>
1974
+
1975
+ <script>
1976
+ $('#user').editable({
1977
+ selector: 'a',
1978
+ url: '/post',
1979
+ pk: 1
1980
+ });
1981
+ </script>
1982
+ **/
1983
+ selector: null
1984
+ };
1985
+
1986
+ }(window.jQuery));
1987
+
1988
+ /**
1989
+ AbstractInput - base class for all editable inputs.
1990
+ It defines interface to be implemented by any input type.
1991
+ To create your own input you can inherit from this class.
1992
+
1993
+ @class abstractinput
1994
+ **/
1995
+ (function ($) {
1996
+
1997
+ //types
1998
+ $.fn.editabletypes = {};
1999
+
2000
+ var AbstractInput = function () { };
2001
+
2002
+ AbstractInput.prototype = {
2003
+ /**
2004
+ Initializes input
2005
+
2006
+ @method init()
2007
+ **/
2008
+ init: function(type, options, defaults) {
2009
+ this.type = type;
2010
+ this.options = $.extend({}, defaults, options);
2011
+ },
2012
+
2013
+ /*
2014
+ this method called before render to init $tpl that is inserted in DOM
2015
+ */
2016
+ prerender: function() {
2017
+ this.$tpl = $(this.options.tpl); //whole tpl as jquery object
2018
+ this.$input = this.$tpl; //control itself, can be changed in render method
2019
+ this.$clear = null; //clear button
2020
+ this.error = null; //error message, if input cannot be rendered
2021
+ },
2022
+
2023
+ /**
2024
+ Renders input from tpl. Can return jQuery deferred object.
2025
+ Can be overwritten in child objects
2026
+
2027
+ @method render()
2028
+ **/
2029
+ render: function() {
2030
+
2031
+ },
2032
+
2033
+ /**
2034
+ Sets element's html by value.
2035
+
2036
+ @method value2html(value, element)
2037
+ @param {mixed} value
2038
+ @param {DOMElement} element
2039
+ **/
2040
+ value2html: function(value, element) {
2041
+ $(element).text(value);
2042
+ },
2043
+
2044
+ /**
2045
+ Converts element's html to value
2046
+
2047
+ @method html2value(html)
2048
+ @param {string} html
2049
+ @returns {mixed}
2050
+ **/
2051
+ html2value: function(html) {
2052
+ return $('<div>').html(html).text();
2053
+ },
2054
+
2055
+ /**
2056
+ Converts value to string (for internal compare). For submitting to server used value2submit().
2057
+
2058
+ @method value2str(value)
2059
+ @param {mixed} value
2060
+ @returns {string}
2061
+ **/
2062
+ value2str: function(value) {
2063
+ return value;
2064
+ },
2065
+
2066
+ /**
2067
+ Converts string received from server into value. Usually from `data-value` attribute.
2068
+
2069
+ @method str2value(str)
2070
+ @param {string} str
2071
+ @returns {mixed}
2072
+ **/
2073
+ str2value: function(str) {
2074
+ return str;
2075
+ },
2076
+
2077
+ /**
2078
+ Converts value for submitting to server. Result can be string or object.
2079
+
2080
+ @method value2submit(value)
2081
+ @param {mixed} value
2082
+ @returns {mixed}
2083
+ **/
2084
+ value2submit: function(value) {
2085
+ return value;
2086
+ },
2087
+
2088
+ /**
2089
+ Sets value of input.
2090
+
2091
+ @method value2input(value)
2092
+ @param {mixed} value
2093
+ **/
2094
+ value2input: function(value) {
2095
+ this.$input.val(value);
2096
+ },
2097
+
2098
+ /**
2099
+ Returns value of input. Value can be object (e.g. datepicker)
2100
+
2101
+ @method input2value()
2102
+ **/
2103
+ input2value: function() {
2104
+ return this.$input.val();
2105
+ },
2106
+
2107
+ /**
2108
+ Activates input. For text it sets focus.
2109
+
2110
+ @method activate()
2111
+ **/
2112
+ activate: function() {
2113
+ if(this.$input.is(':visible')) {
2114
+ this.$input.focus();
2115
+ }
2116
+ },
2117
+
2118
+ /**
2119
+ Creates input.
2120
+
2121
+ @method clear()
2122
+ **/
2123
+ clear: function() {
2124
+ this.$input.val(null);
2125
+ },
2126
+
2127
+ /**
2128
+ method to escape html.
2129
+ **/
2130
+ escape: function(str) {
2131
+ return $('<div>').text(str).html();
2132
+ },
2133
+
2134
+ /**
2135
+ attach handler to automatically submit form when value changed (useful when buttons not shown)
2136
+ **/
2137
+ autosubmit: function() {
2138
+
2139
+ },
2140
+
2141
+ // -------- helper functions --------
2142
+ setClass: function() {
2143
+ if(this.options.inputclass) {
2144
+ this.$input.addClass(this.options.inputclass);
2145
+ }
2146
+ },
2147
+
2148
+ setAttr: function(attr) {
2149
+ if (this.options[attr]) {
2150
+ this.$input.attr(attr, this.options[attr]);
2151
+ }
2152
+ },
2153
+
2154
+ option: function(key, value) {
2155
+ this.options[key] = value;
2156
+ }
2157
+
2158
+ };
2159
+
2160
+ AbstractInput.defaults = {
2161
+ /**
2162
+ HTML template of input. Normally you should not change it.
2163
+
2164
+ @property tpl
2165
+ @type string
2166
+ @default ''
2167
+ **/
2168
+ tpl: '',
2169
+ /**
2170
+ CSS class automatically applied to input
2171
+
2172
+ @property inputclass
2173
+ @type string
2174
+ @default input-medium
2175
+ **/
2176
+ inputclass: 'input-medium'
2177
+ };
2178
+
2179
+ $.extend($.fn.editabletypes, {abstractinput: AbstractInput});
2180
+
2181
+ }(window.jQuery));
2182
+
2183
+ /**
2184
+ List - abstract class for inputs that have source option loaded from js array or via ajax
2185
+
2186
+ @class list
2187
+ @extends abstractinput
2188
+ **/
2189
+ (function ($) {
2190
+
2191
+ var List = function (options) {
2192
+
2193
+ };
2194
+
2195
+ $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);
2196
+
2197
+ $.extend(List.prototype, {
2198
+ render: function () {
2199
+ var deferred = $.Deferred();
2200
+
2201
+ this.error = null;
2202
+ this.onSourceReady(function () {
2203
+ this.renderList();
2204
+ deferred.resolve();
2205
+ }, function () {
2206
+ this.error = this.options.sourceError;
2207
+ deferred.resolve();
2208
+ });
2209
+
2210
+ return deferred.promise();
2211
+ },
2212
+
2213
+ html2value: function (html) {
2214
+ return null; //can't set value by text
2215
+ },
2216
+
2217
+ value2html: function (value, element, display, response) {
2218
+ var deferred = $.Deferred(),
2219
+ success = function () {
2220
+ if(typeof display === 'function') {
2221
+ //custom display method
2222
+ display.call(element, value, this.sourceData, response);
2223
+ } else {
2224
+ this.value2htmlFinal(value, element);
2225
+ }
2226
+ deferred.resolve();
2227
+ };
2228
+
2229
+ //for null value just call success without loading source
2230
+ if(value === null) {
2231
+ success.call(this);
2232
+ } else {
2233
+ this.onSourceReady(success, function () { deferred.resolve(); });
2234
+ }
2235
+
2236
+ return deferred.promise();
2237
+ },
2238
+
2239
+ // ------------- additional functions ------------
2240
+
2241
+ onSourceReady: function (success, error) {
2242
+ //if allready loaded just call success
2243
+ if($.isArray(this.sourceData)) {
2244
+ success.call(this);
2245
+ return;
2246
+ }
2247
+
2248
+ // try parse json in single quotes (for double quotes jquery does automatically)
2249
+ try {
2250
+ this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false);
2251
+ } catch (e) {
2252
+ error.call(this);
2253
+ return;
2254
+ }
2255
+
2256
+ //loading from url
2257
+ if (typeof this.options.source === 'string') {
2258
+ //try to get from cache
2259
+ if(this.options.sourceCache) {
2260
+ var cacheID = this.options.source,
2261
+ cache;
2262
+
2263
+ if (!$(document).data(cacheID)) {
2264
+ $(document).data(cacheID, {});
2265
+ }
2266
+ cache = $(document).data(cacheID);
2267
+
2268
+ //check for cached data
2269
+ if (cache.loading === false && cache.sourceData) { //take source from cache
2270
+ this.sourceData = cache.sourceData;
2271
+ this.doPrepend();
2272
+ success.call(this);
2273
+ return;
2274
+ } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
2275
+ cache.callbacks.push($.proxy(function () {
2276
+ this.sourceData = cache.sourceData;
2277
+ this.doPrepend();
2278
+ success.call(this);
2279
+ }, this));
2280
+
2281
+ //also collecting error callbacks
2282
+ cache.err_callbacks.push($.proxy(error, this));
2283
+ return;
2284
+ } else { //no cache yet, activate it
2285
+ cache.loading = true;
2286
+ cache.callbacks = [];
2287
+ cache.err_callbacks = [];
2288
+ }
2289
+ }
2290
+
2291
+ //loading sourceData from server
2292
+ $.ajax({
2293
+ url: this.options.source,
2294
+ type: 'get',
2295
+ cache: false,
2296
+ dataType: 'json',
2297
+ success: $.proxy(function (data) {
2298
+ if(cache) {
2299
+ cache.loading = false;
2300
+ }
2301
+ this.sourceData = this.makeArray(data);
2302
+ if($.isArray(this.sourceData)) {
2303
+ if(cache) {
2304
+ //store result in cache
2305
+ cache.sourceData = this.sourceData;
2306
+ //run success callbacks for other fields waiting for this source
2307
+ $.each(cache.callbacks, function () { this.call(); });
2308
+ }
2309
+ this.doPrepend();
2310
+ success.call(this);
2311
+ } else {
2312
+ error.call(this);
2313
+ if(cache) {
2314
+ //run error callbacks for other fields waiting for this source
2315
+ $.each(cache.err_callbacks, function () { this.call(); });
2316
+ }
2317
+ }
2318
+ }, this),
2319
+ error: $.proxy(function () {
2320
+ error.call(this);
2321
+ if(cache) {
2322
+ cache.loading = false;
2323
+ //run error callbacks for other fields
2324
+ $.each(cache.err_callbacks, function () { this.call(); });
2325
+ }
2326
+ }, this)
2327
+ });
2328
+ } else { //options as json/array/function
2329
+ if ($.isFunction(this.options.source)) {
2330
+ this.sourceData = this.makeArray(this.options.source());
2331
+ } else {
2332
+ this.sourceData = this.makeArray(this.options.source);
2333
+ }
2334
+
2335
+ if($.isArray(this.sourceData)) {
2336
+ this.doPrepend();
2337
+ success.call(this);
2338
+ } else {
2339
+ error.call(this);
2340
+ }
2341
+ }
2342
+ },
2343
+
2344
+ doPrepend: function () {
2345
+ if(this.options.prepend === null || this.options.prepend === undefined) {
2346
+ return;
2347
+ }
2348
+
2349
+ if(!$.isArray(this.prependData)) {
2350
+ //try parse json in single quotes
2351
+ this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);
2352
+ if (typeof this.options.prepend === 'string') {
2353
+ this.options.prepend = {'': this.options.prepend};
2354
+ }
2355
+ if (typeof this.options.prepend === 'function') {
2356
+ this.prependData = this.makeArray(this.options.prepend());
2357
+ } else {
2358
+ this.prependData = this.makeArray(this.options.prepend);
2359
+ }
2360
+ }
2361
+
2362
+ if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
2363
+ this.sourceData = this.prependData.concat(this.sourceData);
2364
+ }
2365
+ },
2366
+
2367
+ /*
2368
+ renders input list
2369
+ */
2370
+ renderList: function() {
2371
+ // this method should be overwritten in child class
2372
+ },
2373
+
2374
+ /*
2375
+ set element's html by value
2376
+ */
2377
+ value2htmlFinal: function(value, element) {
2378
+ // this method should be overwritten in child class
2379
+ },
2380
+
2381
+ /**
2382
+ * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
2383
+ */
2384
+ makeArray: function(data) {
2385
+ var count, obj, result = [], item, iterateItem;
2386
+ if(!data || typeof data === 'string') {
2387
+ return null;
2388
+ }
2389
+
2390
+ if($.isArray(data)) { //array
2391
+ /*
2392
+ function to iterate inside item of array if item is object.
2393
+ Caclulates count of keys in item and store in obj.
2394
+ */
2395
+ iterateItem = function (k, v) {
2396
+ obj = {value: k, text: v};
2397
+ if(count++ >= 2) {
2398
+ return false;// exit from `each` if item has more than one key.
2399
+ }
2400
+ };
2401
+
2402
+ for(var i = 0; i < data.length; i++) {
2403
+ item = data[i];
2404
+ if(typeof item === 'object') {
2405
+ count = 0; //count of keys inside item
2406
+ $.each(item, iterateItem);
2407
+ //case: [{val1: 'text1'}, {val2: 'text2} ...]
2408
+ if(count === 1) {
2409
+ result.push(obj);
2410
+ //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...]
2411
+ } else if(count > 1) {
2412
+ //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text')
2413
+ if(item.children) {
2414
+ item.children = this.makeArray(item.children);
2415
+ }
2416
+ result.push(item);
2417
+ }
2418
+ } else {
2419
+ //case: ['text1', 'text2' ...]
2420
+ result.push({value: item, text: item});
2421
+ }
2422
+ }
2423
+ } else { //case: {val1: 'text1', val2: 'text2, ...}
2424
+ $.each(data, function (k, v) {
2425
+ result.push({value: k, text: v});
2426
+ });
2427
+ }
2428
+ return result;
2429
+ },
2430
+
2431
+ option: function(key, value) {
2432
+ this.options[key] = value;
2433
+ if(key === 'source') {
2434
+ this.sourceData = null;
2435
+ }
2436
+ if(key === 'prepend') {
2437
+ this.prependData = null;
2438
+ }
2439
+ }
2440
+
2441
+ });
2442
+
2443
+ List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2444
+ /**
2445
+ Source data for list.
2446
+ If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`
2447
+ For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order.
2448
+
2449
+ If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option.
2450
+
2451
+ If **function**, it should return data in format above (since 1.4.0).
2452
+
2453
+ Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only).
2454
+ `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]`
2455
+
2456
+
2457
+ @property source
2458
+ @type string | array | object | function
2459
+ @default null
2460
+ **/
2461
+ source: null,
2462
+ /**
2463
+ Data automatically prepended to the beginning of dropdown list.
2464
+
2465
+ @property prepend
2466
+ @type string | array | object | function
2467
+ @default false
2468
+ **/
2469
+ prepend: false,
2470
+ /**
2471
+ Error message when list cannot be loaded (e.g. ajax error)
2472
+
2473
+ @property sourceError
2474
+ @type string
2475
+ @default Error when loading list
2476
+ **/
2477
+ sourceError: 'Error when loading list',
2478
+ /**
2479
+ if <code>true</code> and source is **string url** - results will be cached for fields with the same source.
2480
+ Usefull for editable column in grid to prevent extra requests.
2481
+
2482
+ @property sourceCache
2483
+ @type boolean
2484
+ @default true
2485
+ @since 1.2.0
2486
+ **/
2487
+ sourceCache: true
2488
+ });
2489
+
2490
+ $.fn.editabletypes.list = List;
2491
+
2492
+ }(window.jQuery));
2493
+
2494
+ /**
2495
+ Text input
2496
+
2497
+ @class text
2498
+ @extends abstractinput
2499
+ @final
2500
+ @example
2501
+ <a href="#" id="username" data-type="text" data-pk="1">awesome</a>
2502
+ <script>
2503
+ $(function(){
2504
+ $('#username').editable({
2505
+ url: '/post',
2506
+ title: 'Enter username'
2507
+ });
2508
+ });
2509
+ </script>
2510
+ **/
2511
+ (function ($) {
2512
+ var Text = function (options) {
2513
+ this.init('text', options, Text.defaults);
2514
+ };
2515
+
2516
+ $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
2517
+
2518
+ $.extend(Text.prototype, {
2519
+ render: function() {
2520
+ this.renderClear();
2521
+ this.setClass();
2522
+ this.setAttr('placeholder');
2523
+ },
2524
+
2525
+ activate: function() {
2526
+ if(this.$input.is(':visible')) {
2527
+ this.$input.focus();
2528
+ $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2529
+ if(this.toggleClear) {
2530
+ this.toggleClear();
2531
+ }
2532
+ }
2533
+ },
2534
+
2535
+ //render clear button
2536
+ renderClear: function() {
2537
+ if (this.options.clear) {
2538
+ this.$clear = $('<span class="editable-clear-x"></span>');
2539
+ this.$input.after(this.$clear)
2540
+ .css('padding-right', 20)
2541
+ .keyup($.proxy(this.toggleClear, this))
2542
+ .parent().css('position', 'relative');
2543
+
2544
+ this.$clear.click($.proxy(this.clear, this));
2545
+ }
2546
+ },
2547
+
2548
+ postrender: function() {
2549
+ if(this.$clear) {
2550
+ //can position clear button only here, when form is shown and height can be calculated
2551
+ var h = this.$input.outerHeight() || 20,
2552
+ delta = (h - this.$clear.height()) / 2;
2553
+
2554
+ //workaround for plain-popup
2555
+ if(delta < 3) {
2556
+ delta = 3;
2557
+ }
2558
+
2559
+ this.$clear.css({top: delta, right: delta});
2560
+ }
2561
+ },
2562
+
2563
+ //show / hide clear button
2564
+ toggleClear: function() {
2565
+ if(!this.$clear) {
2566
+ return;
2567
+ }
2568
+
2569
+ if(this.$input.val().length) {
2570
+ this.$clear.show();
2571
+ } else {
2572
+ this.$clear.hide();
2573
+ }
2574
+ },
2575
+
2576
+ clear: function() {
2577
+ this.$clear.hide();
2578
+ this.$input.val('').focus();
2579
+ }
2580
+ });
2581
+
2582
+ Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2583
+ /**
2584
+ @property tpl
2585
+ @default <input type="text">
2586
+ **/
2587
+ tpl: '<input type="text">',
2588
+ /**
2589
+ Placeholder attribute of input. Shown when input is empty.
2590
+
2591
+ @property placeholder
2592
+ @type string
2593
+ @default null
2594
+ **/
2595
+ placeholder: null,
2596
+
2597
+ /**
2598
+ Whether to show `clear` button
2599
+
2600
+ @property clear
2601
+ @type boolean
2602
+ @default true
2603
+ **/
2604
+ clear: true
2605
+ });
2606
+
2607
+ $.fn.editabletypes.text = Text;
2608
+
2609
+ }(window.jQuery));
2610
+
2611
+ /**
2612
+ Textarea input
2613
+
2614
+ @class textarea
2615
+ @extends abstractinput
2616
+ @final
2617
+ @example
2618
+ <a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a>
2619
+ <script>
2620
+ $(function(){
2621
+ $('#comments').editable({
2622
+ url: '/post',
2623
+ title: 'Enter comments',
2624
+ rows: 10
2625
+ });
2626
+ });
2627
+ </script>
2628
+ **/
2629
+ (function ($) {
2630
+
2631
+ var Textarea = function (options) {
2632
+ this.init('textarea', options, Textarea.defaults);
2633
+ };
2634
+
2635
+ $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput);
2636
+
2637
+ $.extend(Textarea.prototype, {
2638
+ render: function () {
2639
+ this.setClass();
2640
+ this.setAttr('placeholder');
2641
+ this.setAttr('rows');
2642
+
2643
+ //ctrl + enter
2644
+ this.$input.keydown(function (e) {
2645
+ if (e.ctrlKey && e.which === 13) {
2646
+ $(this).closest('form').submit();
2647
+ }
2648
+ });
2649
+ },
2650
+
2651
+ value2html: function(value, element) {
2652
+ var html = '', lines;
2653
+ if(value) {
2654
+ lines = value.split("\n");
2655
+ for (var i = 0; i < lines.length; i++) {
2656
+ lines[i] = $('<div>').text(lines[i]).html();
2657
+ }
2658
+ html = lines.join('<br>');
2659
+ }
2660
+ $(element).html(html);
2661
+ },
2662
+
2663
+ html2value: function(html) {
2664
+ if(!html) {
2665
+ return '';
2666
+ }
2667
+
2668
+ var regex = new RegExp(String.fromCharCode(10), 'g');
2669
+ var lines = html.split(/<br\s*\/?>/i);
2670
+ for (var i = 0; i < lines.length; i++) {
2671
+ var text = $('<div>').html(lines[i]).text();
2672
+
2673
+ // Remove newline characters (\n) to avoid them being converted by value2html() method
2674
+ // thus adding extra <br> tags
2675
+ text = text.replace(regex, '');
2676
+
2677
+ lines[i] = text;
2678
+ }
2679
+ return lines.join("\n");
2680
+ },
2681
+
2682
+ activate: function() {
2683
+ $.fn.editabletypes.text.prototype.activate.call(this);
2684
+ }
2685
+ });
2686
+
2687
+ Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2688
+ /**
2689
+ @property tpl
2690
+ @default <textarea></textarea>
2691
+ **/
2692
+ tpl:'<textarea></textarea>',
2693
+ /**
2694
+ @property inputclass
2695
+ @default input-large
2696
+ **/
2697
+ inputclass: 'input-large',
2698
+ /**
2699
+ Placeholder attribute of input. Shown when input is empty.
2700
+
2701
+ @property placeholder
2702
+ @type string
2703
+ @default null
2704
+ **/
2705
+ placeholder: null,
2706
+ /**
2707
+ Number of rows in textarea
2708
+
2709
+ @property rows
2710
+ @type integer
2711
+ @default 7
2712
+ **/
2713
+ rows: 7
2714
+ });
2715
+
2716
+ $.fn.editabletypes.textarea = Textarea;
2717
+
2718
+ }(window.jQuery));
2719
+
2720
+ /**
2721
+ Select (dropdown)
2722
+
2723
+ @class select
2724
+ @extends list
2725
+ @final
2726
+ @example
2727
+ <a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a>
2728
+ <script>
2729
+ $(function(){
2730
+ $('#status').editable({
2731
+ value: 2,
2732
+ source: [
2733
+ {value: 1, text: 'Active'},
2734
+ {value: 2, text: 'Blocked'},
2735
+ {value: 3, text: 'Deleted'}
2736
+ ]
2737
+ }
2738
+ });
2739
+ });
2740
+ </script>
2741
+ **/
2742
+ (function ($) {
2743
+
2744
+ var Select = function (options) {
2745
+ this.init('select', options, Select.defaults);
2746
+ };
2747
+
2748
+ $.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
2749
+
2750
+ $.extend(Select.prototype, {
2751
+ renderList: function() {
2752
+ this.$input.empty();
2753
+
2754
+ var fillItems = function($el, data) {
2755
+ if($.isArray(data)) {
2756
+ for(var i=0; i<data.length; i++) {
2757
+ if(data[i].children) {
2758
+ $el.append(fillItems($('<optgroup>', {label: data[i].text}), data[i].children));
2759
+ } else {
2760
+ $el.append($('<option>', {value: data[i].value}).text(data[i].text));
2761
+ }
2762
+ }
2763
+ }
2764
+ return $el;
2765
+ };
2766
+
2767
+ fillItems(this.$input, this.sourceData);
2768
+
2769
+ this.setClass();
2770
+
2771
+ //enter submit
2772
+ this.$input.on('keydown.editable', function (e) {
2773
+ if (e.which === 13) {
2774
+ $(this).closest('form').submit();
2775
+ }
2776
+ });
2777
+ },
2778
+
2779
+ value2htmlFinal: function(value, element) {
2780
+ var text = '',
2781
+ items = $.fn.editableutils.itemsByValue(value, this.sourceData);
2782
+
2783
+ if(items.length) {
2784
+ text = items[0].text;
2785
+ }
2786
+
2787
+ $(element).text(text);
2788
+ },
2789
+
2790
+ autosubmit: function() {
2791
+ this.$input.off('keydown.editable').on('change.editable', function(){
2792
+ $(this).closest('form').submit();
2793
+ });
2794
+ }
2795
+ });
2796
+
2797
+ Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
2798
+ /**
2799
+ @property tpl
2800
+ @default <select></select>
2801
+ **/
2802
+ tpl:'<select></select>'
2803
+ });
2804
+
2805
+ $.fn.editabletypes.select = Select;
2806
+
2807
+ }(window.jQuery));
2808
+ /**
2809
+ List of checkboxes.
2810
+ Internally value stored as javascript array of values.
2811
+
2812
+ @class checklist
2813
+ @extends list
2814
+ @final
2815
+ @example
2816
+ <a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a>
2817
+ <script>
2818
+ $(function(){
2819
+ $('#options').editable({
2820
+ value: [2, 3],
2821
+ source: [
2822
+ {value: 1, text: 'option1'},
2823
+ {value: 2, text: 'option2'},
2824
+ {value: 3, text: 'option3'}
2825
+ ]
2826
+ }
2827
+ });
2828
+ });
2829
+ </script>
2830
+ **/
2831
+ (function ($) {
2832
+
2833
+ var Checklist = function (options) {
2834
+ this.init('checklist', options, Checklist.defaults);
2835
+ };
2836
+
2837
+ $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list);
2838
+
2839
+ $.extend(Checklist.prototype, {
2840
+ renderList: function() {
2841
+ var $label, $div;
2842
+
2843
+ this.$tpl.empty();
2844
+
2845
+ if(!$.isArray(this.sourceData)) {
2846
+ return;
2847
+ }
2848
+
2849
+ for(var i=0; i<this.sourceData.length; i++) {
2850
+ $label = $('<label>').append($('<input>', {
2851
+ type: 'checkbox',
2852
+ value: this.sourceData[i].value
2853
+ }))
2854
+ .append($('<span>').text(' '+this.sourceData[i].text));
2855
+
2856
+ $('<div>').append($label).appendTo(this.$tpl);
2857
+ }
2858
+
2859
+ this.$input = this.$tpl.find('input[type="checkbox"]');
2860
+ this.setClass();
2861
+ },
2862
+
2863
+ value2str: function(value) {
2864
+ return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : '';
2865
+ },
2866
+
2867
+ //parse separated string
2868
+ str2value: function(str) {
2869
+ var reg, value = null;
2870
+ if(typeof str === 'string' && str.length) {
2871
+ reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*');
2872
+ value = str.split(reg);
2873
+ } else if($.isArray(str)) {
2874
+ value = str;
2875
+ }
2876
+ return value;
2877
+ },
2878
+
2879
+ //set checked on required checkboxes
2880
+ value2input: function(value) {
2881
+ this.$input.prop('checked', false);
2882
+ if($.isArray(value) && value.length) {
2883
+ this.$input.each(function(i, el) {
2884
+ var $el = $(el);
2885
+ // cannot use $.inArray as it performs strict comparison
2886
+ $.each(value, function(j, val){
2887
+ /*jslint eqeq: true*/
2888
+ if($el.val() == val) {
2889
+ /*jslint eqeq: false*/
2890
+ $el.prop('checked', true);
2891
+ }
2892
+ });
2893
+ });
2894
+ }
2895
+ },
2896
+
2897
+ input2value: function() {
2898
+ var checked = [];
2899
+ this.$input.filter(':checked').each(function(i, el) {
2900
+ checked.push($(el).val());
2901
+ });
2902
+ return checked;
2903
+ },
2904
+
2905
+ //collect text of checked boxes
2906
+ value2htmlFinal: function(value, element) {
2907
+ var html = [],
2908
+ checked = $.fn.editableutils.itemsByValue(value, this.sourceData);
2909
+
2910
+ if(checked.length) {
2911
+ $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
2912
+ $(element).html(html.join('<br>'));
2913
+ } else {
2914
+ $(element).empty();
2915
+ }
2916
+ },
2917
+
2918
+ activate: function() {
2919
+ this.$input.first().focus();
2920
+ },
2921
+
2922
+ autosubmit: function() {
2923
+ this.$input.on('keydown', function(e){
2924
+ if (e.which === 13) {
2925
+ $(this).closest('form').submit();
2926
+ }
2927
+ });
2928
+ }
2929
+ });
2930
+
2931
+ Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
2932
+ /**
2933
+ @property tpl
2934
+ @default <div></div>
2935
+ **/
2936
+ tpl:'<div class="editable-checklist"></div>',
2937
+
2938
+ /**
2939
+ @property inputclass
2940
+ @type string
2941
+ @default null
2942
+ **/
2943
+ inputclass: null,
2944
+
2945
+ /**
2946
+ Separator of values when reading from `data-value` attribute
2947
+
2948
+ @property separator
2949
+ @type string
2950
+ @default ','
2951
+ **/
2952
+ separator: ','
2953
+ });
2954
+
2955
+ $.fn.editabletypes.checklist = Checklist;
2956
+
2957
+ }(window.jQuery));
2958
+
2959
+ /**
2960
+ HTML5 input types.
2961
+ Following types are supported:
2962
+
2963
+ * password
2964
+ * email
2965
+ * url
2966
+ * tel
2967
+ * number
2968
+ * range
2969
+
2970
+ Learn more about html5 inputs:
2971
+ http://www.w3.org/wiki/HTML5_form_additions
2972
+ To check browser compatibility please see:
2973
+ https://developer.mozilla.org/en-US/docs/HTML/Element/Input
2974
+
2975
+ @class html5types
2976
+ @extends text
2977
+ @final
2978
+ @since 1.3.0
2979
+ @example
2980
+ <a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a>
2981
+ <script>
2982
+ $(function(){
2983
+ $('#email').editable({
2984
+ url: '/post',
2985
+ title: 'Enter email'
2986
+ });
2987
+ });
2988
+ </script>
2989
+ **/
2990
+
2991
+ /**
2992
+ @property tpl
2993
+ @default depends on type
2994
+ **/
2995
+
2996
+ /*
2997
+ Password
2998
+ */
2999
+ (function ($) {
3000
+ var Password = function (options) {
3001
+ this.init('password', options, Password.defaults);
3002
+ };
3003
+ $.fn.editableutils.inherit(Password, $.fn.editabletypes.text);
3004
+ $.extend(Password.prototype, {
3005
+ //do not display password, show '[hidden]' instead
3006
+ value2html: function(value, element) {
3007
+ if(value) {
3008
+ $(element).text('[hidden]');
3009
+ } else {
3010
+ $(element).empty();
3011
+ }
3012
+ },
3013
+ //as password not displayed, should not set value by html
3014
+ html2value: function(html) {
3015
+ return null;
3016
+ }
3017
+ });
3018
+ Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3019
+ tpl: '<input type="password">'
3020
+ });
3021
+ $.fn.editabletypes.password = Password;
3022
+ }(window.jQuery));
3023
+
3024
+
3025
+ /*
3026
+ Email
3027
+ */
3028
+ (function ($) {
3029
+ var Email = function (options) {
3030
+ this.init('email', options, Email.defaults);
3031
+ };
3032
+ $.fn.editableutils.inherit(Email, $.fn.editabletypes.text);
3033
+ Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3034
+ tpl: '<input type="email">'
3035
+ });
3036
+ $.fn.editabletypes.email = Email;
3037
+ }(window.jQuery));
3038
+
3039
+
3040
+ /*
3041
+ Url
3042
+ */
3043
+ (function ($) {
3044
+ var Url = function (options) {
3045
+ this.init('url', options, Url.defaults);
3046
+ };
3047
+ $.fn.editableutils.inherit(Url, $.fn.editabletypes.text);
3048
+ Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3049
+ tpl: '<input type="url">'
3050
+ });
3051
+ $.fn.editabletypes.url = Url;
3052
+ }(window.jQuery));
3053
+
3054
+
3055
+ /*
3056
+ Tel
3057
+ */
3058
+ (function ($) {
3059
+ var Tel = function (options) {
3060
+ this.init('tel', options, Tel.defaults);
3061
+ };
3062
+ $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text);
3063
+ Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3064
+ tpl: '<input type="tel">'
3065
+ });
3066
+ $.fn.editabletypes.tel = Tel;
3067
+ }(window.jQuery));
3068
+
3069
+
3070
+ /*
3071
+ Number
3072
+ */
3073
+ (function ($) {
3074
+ var NumberInput = function (options) {
3075
+ this.init('number', options, NumberInput.defaults);
3076
+ };
3077
+ $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text);
3078
+ $.extend(NumberInput.prototype, {
3079
+ render: function () {
3080
+ NumberInput.superclass.render.call(this);
3081
+ this.setAttr('min');
3082
+ this.setAttr('max');
3083
+ this.setAttr('step');
3084
+ }
3085
+ });
3086
+ NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
3087
+ tpl: '<input type="number">',
3088
+ inputclass: 'input-mini',
3089
+ min: null,
3090
+ max: null,
3091
+ step: null
3092
+ });
3093
+ $.fn.editabletypes.number = NumberInput;
3094
+ }(window.jQuery));
3095
+
3096
+
3097
+ /*
3098
+ Range (inherit from number)
3099
+ */
3100
+ (function ($) {
3101
+ var Range = function (options) {
3102
+ this.init('range', options, Range.defaults);
3103
+ };
3104
+ $.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
3105
+ $.extend(Range.prototype, {
3106
+ render: function () {
3107
+ this.$input = this.$tpl.filter('input');
3108
+
3109
+ this.setClass();
3110
+ this.setAttr('min');
3111
+ this.setAttr('max');
3112
+ this.setAttr('step');
3113
+
3114
+ this.$input.on('input', function(){
3115
+ $(this).siblings('output').text($(this).val());
3116
+ });
3117
+ },
3118
+ activate: function() {
3119
+ this.$input.focus();
3120
+ }
3121
+ });
3122
+ Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
3123
+ tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>',
3124
+ inputclass: 'input-medium'
3125
+ });
3126
+ $.fn.editabletypes.range = Range;
3127
+ }(window.jQuery));
3128
+ /**
3129
+ Select2 input. Based on amazing work of Igor Vaynberg https://github.com/ivaynberg/select2.
3130
+ Please see [original docs](http://ivaynberg.github.com/select2) for detailed description and options.
3131
+ You should manually include select2 distributive:
3132
+
3133
+ <link href="select2/select2.css" rel="stylesheet" type="text/css"></link>
3134
+ <script src="select2/select2.js"></script>
3135
+
3136
+ @class select2
3137
+ @extends abstractinput
3138
+ @since 1.4.1
3139
+ @final
3140
+ @example
3141
+ <a href="#" id="country" data-type="select2" data-pk="1" data-value="ru" data-url="/post" data-original-title="Select country"></a>
3142
+ <script>
3143
+ $(function(){
3144
+ $('#country').editable({
3145
+ source: [
3146
+ {id: 'gb', text: 'Great Britain'},
3147
+ {id: 'us', text: 'United States'},
3148
+ {id: 'ru', text: 'Russia'}
3149
+ ],
3150
+ select2: {
3151
+ multiple: true
3152
+ }
3153
+ });
3154
+ });
3155
+ </script>
3156
+ **/
3157
+ (function ($) {
3158
+
3159
+ var Constructor = function (options) {
3160
+ this.init('select2', options, Constructor.defaults);
3161
+
3162
+ options.select2 = options.select2 || {};
3163
+
3164
+ var that = this,
3165
+ mixin = {
3166
+ placeholder: options.placeholder
3167
+ };
3168
+
3169
+ //detect whether it is multi-valued
3170
+ this.isMultiple = options.select2.tags || options.select2.multiple;
3171
+
3172
+ //if not `tags` mode, we need define init set data from source
3173
+ if(!options.select2.tags) {
3174
+ if(options.source) {
3175
+ mixin.data = options.source;
3176
+ }
3177
+
3178
+ //this function can be defaulted in seletc2. See https://github.com/ivaynberg/select2/issues/710
3179
+ mixin.initSelection = function (element, callback) {
3180
+ var val = that.str2value(element.val()),
3181
+ data = $.fn.editableutils.itemsByValue(val, mixin.data, 'id');
3182
+
3183
+ //for single-valued mode should not use array. Take first element instead.
3184
+ if($.isArray(data) && data.length && !that.isMultiple) {
3185
+ data = data[0];
3186
+ }
3187
+
3188
+ callback(data);
3189
+ };
3190
+ }
3191
+
3192
+ //overriding objects in config (as by default jQuery extend() is not recursive)
3193
+ this.options.select2 = $.extend({}, Constructor.defaults.select2, mixin, options.select2);
3194
+ };
3195
+
3196
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
3197
+
3198
+ $.extend(Constructor.prototype, {
3199
+ render: function() {
3200
+ this.setClass();
3201
+ //apply select2
3202
+ this.$input.select2(this.options.select2);
3203
+
3204
+ //trigger resize of editableform to re-position container in multi-valued mode
3205
+ if(this.isMultiple) {
3206
+ this.$input.on('change', function() {
3207
+ $(this).closest('form').parent().triggerHandler('resize');
3208
+ });
3209
+ }
3210
+
3211
+ },
3212
+
3213
+ value2html: function(value, element) {
3214
+ var text = '', data;
3215
+ if(this.$input) { //when submitting form
3216
+ data = this.$input.select2('data');
3217
+ } else { //on init (autotext)
3218
+ //here select2 instance not created yet and data may be even not loaded.
3219
+ //we can check data/tags property of select config and if exist lookup text
3220
+ if(this.options.select2.tags) {
3221
+ data = value;
3222
+ } else if(this.options.select2.data) {
3223
+ data = $.fn.editableutils.itemsByValue(value, this.options.select2.data, 'id');
3224
+ }
3225
+ }
3226
+
3227
+ if($.isArray(data)) {
3228
+ //collect selected data and show with separator
3229
+ text = [];
3230
+ $.each(data, function(k, v){
3231
+ text.push(v && typeof v === 'object' ? v.text : v);
3232
+ });
3233
+ } else if(data) {
3234
+ text = data.text;
3235
+ }
3236
+
3237
+ text = $.isArray(text) ? text.join(this.options.viewseparator) : text;
3238
+
3239
+ $(element).text(text);
3240
+ },
3241
+
3242
+ html2value: function(html) {
3243
+ return this.options.select2.tags ? this.str2value(html, this.options.viewseparator) : null;
3244
+ },
3245
+
3246
+ value2input: function(value) {
3247
+ this.$input.val(value).trigger('change');
3248
+ },
3249
+
3250
+ input2value: function() {
3251
+ return this.$input.select2('val');
3252
+ },
3253
+
3254
+ str2value: function(str, separator) {
3255
+ if(typeof str !== 'string' || !this.isMultiple) {
3256
+ return str;
3257
+ }
3258
+
3259
+ separator = separator || this.options.select2.separator || $.fn.select2.defaults.separator;
3260
+
3261
+ var val, i, l;
3262
+
3263
+ if (str === null || str.length < 1) {
3264
+ return null;
3265
+ }
3266
+ val = str.split(separator);
3267
+ for (i = 0, l = val.length; i < l; i = i + 1) {
3268
+ val[i] = $.trim(val[i]);
3269
+ }
3270
+
3271
+ return val;
3272
+ }
3273
+
3274
+ });
3275
+
3276
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3277
+ /**
3278
+ @property tpl
3279
+ @default <input type="hidden">
3280
+ **/
3281
+ tpl:'<input type="hidden">',
3282
+ /**
3283
+ Configuration of select2. [Full list of options](http://ivaynberg.github.com/select2).
3284
+
3285
+ @property select2
3286
+ @type object
3287
+ @default null
3288
+ **/
3289
+ select2: null,
3290
+ /**
3291
+ Placeholder attribute of select
3292
+
3293
+ @property placeholder
3294
+ @type string
3295
+ @default null
3296
+ **/
3297
+ placeholder: null,
3298
+ /**
3299
+ Source data for select. It will be assigned to select2 `data` property and kept here just for convenience.
3300
+ Please note, that format is different from simple `select` input: use 'id' instead of 'value'.
3301
+ E.g. `[{id: 1, text: "text1"}, {id: 2, text: "text2"}, ...]`.
3302
+
3303
+ @property source
3304
+ @type array
3305
+ @default null
3306
+ **/
3307
+ source: null,
3308
+ /**
3309
+ Separator used to display tags.
3310
+
3311
+ @property viewseparator
3312
+ @type string
3313
+ @default ', '
3314
+ **/
3315
+ viewseparator: ', '
3316
+ });
3317
+
3318
+ $.fn.editabletypes.select2 = Constructor;
3319
+
3320
+ }(window.jQuery));
3321
+ /**
3322
+ * Combodate - 1.0.1
3323
+ * Dropdown date and time picker.
3324
+ * Converts text input into dropdowns to pick day, month, year, hour, minute and second.
3325
+ * Uses momentjs as datetime library http://momentjs.com.
3326
+ * For i18n include corresponding file from https://github.com/timrwood/moment/tree/master/lang
3327
+ *
3328
+ * Author: Vitaliy Potapov
3329
+ * Project page: http://github.com/vitalets/combodate
3330
+ * Copyright (c) 2012 Vitaliy Potapov. Released under MIT License.
3331
+ **/
3332
+ (function ($) {
3333
+
3334
+ var Combodate = function (element, options) {
3335
+ this.$element = $(element);
3336
+ if(!this.$element.is('input')) {
3337
+ $.error('Combodate should be applied to INPUT element');
3338
+ return;
3339
+ }
3340
+ this.options = $.extend({}, $.fn.combodate.defaults, options, this.$element.data());
3341
+ this.init();
3342
+ };
3343
+
3344
+ Combodate.prototype = {
3345
+ constructor: Combodate,
3346
+ init: function () {
3347
+ this.map = {
3348
+ //key regexp moment.method
3349
+ day: ['D', 'date'],
3350
+ month: ['M', 'month'],
3351
+ year: ['Y', 'year'],
3352
+ hour: ['[Hh]', 'hours'],
3353
+ minute: ['m', 'minutes'],
3354
+ second: ['s', 'seconds'],
3355
+ ampm: ['[Aa]', '']
3356
+ };
3357
+
3358
+ this.$widget = $('<span class="combodate"></span>').html(this.getTemplate());
3359
+
3360
+ this.initCombos();
3361
+
3362
+ //update original input on change
3363
+ this.$widget.on('change', 'select', $.proxy(function(){
3364
+ this.$element.val(this.getValue());
3365
+ }, this));
3366
+
3367
+ this.$widget.find('select').css('width', 'auto');
3368
+
3369
+ //hide original input and insert widget
3370
+ this.$element.hide().after(this.$widget);
3371
+
3372
+ //set initial value
3373
+ this.setValue(this.$element.val() || this.options.value);
3374
+ },
3375
+
3376
+ /*
3377
+ Replace tokens in template with <select> elements
3378
+ */
3379
+ getTemplate: function() {
3380
+ var tpl = this.options.template;
3381
+
3382
+ //first pass
3383
+ $.each(this.map, function(k, v) {
3384
+ v = v[0];
3385
+ var r = new RegExp(v+'+'),
3386
+ token = v.length > 1 ? v.substring(1, 2) : v;
3387
+
3388
+ tpl = tpl.replace(r, '{'+token+'}');
3389
+ });
3390
+
3391
+ //replace spaces with &nbsp;
3392
+ tpl = tpl.replace(/ /g, '&nbsp;');
3393
+
3394
+ //second pass
3395
+ $.each(this.map, function(k, v) {
3396
+ v = v[0];
3397
+ var token = v.length > 1 ? v.substring(1, 2) : v;
3398
+
3399
+ tpl = tpl.replace('{'+token+'}', '<select class="'+k+'"></select>');
3400
+ });
3401
+
3402
+ return tpl;
3403
+ },
3404
+
3405
+ /*
3406
+ Initialize combos that presents in template
3407
+ */
3408
+ initCombos: function() {
3409
+ var that = this;
3410
+ $.each(this.map, function(k, v) {
3411
+ var $c = that.$widget.find('.'+k), f, items;
3412
+ if($c.length) {
3413
+ that['$'+k] = $c; //set properties like this.$day, this.$month etc.
3414
+ f = 'fill' + k.charAt(0).toUpperCase() + k.slice(1); //define method name to fill items, e.g `fillDays`
3415
+ items = that[f]();
3416
+ that['$'+k].html(that.renderItems(items));
3417
+ }
3418
+ });
3419
+ },
3420
+
3421
+ /*
3422
+ Initialize items of combos. Handles `firstItem` option
3423
+ */
3424
+ initItems: function(key) {
3425
+ var values = [];
3426
+ if(this.options.firstItem === 'name') {
3427
+ var header = typeof moment.relativeTime[key] === 'function' ? moment.relativeTime[key](1, true, key, false) : moment.relativeTime[key];
3428
+ //take last entry (see momentjs lang files structure)
3429
+ header = header.split(' ').reverse()[0];
3430
+ values.push(['', header]);
3431
+ } else if(this.options.firstItem === 'empty') {
3432
+ values.push(['', '']);
3433
+ }
3434
+ return values;
3435
+ },
3436
+
3437
+ /*
3438
+ render items to string of <option> tags
3439
+ */
3440
+ renderItems: function(items) {
3441
+ var str = [];
3442
+ for(var i=0; i<items.length; i++) {
3443
+ str.push('<option value="'+items[i][0]+'">'+items[i][1]+'</option>');
3444
+ }
3445
+ return str.join("\n");
3446
+ },
3447
+
3448
+ /*
3449
+ fill day
3450
+ */
3451
+ fillDay: function() {
3452
+ var items = this.initItems('d'), name, i,
3453
+ twoDigit = this.options.template.indexOf('DD') !== -1;
3454
+
3455
+ for(i=1; i<=31; i++) {
3456
+ name = twoDigit ? this.leadZero(i) : i;
3457
+ items.push([i, name]);
3458
+ }
3459
+ return items;
3460
+ },
3461
+
3462
+ /*
3463
+ fill month
3464
+ */
3465
+ fillMonth: function() {
3466
+ var items = this.initItems('M'), name, i,
3467
+ longNames = this.options.template.indexOf('MMMM') !== -1,
3468
+ shortNames = this.options.template.indexOf('MMM') !== -1,
3469
+ twoDigit = this.options.template.indexOf('MM') !== -1;
3470
+
3471
+ for(i=0; i<=11; i++) {
3472
+ if(longNames) {
3473
+ name = moment.months[i];
3474
+ } else if(shortNames) {
3475
+ name = moment.monthsShort[i];
3476
+ } else if(twoDigit) {
3477
+ name = this.leadZero(i+1);
3478
+ } else {
3479
+ name = i+1;
3480
+ }
3481
+ items.push([i, name]);
3482
+ }
3483
+ return items;
3484
+ },
3485
+
3486
+ /*
3487
+ fill year
3488
+ */
3489
+ fillYear: function() {
3490
+ var items = this.initItems('y'), name, i,
3491
+ longNames = this.options.template.indexOf('YYYY') !== -1;
3492
+
3493
+ for(i=this.options.maxYear; i>=this.options.minYear; i--) {
3494
+ name = longNames ? i : (i+'').substring(2);
3495
+ items.push([i, name]);
3496
+ }
3497
+ return items;
3498
+ },
3499
+
3500
+ /*
3501
+ fill hour
3502
+ */
3503
+ fillHour: function() {
3504
+ var items = this.initItems('h'), name, i,
3505
+ h12 = this.options.template.indexOf('h') !== -1,
3506
+ h24 = this.options.template.indexOf('H') !== -1,
3507
+ twoDigit = this.options.template.toLowerCase().indexOf('hh') !== -1,
3508
+ max = h12 ? 12 : 23;
3509
+
3510
+ for(i=0; i<=max; i++) {
3511
+ name = twoDigit ? this.leadZero(i) : i;
3512
+ items.push([i, name]);
3513
+ }
3514
+ return items;
3515
+ },
3516
+
3517
+ /*
3518
+ fill minute
3519
+ */
3520
+ fillMinute: function() {
3521
+ var items = this.initItems('m'), name, i,
3522
+ twoDigit = this.options.template.indexOf('mm') !== -1;
3523
+
3524
+ for(i=0; i<=59; i+= this.options.minuteStep) {
3525
+ name = twoDigit ? this.leadZero(i) : i;
3526
+ items.push([i, name]);
3527
+ }
3528
+ return items;
3529
+ },
3530
+
3531
+ /*
3532
+ fill second
3533
+ */
3534
+ fillSecond: function() {
3535
+ var items = this.initItems('s'), name, i,
3536
+ twoDigit = this.options.template.indexOf('ss') !== -1;
3537
+
3538
+ for(i=0; i<=59; i+= this.options.secondStep) {
3539
+ name = twoDigit ? this.leadZero(i) : i;
3540
+ items.push([i, name]);
3541
+ }
3542
+ return items;
3543
+ },
3544
+
3545
+ /*
3546
+ fill ampm
3547
+ */
3548
+ fillAmpm: function() {
3549
+ var ampmL = this.options.template.indexOf('a') !== -1,
3550
+ ampmU = this.options.template.indexOf('A') !== -1,
3551
+ items = [
3552
+ ['am', ampmL ? 'am' : 'AM'],
3553
+ ['pm', ampmL ? 'pm' : 'PM']
3554
+ ];
3555
+ return items;
3556
+ },
3557
+
3558
+ /*
3559
+ Returns current date value.
3560
+ If format not specified - `options.format` used.
3561
+ If format = `null` - Moment object returned.
3562
+ */
3563
+ getValue: function(format) {
3564
+ var dt, values = {},
3565
+ that = this,
3566
+ notSelected = false;
3567
+
3568
+ //getting selected values
3569
+ $.each(this.map, function(k, v) {
3570
+ if(k === 'ampm') {
3571
+ return;
3572
+ }
3573
+ var def = k === 'day' ? 1 : 0;
3574
+
3575
+ values[k] = that['$'+k] ? parseInt(that['$'+k].val(), 10) : def;
3576
+
3577
+ if(isNaN(values[k])) {
3578
+ notSelected = true;
3579
+ return false;
3580
+ }
3581
+ });
3582
+
3583
+ //if at least one visible combo not selected - return empty string
3584
+ if(notSelected) {
3585
+ return '';
3586
+ }
3587
+
3588
+ //convert hours if 12h format
3589
+ if(this.$ampm) {
3590
+ values.hour = this.$ampm.val() === 'am' ? values.hour : values.hour+12;
3591
+ if(values.hour === 24) {
3592
+ values.hour = 0;
3593
+ }
3594
+ }
3595
+
3596
+ dt = moment([values.year, values.month, values.day, values.hour, values.minute, values.second]);
3597
+
3598
+ //highlight invalid date
3599
+ this.highlight(dt);
3600
+
3601
+ format = format === undefined ? this.options.format : format;
3602
+ if(format === null) {
3603
+ return dt.isValid() ? dt : null;
3604
+ } else {
3605
+ return dt.isValid() ? dt.format(format) : '';
3606
+ }
3607
+ },
3608
+
3609
+ setValue: function(value) {
3610
+ if(!value) {
3611
+ return;
3612
+ }
3613
+
3614
+ var dt = typeof value === 'string' ? moment(value, this.options.format) : moment(value),
3615
+ that = this,
3616
+ values = {};
3617
+
3618
+ if(dt.isValid()) {
3619
+ //read values from date object
3620
+ $.each(this.map, function(k, v) {
3621
+ if(k === 'ampm') {
3622
+ return;
3623
+ }
3624
+ values[k] = dt[v[1]]();
3625
+ });
3626
+
3627
+ if(this.$ampm) {
3628
+ if(values.hour > 12) {
3629
+ values.hour -= 12;
3630
+ values.ampm = 'pm';
3631
+ } else {
3632
+ values.ampm = 'am';
3633
+ }
3634
+ }
3635
+
3636
+ $.each(values, function(k, v) {
3637
+ if(that['$'+k]) {
3638
+ that['$'+k].val(v);
3639
+ }
3640
+ });
3641
+
3642
+ this.$element.val(dt.format(this.options.format));
3643
+ }
3644
+ },
3645
+
3646
+ /*
3647
+ highlight combos if date is invalid
3648
+ */
3649
+ highlight: function(dt) {
3650
+ if(!dt.isValid()) {
3651
+ if(this.options.errorClass) {
3652
+ this.$widget.addClass(this.options.errorClass);
3653
+ } else {
3654
+ //store original border color
3655
+ if(!this.borderColor) {
3656
+ this.borderColor = this.$widget.find('select').css('border-color');
3657
+ }
3658
+ this.$widget.find('select').css('border-color', 'red');
3659
+ }
3660
+ } else {
3661
+ if(this.options.errorClass) {
3662
+ this.$widget.removeClass(this.options.errorClass);
3663
+ } else {
3664
+ this.$widget.find('select').css('border-color', this.borderColor);
3665
+ }
3666
+ }
3667
+ },
3668
+
3669
+ leadZero: function(v) {
3670
+ return v <= 9 ? '0' + v : v;
3671
+ },
3672
+
3673
+ destroy: function() {
3674
+ this.$widget.remove();
3675
+ this.$element.removeData('combodate').show();
3676
+ }
3677
+
3678
+ //todo: clear method
3679
+ };
3680
+
3681
+ $.fn.combodate = function ( option ) {
3682
+ var d, args = Array.apply(null, arguments);
3683
+ args.shift();
3684
+
3685
+ //getValue returns date as string / object (not jQuery object)
3686
+ if(option === 'getValue' && this.length && (d = this.eq(0).data('combodate'))) {
3687
+ return d.getValue.apply(d, args);
3688
+ }
3689
+
3690
+ return this.each(function () {
3691
+ var $this = $(this),
3692
+ data = $this.data('combodate'),
3693
+ options = typeof option == 'object' && option;
3694
+ if (!data) {
3695
+ $this.data('combodate', (data = new Combodate(this, options)));
3696
+ }
3697
+ if (typeof option == 'string' && typeof data[option] == 'function') {
3698
+ data[option].apply(data, args);
3699
+ }
3700
+ });
3701
+ };
3702
+
3703
+ $.fn.combodate.defaults = {
3704
+ //in this format value stored in original input
3705
+ format: 'DD-MM-YYYY HH:mm',
3706
+ //in this format items in dropdowns are displayed
3707
+ template: 'D / MMM / YYYY H : mm',
3708
+ //initial value, can be `new Date()`
3709
+ value: null,
3710
+ minYear: 1970,
3711
+ maxYear: 2015,
3712
+ minuteStep: 5,
3713
+ secondStep: 1,
3714
+ firstItem: 'empty', //'name', 'empty', 'none'
3715
+ errorClass: null
3716
+ };
3717
+
3718
+ }(window.jQuery));
3719
+ /**
3720
+ Combodate input - dropdown date and time picker.
3721
+ Based on [combodate](http://vitalets.github.com/combodate) plugin. To use it you should manually include [momentjs](http://momentjs.com).
3722
+
3723
+ <script src="js/moment.min.js"></script>
3724
+
3725
+ Allows to input:
3726
+
3727
+ * only date
3728
+ * only time
3729
+ * both date and time
3730
+
3731
+ Please note, that format is taken from momentjs and **not compatible** with bootstrap-datepicker / jquery UI datepicker.
3732
+ Internally value stored as `momentjs` object.
3733
+
3734
+ @class combodate
3735
+ @extends abstractinput
3736
+ @final
3737
+ @since 1.4.0
3738
+ @example
3739
+ <a href="#" id="dob" data-type="combodate" data-pk="1" data-url="/post" data-value="1984-05-15" data-original-title="Select date"></a>
3740
+ <script>
3741
+ $(function(){
3742
+ $('#dob').editable({
3743
+ format: 'YYYY-MM-DD',
3744
+ viewformat: 'DD.MM.YYYY',
3745
+ template: 'D / MMMM / YYYY',
3746
+ combodate: {
3747
+ minYear: 2000,
3748
+ maxYear: 2015,
3749
+ minuteStep: 1
3750
+ }
3751
+ }
3752
+ });
3753
+ });
3754
+ </script>
3755
+ **/
3756
+
3757
+ /*global moment*/
3758
+
3759
+ (function ($) {
3760
+
3761
+ var Constructor = function (options) {
3762
+ this.init('combodate', options, Constructor.defaults);
3763
+
3764
+ //by default viewformat equals to format
3765
+ if(!this.options.viewformat) {
3766
+ this.options.viewformat = this.options.format;
3767
+ }
3768
+
3769
+ //overriding combodate config (as by default jQuery extend() is not recursive)
3770
+ this.options.combodate = $.extend({}, Constructor.defaults.combodate, options.combodate, {
3771
+ format: this.options.format,
3772
+ template: this.options.template
3773
+ });
3774
+ };
3775
+
3776
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.abstractinput);
3777
+
3778
+ $.extend(Constructor.prototype, {
3779
+ render: function () {
3780
+ this.$input.combodate(this.options.combodate);
3781
+
3782
+ //"clear" link
3783
+ /*
3784
+ if(this.options.clear) {
3785
+ this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
3786
+ e.preventDefault();
3787
+ e.stopPropagation();
3788
+ this.clear();
3789
+ }, this));
3790
+
3791
+ this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
3792
+ }
3793
+ */
3794
+ },
3795
+
3796
+ value2html: function(value, element) {
3797
+ var text = value ? value.format(this.options.viewformat) : '';
3798
+ $(element).text(text);
3799
+ },
3800
+
3801
+ html2value: function(html) {
3802
+ return html ? moment(html, this.options.viewformat) : null;
3803
+ },
3804
+
3805
+ value2str: function(value) {
3806
+ return value ? value.format(this.options.format) : '';
3807
+ },
3808
+
3809
+ str2value: function(str) {
3810
+ return str ? moment(str, this.options.format) : null;
3811
+ },
3812
+
3813
+ value2submit: function(value) {
3814
+ return this.value2str(value);
3815
+ },
3816
+
3817
+ value2input: function(value) {
3818
+ this.$input.combodate('setValue', value);
3819
+ },
3820
+
3821
+ input2value: function() {
3822
+ return this.$input.combodate('getValue', null);
3823
+ },
3824
+
3825
+ activate: function() {
3826
+ this.$input.siblings('.combodate').find('select').eq(0).focus();
3827
+ },
3828
+
3829
+ /*
3830
+ clear: function() {
3831
+ this.$input.data('datepicker').date = null;
3832
+ this.$input.find('.active').removeClass('active');
3833
+ },
3834
+ */
3835
+
3836
+ autosubmit: function() {
3837
+
3838
+ }
3839
+
3840
+ });
3841
+
3842
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
3843
+ /**
3844
+ @property tpl
3845
+ @default <input type="text">
3846
+ **/
3847
+ tpl:'<input type="text">',
3848
+ /**
3849
+ @property inputclass
3850
+ @default null
3851
+ **/
3852
+ inputclass: null,
3853
+ /**
3854
+ Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
3855
+ See list of tokens in [momentjs docs](http://momentjs.com/docs/#/parsing/string-format)
3856
+
3857
+ @property format
3858
+ @type string
3859
+ @default YYYY-MM-DD
3860
+ **/
3861
+ format:'YYYY-MM-DD',
3862
+ /**
3863
+ Format used for displaying date. Also applied when converting date from element's text on init.
3864
+ If not specified equals to `format`.
3865
+
3866
+ @property viewformat
3867
+ @type string
3868
+ @default null
3869
+ **/
3870
+ viewformat: null,
3871
+ /**
3872
+ Template used for displaying dropdowns.
3873
+
3874
+ @property template
3875
+ @type string
3876
+ @default D / MMM / YYYY
3877
+ **/
3878
+ template: 'D / MMM / YYYY',
3879
+ /**
3880
+ Configuration of combodate.
3881
+ Full list of options: http://vitalets.github.com/combodate/#docs
3882
+
3883
+ @property combodate
3884
+ @type object
3885
+ @default null
3886
+ **/
3887
+ combodate: null
3888
+
3889
+ /*
3890
+ (not implemented yet)
3891
+ Text shown as clear date button.
3892
+ If <code>false</code> clear button will not be rendered.
3893
+
3894
+ @property clear
3895
+ @type boolean|string
3896
+ @default 'x clear'
3897
+ */
3898
+ //clear: '&times; clear'
3899
+ });
3900
+
3901
+ $.fn.editabletypes.combodate = Constructor;
3902
+
3903
+ }(window.jQuery));
3904
+
3905
+ /*
3906
+ Editableform based on Twitter Bootstrap
3907
+ */
3908
+ (function ($) {
3909
+
3910
+ $.extend($.fn.editableform.Constructor.prototype, {
3911
+ initTemplate: function() {
3912
+ this.$form = $($.fn.editableform.template);
3913
+ this.$form.find('.editable-error-block').addClass('help-block');
3914
+ }
3915
+ });
3916
+
3917
+ //buttons
3918
+ $.fn.editableform.buttons = '<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button>'+
3919
+ '<button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>';
3920
+
3921
+ //error classes
3922
+ $.fn.editableform.errorGroupClass = 'error';
3923
+ $.fn.editableform.errorBlockClass = null;
3924
+
3925
+ }(window.jQuery));
3926
+ /**
3927
+ * Editable Popover
3928
+ * ---------------------
3929
+ * requires bootstrap-popover.js
3930
+ */
3931
+ (function ($) {
3932
+
3933
+ //extend methods
3934
+ $.extend($.fn.editableContainer.Popup.prototype, {
3935
+ containerName: 'popover',
3936
+ //for compatibility with bootstrap <= 2.2.1 (content inserted into <p> instead of directly .popover-content)
3937
+ innerCss: $($.fn.popover.defaults.template).find('p').length ? '.popover-content p' : '.popover-content',
3938
+
3939
+ initContainer: function(){
3940
+ $.extend(this.containerOptions, {
3941
+ trigger: 'manual',
3942
+ selector: false,
3943
+ content: ' ',
3944
+ template: $.fn.popover.defaults.template
3945
+ });
3946
+
3947
+ //as template property is used in inputs, hide it from popover
3948
+ var t;
3949
+ if(this.$element.data('template')) {
3950
+ t = this.$element.data('template');
3951
+ this.$element.removeData('template');
3952
+ }
3953
+
3954
+ this.call(this.containerOptions);
3955
+
3956
+ if(t) {
3957
+ //restore data('template')
3958
+ this.$element.data('template', t);
3959
+ }
3960
+ },
3961
+
3962
+ /* show */
3963
+ innerShow: function () {
3964
+ this.call('show');
3965
+ },
3966
+
3967
+ /* hide */
3968
+ innerHide: function () {
3969
+ this.call('hide');
3970
+ },
3971
+
3972
+ /* destroy */
3973
+ innerDestroy: function() {
3974
+ this.call('destroy');
3975
+ },
3976
+
3977
+ setContainerOption: function(key, value) {
3978
+ this.container().options[key] = value;
3979
+ },
3980
+
3981
+ /**
3982
+ * move popover to new position. This function mainly copied from bootstrap-popover.
3983
+ */
3984
+ /*jshint laxcomma: true*/
3985
+ setPosition: function () {
3986
+
3987
+ (function() {
3988
+ var $tip = this.tip()
3989
+ , inside
3990
+ , pos
3991
+ , actualWidth
3992
+ , actualHeight
3993
+ , placement
3994
+ , tp;
3995
+
3996
+ placement = typeof this.options.placement === 'function' ?
3997
+ this.options.placement.call(this, $tip[0], this.$element[0]) :
3998
+ this.options.placement;
3999
+
4000
+ inside = /in/.test(placement);
4001
+
4002
+ $tip
4003
+ // .detach()
4004
+ //vitalets: remove any placement class because otherwise they dont influence on re-positioning of visible popover
4005
+ .removeClass('top right bottom left')
4006
+ .css({ top: 0, left: 0, display: 'block' });
4007
+ // .insertAfter(this.$element);
4008
+
4009
+ pos = this.getPosition(inside);
4010
+
4011
+ actualWidth = $tip[0].offsetWidth;
4012
+ actualHeight = $tip[0].offsetHeight;
4013
+
4014
+ switch (inside ? placement.split(' ')[1] : placement) {
4015
+ case 'bottom':
4016
+ tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2};
4017
+ break;
4018
+ case 'top':
4019
+ tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2};
4020
+ break;
4021
+ case 'left':
4022
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth};
4023
+ break;
4024
+ case 'right':
4025
+ tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width};
4026
+ break;
4027
+ }
4028
+
4029
+ $tip
4030
+ .offset(tp)
4031
+ .addClass(placement)
4032
+ .addClass('in');
4033
+
4034
+ }).call(this.container());
4035
+ /*jshint laxcomma: false*/
4036
+ }
4037
+ });
4038
+
4039
+ }(window.jQuery));
4040
+ /**
4041
+ Bootstrap-datepicker.
4042
+ Description and examples: https://github.com/eternicode/bootstrap-datepicker.
4043
+ For **i18n** you should include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
4044
+ and set `language` option.
4045
+ Since 1.4.0 date has different appearance in **popup** and **inline** modes.
4046
+
4047
+ @class date
4048
+ @extends abstractinput
4049
+ @final
4050
+ @example
4051
+ <a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a>
4052
+ <script>
4053
+ $(function(){
4054
+ $('#dob').editable({
4055
+ format: 'yyyy-mm-dd',
4056
+ viewformat: 'dd/mm/yyyy',
4057
+ datepicker: {
4058
+ weekStart: 1
4059
+ }
4060
+ }
4061
+ });
4062
+ });
4063
+ </script>
4064
+ **/
4065
+ (function ($) {
4066
+
4067
+ var Date = function (options) {
4068
+ this.init('date', options, Date.defaults);
4069
+ this.initPicker(options, Date.defaults);
4070
+ };
4071
+
4072
+ $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput);
4073
+
4074
+ $.extend(Date.prototype, {
4075
+ initPicker: function(options, defaults) {
4076
+ //'format' is set directly from settings or data-* attributes
4077
+
4078
+ //by default viewformat equals to format
4079
+ if(!this.options.viewformat) {
4080
+ this.options.viewformat = this.options.format;
4081
+ }
4082
+
4083
+ //overriding datepicker config (as by default jQuery extend() is not recursive)
4084
+ //since 1.4 datepicker internally uses viewformat instead of format. Format is for submit only
4085
+ this.options.datepicker = $.extend({}, defaults.datepicker, options.datepicker, {
4086
+ format: this.options.viewformat
4087
+ });
4088
+
4089
+ //language
4090
+ this.options.datepicker.language = this.options.datepicker.language || 'en';
4091
+
4092
+ //store DPglobal
4093
+ this.dpg = $.fn.datepicker.DPGlobal;
4094
+
4095
+ //store parsed formats
4096
+ this.parsedFormat = this.dpg.parseFormat(this.options.format);
4097
+ this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
4098
+ },
4099
+
4100
+ render: function () {
4101
+ this.$input.datepicker(this.options.datepicker);
4102
+
4103
+ //"clear" link
4104
+ if(this.options.clear) {
4105
+ this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
4106
+ e.preventDefault();
4107
+ e.stopPropagation();
4108
+ this.clear();
4109
+ }, this));
4110
+
4111
+ this.$tpl.parent().append($('<div class="editable-clear">').append(this.$clear));
4112
+ }
4113
+ },
4114
+
4115
+ value2html: function(value, element) {
4116
+ var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
4117
+ Date.superclass.value2html(text, element);
4118
+ },
4119
+
4120
+ html2value: function(html) {
4121
+ return html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datepicker.language) : null;
4122
+ },
4123
+
4124
+ value2str: function(value) {
4125
+ return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : '';
4126
+ },
4127
+
4128
+ str2value: function(str) {
4129
+ return str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datepicker.language) : null;
4130
+ },
4131
+
4132
+ value2submit: function(value) {
4133
+ return this.value2str(value);
4134
+ },
4135
+
4136
+ value2input: function(value) {
4137
+ this.$input.datepicker('update', value);
4138
+ },
4139
+
4140
+ input2value: function() {
4141
+ return this.$input.data('datepicker').date;
4142
+ },
4143
+
4144
+ activate: function() {
4145
+ },
4146
+
4147
+ clear: function() {
4148
+ this.$input.data('datepicker').date = null;
4149
+ this.$input.find('.active').removeClass('active');
4150
+ },
4151
+
4152
+ autosubmit: function() {
4153
+ this.$input.on('changeDate', function(e){
4154
+ var $form = $(this).closest('form');
4155
+ setTimeout(function() {
4156
+ $form.submit();
4157
+ }, 200);
4158
+ });
4159
+ }
4160
+
4161
+ });
4162
+
4163
+ Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
4164
+ /**
4165
+ @property tpl
4166
+ @default <div></div>
4167
+ **/
4168
+ tpl:'<div class="editable-date well"></div>',
4169
+ /**
4170
+ @property inputclass
4171
+ @default null
4172
+ **/
4173
+ inputclass: null,
4174
+ /**
4175
+ Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
4176
+ Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code>
4177
+
4178
+ @property format
4179
+ @type string
4180
+ @default yyyy-mm-dd
4181
+ **/
4182
+ format:'yyyy-mm-dd',
4183
+ /**
4184
+ Format used for displaying date. Also applied when converting date from element's text on init.
4185
+ If not specified equals to <code>format</code>
4186
+
4187
+ @property viewformat
4188
+ @type string
4189
+ @default null
4190
+ **/
4191
+ viewformat: null,
4192
+ /**
4193
+ Configuration of datepicker.
4194
+ Full list of options: http://vitalets.github.com/bootstrap-datepicker
4195
+
4196
+ @property datepicker
4197
+ @type object
4198
+ @default {
4199
+ weekStart: 0,
4200
+ startView: 0,
4201
+ autoclose: false
4202
+ }
4203
+ **/
4204
+ datepicker:{
4205
+ weekStart: 0,
4206
+ startView: 0,
4207
+ autoclose: false
4208
+ },
4209
+ /**
4210
+ Text shown as clear date button.
4211
+ If <code>false</code> clear button will not be rendered.
4212
+
4213
+ @property clear
4214
+ @type boolean|string
4215
+ @default 'x clear'
4216
+ **/
4217
+ clear: '&times; clear'
4218
+ });
4219
+
4220
+ $.fn.editabletypes.date = Date;
4221
+
4222
+ }(window.jQuery));
4223
+
4224
+ /**
4225
+ Bootstrap datefield input - modification for inline mode.
4226
+ Shows normal <input type="text"> and binds popup datepicker.
4227
+ Automatically shown in inline mode.
4228
+
4229
+ @class datefield
4230
+ @extends date
4231
+
4232
+ @since 1.4.0
4233
+ **/
4234
+ (function ($) {
4235
+
4236
+ var DateField = function (options) {
4237
+ this.init('datefield', options, DateField.defaults);
4238
+ this.initPicker(options, DateField.defaults);
4239
+ };
4240
+
4241
+ $.fn.editableutils.inherit(DateField, $.fn.editabletypes.date);
4242
+
4243
+ $.extend(DateField.prototype, {
4244
+ render: function () {
4245
+ this.$input = this.$tpl.find('input');
4246
+ this.setClass();
4247
+ this.setAttr('placeholder');
4248
+
4249
+ this.$tpl.datepicker(this.options.datepicker);
4250
+
4251
+ //need to disable original event handlers
4252
+ this.$input.off('focus keydown');
4253
+
4254
+ //update value of datepicker
4255
+ this.$input.keyup($.proxy(function(){
4256
+ this.$tpl.removeData('date');
4257
+ this.$tpl.datepicker('update');
4258
+ }, this));
4259
+
4260
+ },
4261
+
4262
+ value2input: function(value) {
4263
+ this.$input.val(value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '');
4264
+ this.$tpl.datepicker('update');
4265
+ },
4266
+
4267
+ input2value: function() {
4268
+ return this.html2value(this.$input.val());
4269
+ },
4270
+
4271
+ activate: function() {
4272
+ $.fn.editabletypes.text.prototype.activate.call(this);
4273
+ },
4274
+
4275
+ autosubmit: function() {
4276
+ //reset autosubmit to empty
4277
+ }
4278
+ });
4279
+
4280
+ DateField.defaults = $.extend({}, $.fn.editabletypes.date.defaults, {
4281
+ /**
4282
+ @property tpl
4283
+ **/
4284
+ tpl:'<div class="input-append date"><input type="text"/><span class="add-on"><i class="icon-th"></i></span></div>',
4285
+ /**
4286
+ @property inputclass
4287
+ @default 'input-small'
4288
+ **/
4289
+ inputclass: 'input-small',
4290
+
4291
+ /* datepicker config */
4292
+ datepicker: {
4293
+ weekStart: 0,
4294
+ startView: 0,
4295
+ autoclose: true
4296
+ }
4297
+ });
4298
+
4299
+ $.fn.editabletypes.datefield = DateField;
4300
+
4301
+ }(window.jQuery));
4302
+
4303
+ /**
4304
+ Typeahead input (bootstrap only). Based on Twitter Bootstrap [typeahead](http://twitter.github.com/bootstrap/javascript.html#typeahead).
4305
+ Depending on `source` format typeahead operates in two modes:
4306
+
4307
+ * **strings**:
4308
+ When `source` defined as array of strings, e.g. `['text1', 'text2', 'text3' ...]`.
4309
+ User can submit one of these strings or any text entered in input (even if it is not matching source).
4310
+
4311
+ * **objects**:
4312
+ When `source` defined as array of objects, e.g. `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]`.
4313
+ User can submit only values that are in source (otherwise `null` is submitted). This is more like *dropdown* behavior.
4314
+
4315
+ @class typeahead
4316
+ @extends list
4317
+ @since 1.4.1
4318
+ @final
4319
+ @example
4320
+ <a href="#" id="country" data-type="typeahead" data-pk="1" data-url="/post" data-original-title="Input country"></a>
4321
+ <script>
4322
+ $(function(){
4323
+ $('#country').editable({
4324
+ value: 'ru',
4325
+ source: [
4326
+ {value: 'gb', text: 'Great Britain'},
4327
+ {value: 'us', text: 'United States'},
4328
+ {value: 'ru', text: 'Russia'}
4329
+ ]
4330
+ }
4331
+ });
4332
+ });
4333
+ </script>
4334
+ **/
4335
+ (function ($) {
4336
+
4337
+ var Constructor = function (options) {
4338
+ this.init('typeahead', options, Constructor.defaults);
4339
+
4340
+ //overriding objects in config (as by default jQuery extend() is not recursive)
4341
+ this.options.typeahead = $.extend({}, Constructor.defaults.typeahead, {
4342
+ //set default methods for typeahead to work with objects
4343
+ matcher: this.matcher,
4344
+ sorter: this.sorter,
4345
+ highlighter: this.highlighter,
4346
+ updater: this.updater
4347
+ }, options.typeahead);
4348
+ };
4349
+
4350
+ $.fn.editableutils.inherit(Constructor, $.fn.editabletypes.list);
4351
+
4352
+ $.extend(Constructor.prototype, {
4353
+ renderList: function() {
4354
+ this.$input = this.$tpl.is('input') ? this.$tpl : this.$tpl.find('input[type="text"]');
4355
+
4356
+ //set source of typeahead
4357
+ this.options.typeahead.source = this.sourceData;
4358
+
4359
+ //apply typeahead
4360
+ this.$input.typeahead(this.options.typeahead);
4361
+
4362
+ //attach own render method
4363
+ this.$input.data('typeahead').render = $.proxy(this.typeaheadRender, this.$input.data('typeahead'));
4364
+
4365
+ this.renderClear();
4366
+ this.setClass();
4367
+ this.setAttr('placeholder');
4368
+ },
4369
+
4370
+ value2htmlFinal: function(value, element) {
4371
+ if(this.getIsObjects()) {
4372
+ var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
4373
+ $(element).text(items.length ? items[0].text : '');
4374
+ } else {
4375
+ $(element).text(value);
4376
+ }
4377
+ },
4378
+
4379
+ html2value: function (html) {
4380
+ return html ? html : null;
4381
+ },
4382
+
4383
+ value2input: function(value) {
4384
+ if(this.getIsObjects()) {
4385
+ var items = $.fn.editableutils.itemsByValue(value, this.sourceData);
4386
+ this.$input.data('value', value).val(items.length ? items[0].text : '');
4387
+ } else {
4388
+ this.$input.val(value);
4389
+ }
4390
+ },
4391
+
4392
+ input2value: function() {
4393
+ if(this.getIsObjects()) {
4394
+ var value = this.$input.data('value'),
4395
+ items = $.fn.editableutils.itemsByValue(value, this.sourceData);
4396
+
4397
+ if(items.length && items[0].text.toLowerCase() === this.$input.val().toLowerCase()) {
4398
+ return value;
4399
+ } else {
4400
+ return null; //entered string not found in source
4401
+ }
4402
+ } else {
4403
+ return this.$input.val();
4404
+ }
4405
+ },
4406
+
4407
+ /*
4408
+ if in sourceData values <> texts, typeahead in "objects" mode:
4409
+ user must pick some value from list, otherwise `null` returned.
4410
+ if all values == texts put typeahead in "strings" mode:
4411
+ anything what entered is submited.
4412
+ */
4413
+ getIsObjects: function() {
4414
+ if(this.isObjects === undefined) {
4415
+ this.isObjects = false;
4416
+ for(var i=0; i<this.sourceData.length; i++) {
4417
+ if(this.sourceData[i].value !== this.sourceData[i].text) {
4418
+ this.isObjects = true;
4419
+ break;
4420
+ }
4421
+ }
4422
+ }
4423
+ return this.isObjects;
4424
+ },
4425
+
4426
+ /*
4427
+ Methods borrowed from text input
4428
+ */
4429
+ activate: $.fn.editabletypes.text.prototype.activate,
4430
+ renderClear: $.fn.editabletypes.text.prototype.renderClear,
4431
+ postrender: $.fn.editabletypes.text.prototype.postrender,
4432
+ toggleClear: $.fn.editabletypes.text.prototype.toggleClear,
4433
+ clear: function() {
4434
+ $.fn.editabletypes.text.prototype.clear.call(this);
4435
+ this.$input.data('value', '');
4436
+ },
4437
+
4438
+
4439
+ /*
4440
+ Typeahead option methods used as defaults
4441
+ */
4442
+ /*jshint eqeqeq:false, curly: false, laxcomma: true*/
4443
+ matcher: function (item) {
4444
+ return $.fn.typeahead.Constructor.prototype.matcher.call(this, item.text);
4445
+ },
4446
+ sorter: function (items) {
4447
+ var beginswith = []
4448
+ , caseSensitive = []
4449
+ , caseInsensitive = []
4450
+ , item
4451
+ , text;
4452
+
4453
+ while (item = items.shift()) {
4454
+ text = item.text;
4455
+ if (!text.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item);
4456
+ else if (~text.indexOf(this.query)) caseSensitive.push(item);
4457
+ else caseInsensitive.push(item);
4458
+ }
4459
+
4460
+ return beginswith.concat(caseSensitive, caseInsensitive);
4461
+ },
4462
+ highlighter: function (item) {
4463
+ return $.fn.typeahead.Constructor.prototype.highlighter.call(this, item.text);
4464
+ },
4465
+ updater: function (item) {
4466
+ item = this.$menu.find('.active').data('item');
4467
+ this.$element.data('value', item.value);
4468
+ return item.text;
4469
+ },
4470
+
4471
+
4472
+ /*
4473
+ Overwrite typeahead's render method to store objects.
4474
+ There are a lot of disscussion in bootstrap repo on this point and still no result.
4475
+ See https://github.com/twitter/bootstrap/issues/5967
4476
+
4477
+ This function just store item in via jQuery data() method instead of attr('data-value')
4478
+ */
4479
+ typeaheadRender: function (items) {
4480
+ var that = this;
4481
+
4482
+ items = $(items).map(function (i, item) {
4483
+ // i = $(that.options.item).attr('data-value', item)
4484
+ i = $(that.options.item).data('item', item);
4485
+ i.find('a').html(that.highlighter(item));
4486
+ return i[0];
4487
+ });
4488
+
4489
+ items.first().addClass('active');
4490
+ this.$menu.html(items);
4491
+ return this;
4492
+ }
4493
+ /*jshint eqeqeq: true, curly: true, laxcomma: false*/
4494
+
4495
+ });
4496
+
4497
+ Constructor.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
4498
+ /**
4499
+ @property tpl
4500
+ @default <input type="text">
4501
+ **/
4502
+ tpl:'<input type="text">',
4503
+ /**
4504
+ Configuration of typeahead. [Full list of options](http://twitter.github.com/bootstrap/javascript.html#typeahead).
4505
+
4506
+ @property typeahead
4507
+ @type object
4508
+ @default null
4509
+ **/
4510
+ typeahead: null,
4511
+ /**
4512
+ Whether to show `clear` button
4513
+
4514
+ @property clear
4515
+ @type boolean
4516
+ @default true
4517
+ **/
4518
+ clear: true
4519
+ });
4520
+
4521
+ $.fn.editabletypes.typeahead = Constructor;
4522
+
4523
+ }(window.jQuery));