ab_admin 0.3.4 → 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
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));