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
@@ -112,11 +112,11 @@ module AbAdmin
112
112
  mattr_accessor :flash_keys
113
113
  @@flash_keys = [:notice, :error]
114
114
 
115
- mattr_accessor :title_spliter
116
- @@title_spliter = ' – '
115
+ mattr_accessor :title_splitter
116
+ @@title_splitter = ' – '
117
117
 
118
118
  mattr_accessor :site_name
119
- @@title_spliter = 'AbAdmin'
119
+ @@site_name = 'AbAdmin'
120
120
 
121
121
  mattr_accessor :devise_layout
122
122
  @@devise_layout = 'admin/devise'
@@ -6,7 +6,7 @@ module AbAdmin
6
6
  def head_options(record, options = {})
7
7
  return if record.nil?
8
8
 
9
- options = { spliter: AbAdmin.title_spliter, append_title: true }.merge(options)
9
+ options = { splitter: AbAdmin.title_splitter, append_title: true }.merge(options)
10
10
 
11
11
  header = options[:header] || (record.respond_to?(:header) ? record.header : nil)
12
12
 
@@ -24,7 +24,7 @@ module AbAdmin
24
24
  page_title << view_title
25
25
  page_title << I18n.t('page.title') if options[:append_title]
26
26
 
27
- page_title.flatten.compact.uniq.join(options[:spliter])
27
+ page_title.flatten.compact.uniq.join(options[:splitter])
28
28
  end
29
29
  @page_description = [I18n.t('page.prefix'), @page_description].compact.join(' - ')
30
30
  end
@@ -75,10 +75,11 @@ module AbAdmin
75
75
  @options = options
76
76
  end
77
77
 
78
- def render(template, active=false)
78
+ def render(template)
79
79
  return if @options[:if] && !call_method_or_proc_on(template, @options[:if])
80
80
  return if @options[:unless] && call_method_or_proc_on(template, @options[:unless])
81
81
 
82
+ active = template.request.path.split('/')[2] == @path.split('/')[2]
82
83
  <<-HTML.html_safe
83
84
  <li class="#{'active' if active}">#{template.link_to @title, @path, @options}</li>
84
85
  HTML
@@ -25,8 +25,7 @@ module AbAdmin
25
25
  end
26
26
 
27
27
  def as_json(options={})
28
- options.reverse_merge!(methods: [:filename, :url, :preview_url, :thumb_url, :width, :height,
29
- :file_css_class, :human_filesize, :created_at])
28
+ options.reverse_merge!(methods: [:filename, :url, :file_css_class, :human_filesize, :created_at])
30
29
  super
31
30
  end
32
31
  end
@@ -25,7 +25,7 @@ module AbAdmin
25
25
  end
26
26
 
27
27
  def generate_password!
28
- raw_password = Rails.env.test? ? '654321' : Devise.friendly_token[0..7]
28
+ raw_password = Rails.env.test? ? '654321' : ::Devise.friendly_token[0..7]
29
29
  self.password = self.password_confirmation = raw_password
30
30
  self.save(validate: false)
31
31
  raw_password
@@ -1,3 +1,3 @@
1
1
  module AbAdmin
2
- VERSION = '0.3.4'
2
+ VERSION = '0.3.5'
3
3
  end
@@ -3,12 +3,14 @@ module AbAdmin
3
3
  module AdminHelpers
4
4
 
5
5
  def admin_form_for(object, *args, &block)
6
+ record = Array(object).last
7
+ record.fallbacks_for_empty_translations = false if record.respond_to?(:fallbacks_for_empty_translations)
6
8
  options = args.extract_options!
7
9
  options[:remote] = true if request.xhr?
8
10
  options[:html] ||= {}
9
11
  options[:html][:class] ||= 'form-horizontal'
10
12
  options[:builder] ||= ::AbAdmin::Views::FormBuilder
11
- options[:html]['data-id'] = Array(object).last.id
13
+ options[:html]['data-id'] = record.id
12
14
  if controller_name == 'manager' && resource_class == Array(object).last.class
13
15
  options[:url] ||= object.new_record? ? collection_path : resource_path
14
16
  end
@@ -95,12 +97,13 @@ module AbAdmin
95
97
  end
96
98
 
97
99
  def item_image_link(item, options={})
98
- options.reverse_merge!(url: resource_path(item), assoc: :picture)
100
+ options.reverse_merge!(assoc: :picture)
101
+ options[:url] ||= resource_path(item)
99
102
  image = item.send(options[:assoc])
100
103
  return nil unless image
101
104
  version = options[:version] || image.class.thumb_size
102
- popover_data = {content: "<img src='#{image.url}'></img>", title: item.name}
103
- link_to image_tag(image.url(version)), options[:url], data: popover_data, rel: 'popover'
105
+ popover_data = {content: "<img class='image_link_popover popover_#{options[:assoc]}' src='#{image.url}'></img>", title: item.name}
106
+ link_to image_tag(image.url(version)), options[:url], data: popover_data, rel: 'popover', remote: options[:remote]
104
107
  end
105
108
 
106
109
  def item_image(item, assoc=:photo, size=:thumb)
@@ -14,13 +14,13 @@ module AbAdmin
14
14
  end
15
15
 
16
16
  def sort_link(search, attribute, *args)
17
- return unless search
17
+ name = attribute.is_a?(Symbol) ? ha(attribute) : attribute
18
+ return name unless search
19
+
18
20
  options = args.first.is_a?(Hash) ? args.shift.dup : {}
19
21
  search_params = params[:q] || {}.with_indifferent_access
20
22
  attr_name = (options.delete(:column) || attribute).to_s
21
23
 
22
- name = attribute.is_a?(Symbol) ? ha(attribute) : attribute
23
-
24
24
  if existing_sort = search.sorts.detect { |s| s.name == attr_name }
25
25
  prev_attr, prev_dir = existing_sort.name, existing_sort.dir
26
26
  end
@@ -159,7 +159,7 @@ module AbAdmin
159
159
  def batch_action_item(item)
160
160
  if settings[:batch]
161
161
  content_tag :td do
162
- check_box_tag 'ids[]', item.id, false, id: "batch_action_item_#{item.id}"
162
+ check_box_tag 'ids[]', item.id, false, id: "batch_action_item_#{item.id}", class: 'batch_check'
163
163
  end
164
164
  end
165
165
  end
@@ -134,7 +134,7 @@ module AbAdmin
134
134
  file_title: (options[:file_title] || script_options['allowedExtensions'].join(', ')),
135
135
  file_max_size: max_size,
136
136
  assets: [value].flatten.delete_if { |v| v.nil? || v.new_record? },
137
- multiple: script_options['multiple'],
137
+ button_title: options[:button_title] || I18n.t("admin.fileupload.button#{'s' if script_options['multiple']}"),
138
138
  asset_template: (options[:asset_template] || 'asset'),
139
139
  container_data: {
140
140
  klass: params[:assetable_type],
@@ -151,7 +151,7 @@ module AbAdmin
151
151
  locals[:css_class] << 'error' if locals[:error]
152
152
 
153
153
 
154
- js_opts = [locals[:element_id], template.sort_admin_assets_path(klass: asset_klass), locals[:multiple]].map(&:inspect).join(', ')
154
+ js_opts = [locals[:element_id], template.sort_admin_assets_path(klass: asset_klass), script_options['multiple']].map(&:inspect).join(', ')
155
155
  locals[:js] = <<-JAVASCRIPT
156
156
  var upl = new qq.FileUploaderInput(#{script_options.to_json});
157
157
  upl._setupDragDrop();
@@ -26,7 +26,6 @@ gem 'will_paginate', '>= 3.0.3'
26
26
  gem 'bootstrap-wysihtml5-rails'
27
27
  gem 'will_paginate-bootstrap', '0.2.1'
28
28
  gem 'select2-rails'
29
- gem 'bootstrap-x-editable-rails'
30
29
  gem 'gon'
31
30
  gem 'i18n-js'
32
31
 
@@ -9,14 +9,14 @@ describe AbAdmin do
9
9
  before(:each) do
10
10
  AbAdmin.setup do |c|
11
11
  c.flash_keys = [:test, :test2]
12
- c.title_spliter = ' -> '
12
+ c.title_splitter = ' -> '
13
13
  c.site_name = 'Test'
14
14
  end
15
15
  end
16
16
 
17
17
  it 'should store configuration' do
18
18
  AbAdmin.flash_keys.should == [:test, :test2]
19
- AbAdmin.title_spliter.should == ' -> '
19
+ AbAdmin.title_splitter.should == ' -> '
20
20
  AbAdmin.site_name.should == 'Test'
21
21
  end
22
22
  end
@@ -0,0 +1,2895 @@
1
+ // !!! removed datepicker
2
+ /*! X-editable - v1.3.0
3
+ * In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery
4
+ * http://github.com/vitalets/x-editable
5
+ * Copyright (c) 2012 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
+ this.initInput();
25
+ };
26
+
27
+ EditableForm.prototype = {
28
+ constructor: EditableForm,
29
+ initInput: function() { //called once
30
+ var TypeConstructor, typeOptions;
31
+
32
+ //create input of specified type
33
+ if(typeof $.fn.editabletypes[this.options.type] === 'function') {
34
+ TypeConstructor = $.fn.editabletypes[this.options.type];
35
+ typeOptions = $.fn.editableutils.sliceObj(this.options, $.fn.editableutils.objectKeys(TypeConstructor.defaults));
36
+ this.input = new TypeConstructor(typeOptions);
37
+ } else {
38
+ $.error('Unknown type: '+ this.options.type);
39
+ return;
40
+ }
41
+
42
+ this.value = this.input.str2value(this.options.value);
43
+ },
44
+ initTemplate: function() {
45
+ this.$form = $($.fn.editableform.template);
46
+ },
47
+ initButtons: function() {
48
+ this.$form.find('.editable-buttons').append($.fn.editableform.buttons);
49
+ },
50
+ /**
51
+ Renders editableform
52
+
53
+ @method render
54
+ **/
55
+ render: function() {
56
+ this.$loading = $($.fn.editableform.loading);
57
+ this.$div.empty().append(this.$loading);
58
+ this.showLoading();
59
+
60
+ //init form template and buttons
61
+ this.initTemplate();
62
+ if(this.options.showbuttons) {
63
+ this.initButtons();
64
+ } else {
65
+ this.$form.find('.editable-buttons').remove();
66
+ }
67
+
68
+ /**
69
+ Fired when rendering starts
70
+ @event rendering
71
+ @param {Object} event event object
72
+ **/
73
+ this.$div.triggerHandler('rendering');
74
+
75
+ //render input
76
+ $.when(this.input.render())
77
+ .then($.proxy(function () {
78
+ //input
79
+ this.$form.find('div.editable-input').append(this.input.$input);
80
+
81
+ //automatically submit inputs when no buttons shown
82
+ if(!this.options.showbuttons) {
83
+ this.input.autosubmit();
84
+ }
85
+
86
+ //"clear" link
87
+ if(this.input.$clear) {
88
+ this.$form.find('div.editable-input').append($('<div class="editable-clear">').append(this.input.$clear));
89
+ }
90
+
91
+ //append form to container
92
+ this.$div.append(this.$form);
93
+
94
+ //attach 'cancel' handler
95
+ this.$form.find('.editable-cancel').click($.proxy(this.cancel, this));
96
+
97
+ if(this.input.error) {
98
+ this.error(this.input.error);
99
+ this.$form.find('.editable-submit').attr('disabled', true);
100
+ this.input.$input.attr('disabled', true);
101
+ //prevent form from submitting
102
+ this.$form.submit(function(e){ e.preventDefault(); });
103
+ } else {
104
+ this.error(false);
105
+ this.input.$input.removeAttr('disabled');
106
+ this.$form.find('.editable-submit').removeAttr('disabled');
107
+ this.input.value2input(this.value);
108
+ //attach submit handler
109
+ this.$form.submit($.proxy(this.submit, this));
110
+ }
111
+
112
+ /**
113
+ Fired when form is rendered
114
+ @event rendered
115
+ @param {Object} event event object
116
+ **/
117
+ this.$div.triggerHandler('rendered');
118
+
119
+ this.showForm();
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;
132
+ if(this.$form) {
133
+ //set loading size equal to form
134
+ this.$loading.width(this.$form.outerWidth());
135
+ this.$loading.height(this.$form.outerHeight());
136
+ this.$form.hide();
137
+ } else {
138
+ //stretch loading to fill container width
139
+ w = this.$loading.parent().width();
140
+ if(w) {
141
+ this.$loading.width(w);
142
+ }
143
+ }
144
+ this.$loading.show();
145
+ },
146
+
147
+ showForm: function(activate) {
148
+ this.$loading.hide();
149
+ this.$form.show();
150
+ if(activate !== false) {
151
+ this.input.activate();
152
+ }
153
+ /**
154
+ Fired when form is shown
155
+ @event show
156
+ @param {Object} event event object
157
+ **/
158
+ this.$div.triggerHandler('show');
159
+ },
160
+
161
+ error: function(msg) {
162
+ var $group = this.$form.find('.control-group'),
163
+ $block = this.$form.find('.editable-error-block');
164
+
165
+ if(msg === false) {
166
+ $group.removeClass($.fn.editableform.errorGroupClass);
167
+ $block.removeClass($.fn.editableform.errorBlockClass).empty().hide();
168
+ } else {
169
+ $group.addClass($.fn.editableform.errorGroupClass);
170
+ $block.addClass($.fn.editableform.errorBlockClass).text(msg).show();
171
+ }
172
+ },
173
+
174
+ submit: function(e) {
175
+ e.stopPropagation();
176
+ e.preventDefault();
177
+
178
+ var error,
179
+ newValue = this.input.input2value(); //get new value from input
180
+
181
+ //validation
182
+ if (error = this.validate(newValue)) {
183
+ this.error(error);
184
+ this.showForm();
185
+ return;
186
+ }
187
+
188
+ //if value not changed --> trigger 'nochange' event and return
189
+ /*jslint eqeq: true*/
190
+ if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) {
191
+ /*jslint eqeq: false*/
192
+ /**
193
+ Fired when value not changed but form is submitted. Requires savenochange = false.
194
+ @event nochange
195
+ @param {Object} event event object
196
+ **/
197
+ this.$div.triggerHandler('nochange');
198
+ return;
199
+ }
200
+
201
+ //sending data to server
202
+ $.when(this.save(newValue))
203
+ .done($.proxy(function(response) {
204
+ //run success callback
205
+ var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null;
206
+
207
+ //if success callback returns false --> keep form open and do not activate input
208
+ if(res === false) {
209
+ this.error(false);
210
+ this.showForm(false);
211
+ return;
212
+ }
213
+
214
+ //if success callback returns string --> keep form open, show error and activate input
215
+ if(typeof res === 'string') {
216
+ this.error(res);
217
+ this.showForm();
218
+ return;
219
+ }
220
+
221
+ //if success callback returns object like {newValue: <something>} --> use that value instead of submitted
222
+ if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) {
223
+ newValue = res.newValue;
224
+ }
225
+
226
+ //clear error message
227
+ this.error(false);
228
+ this.value = newValue;
229
+ /**
230
+ Fired when form is submitted
231
+ @event save
232
+ @param {Object} event event object
233
+ @param {Object} params additional params
234
+ @param {mixed} params.newValue submitted value
235
+ @param {Object} params.response ajax response
236
+
237
+ @example
238
+ $('#form-div').on('save'), function(e, params){
239
+ if(params.newValue === 'username') {...}
240
+ });
241
+ **/
242
+ this.$div.triggerHandler('save', {newValue: newValue, response: response});
243
+ }, this))
244
+ .fail($.proxy(function(xhr) {
245
+ this.error(typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!');
246
+ this.showForm();
247
+ }, this));
248
+ },
249
+
250
+ save: function(newValue) {
251
+ //convert value for submitting to server
252
+ var submitValue = this.input.value2submit(newValue);
253
+
254
+ //try parse composite pk defined as json string in data-pk
255
+ this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true);
256
+
257
+ var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk,
258
+ send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk)))),
259
+ params;
260
+
261
+ if (send) { //send to server
262
+ this.showLoading();
263
+
264
+ //standard params
265
+ params = {
266
+ name: this.options.name || '',
267
+ value: submitValue,
268
+ pk: pk
269
+ };
270
+
271
+ //additional params
272
+ if(typeof this.options.params === 'function') {
273
+ params = this.options.params.call(this.options.scope, params);
274
+ } else {
275
+ //try parse json in single quotes (from data-params attribute)
276
+ this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true);
277
+ $.extend(params, this.options.params);
278
+ }
279
+
280
+ if(typeof this.options.url === 'function') { //user's function
281
+ return this.options.url.call(this.options.scope, params);
282
+ } else {
283
+ //send ajax to server and return deferred object
284
+ return $.ajax($.extend({
285
+ url : this.options.url,
286
+ data : params,
287
+ type : 'POST'
288
+ }, this.options.ajaxOptions));
289
+ }
290
+ }
291
+ },
292
+
293
+ validate: function (value) {
294
+ if (value === undefined) {
295
+ value = this.value;
296
+ }
297
+ if (typeof this.options.validate === 'function') {
298
+ return this.options.validate.call(this.options.scope, value);
299
+ }
300
+ },
301
+
302
+ option: function(key, value) {
303
+ this.options[key] = value;
304
+ if(key === 'value') {
305
+ this.setValue(value);
306
+ }
307
+ },
308
+
309
+ setValue: function(value, convertStr) {
310
+ if(convertStr) {
311
+ this.value = this.input.str2value(value);
312
+ } else {
313
+ this.value = value;
314
+ }
315
+ }
316
+ };
317
+
318
+ /*
319
+ Initialize editableform. Applied to jQuery object.
320
+
321
+ @method $().editableform(options)
322
+ @params {Object} options
323
+ @example
324
+ var $form = $('&lt;div&gt;').editableform({
325
+ type: 'text',
326
+ name: 'username',
327
+ url: '/post',
328
+ value: 'vitaliy'
329
+ });
330
+
331
+ //to display form you should call 'render' method
332
+ $form.editableform('render');
333
+ */
334
+ $.fn.editableform = function (option) {
335
+ var args = arguments;
336
+ return this.each(function () {
337
+ var $this = $(this),
338
+ data = $this.data('editableform'),
339
+ options = typeof option === 'object' && option;
340
+ if (!data) {
341
+ $this.data('editableform', (data = new EditableForm(this, options)));
342
+ }
343
+
344
+ if (typeof option === 'string') { //call method
345
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
346
+ }
347
+ });
348
+ };
349
+
350
+ //keep link to constructor to allow inheritance
351
+ $.fn.editableform.Constructor = EditableForm;
352
+
353
+ //defaults
354
+ $.fn.editableform.defaults = {
355
+ /* see also defaults for input */
356
+
357
+ /**
358
+ Type of input. Can be <code>text|textarea|select|date|checklist</code>
359
+
360
+ @property type
361
+ @type string
362
+ @default 'text'
363
+ **/
364
+ type: 'text',
365
+ /**
366
+ Url for submit, e.g. <code>'/post'</code>
367
+ If function - it will be called instead of ajax. Function can return deferred object to run fail/done callbacks.
368
+
369
+ @property url
370
+ @type string|function
371
+ @default null
372
+ @example
373
+ url: function(params) {
374
+ if(params.value === 'abc') {
375
+ var d = new $.Deferred;
376
+ return d.reject('field cannot be "abc"'); //returning error via deferred object
377
+ } else {
378
+ someModel.set(params.name, params.value); //save data in some js model
379
+ }
380
+ }
381
+ **/
382
+ url:null,
383
+ /**
384
+ Additional params for submit. If defined as <code>object</code> - it is **appended** to original ajax data (pk, name and value).
385
+ If defined as <code>function</code> - returned object **overwrites** original ajax data.
386
+ @example
387
+ params: function(params) {
388
+ //originally params contain pk, name and value
389
+ params.a = 1;
390
+ return params;
391
+ }
392
+
393
+ @property params
394
+ @type object|function
395
+ @default null
396
+ **/
397
+ params:null,
398
+ /**
399
+ Name of field. Will be submitted on server. Can be taken from <code>id</code> attribute
400
+
401
+ @property name
402
+ @type string
403
+ @default null
404
+ **/
405
+ name: null,
406
+ /**
407
+ Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. <code>{id: 1, lang: 'en'}</code>.
408
+ Can be calculated dynamically via function.
409
+
410
+ @property pk
411
+ @type string|object|function
412
+ @default null
413
+ **/
414
+ pk: null,
415
+ /**
416
+ Initial value. If not defined - will be taken from element's content.
417
+ For __select__ type should be defined (as it is ID of shown text).
418
+
419
+ @property value
420
+ @type string|object
421
+ @default null
422
+ **/
423
+ value: null,
424
+ /**
425
+ Strategy for sending data on server. Can be <code>auto|always|never</code>.
426
+ When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element.
427
+
428
+ @property send
429
+ @type string
430
+ @default 'auto'
431
+ **/
432
+ send: 'auto',
433
+ /**
434
+ Function for client-side validation. If returns string - means validation not passed and string showed as error.
435
+
436
+ @property validate
437
+ @type function
438
+ @default null
439
+ @example
440
+ validate: function(value) {
441
+ if($.trim(value) == '') {
442
+ return 'This field is required';
443
+ }
444
+ }
445
+ **/
446
+ validate: null,
447
+ /**
448
+ Success callback. Called when value successfully sent on server and **response status = 200**.
449
+ Useful to work with json response. For example, if your backend response can be <code>{success: true}</code>
450
+ or <code>{success: false, msg: "server error"}</code> you can check it inside this callback.
451
+ If it returns **string** - means error occured and string is shown as error message.
452
+ If it returns **object like** <code>{newValue: &lt;something&gt;}</code> - it overwrites value, submitted by user.
453
+ Otherwise newValue simply rendered into element.
454
+
455
+ @property success
456
+ @type function
457
+ @default null
458
+ @example
459
+ success: function(response, newValue) {
460
+ if(!response.success) return response.msg;
461
+ }
462
+ **/
463
+ success: null,
464
+ /**
465
+ Additional options for ajax request.
466
+ List of values: http://api.jquery.com/jQuery.ajax
467
+
468
+ @property ajaxOptions
469
+ @type object
470
+ @default null
471
+ @since 1.1.1
472
+ **/
473
+ ajaxOptions: null,
474
+ /**
475
+ Whether to show buttons or not.
476
+ Form without buttons can be auto-submitted by input or by onblur = 'submit'.
477
+ @example
478
+ ajaxOptions: {
479
+ method: 'PUT',
480
+ dataType: 'xml'
481
+ }
482
+
483
+ @property showbuttons
484
+ @type boolean
485
+ @default true
486
+ @since 1.1.1
487
+ **/
488
+ showbuttons: true,
489
+ /**
490
+ Scope for callback methods (success, validate).
491
+ If <code>null</code> means editableform instance itself.
492
+
493
+ @property scope
494
+ @type DOMElement|object
495
+ @default null
496
+ @since 1.2.0
497
+ @private
498
+ **/
499
+ scope: null,
500
+ /**
501
+ Whether to save or cancel value when it was not changed but form was submitted
502
+
503
+ @property savenochange
504
+ @type boolean
505
+ @default false
506
+ @since 1.2.0
507
+ **/
508
+ savenochange: false
509
+ };
510
+
511
+ /*
512
+ Note: following params could redefined in engine: bootstrap or jqueryui:
513
+ Classes 'control-group' and 'editable-error-block' must always present!
514
+ */
515
+ $.fn.editableform.template = '<form class="form-inline editableform">'+
516
+ '<div class="control-group">' +
517
+ '<div><div class="editable-input"></div><div class="editable-buttons"></div></div>'+
518
+ '<div class="editable-error-block"></div>' +
519
+ '</div>' +
520
+ '</form>';
521
+
522
+ //loading div
523
+ $.fn.editableform.loading = '<div class="editableform-loading"></div>';
524
+
525
+ //buttons
526
+ $.fn.editableform.buttons = '<button type="submit" class="editable-submit">ok</button>'+
527
+ '<button type="button" class="editable-cancel">cancel</button>';
528
+
529
+ //error class attached to control-group
530
+ $.fn.editableform.errorGroupClass = null;
531
+
532
+ //error class attached to editable-error-block
533
+ $.fn.editableform.errorBlockClass = 'editable-error';
534
+ }(window.jQuery));
535
+ /**
536
+ * EditableForm utilites
537
+ */
538
+ (function ($) {
539
+ //utils
540
+ $.fn.editableutils = {
541
+ /**
542
+ * classic JS inheritance function
543
+ */
544
+ inherit: function (Child, Parent) {
545
+ var F = function() { };
546
+ F.prototype = Parent.prototype;
547
+ Child.prototype = new F();
548
+ Child.prototype.constructor = Child;
549
+ Child.superclass = Parent.prototype;
550
+ },
551
+
552
+ /**
553
+ * set caret position in input
554
+ * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area
555
+ */
556
+ setCursorPosition: function(elem, pos) {
557
+ if (elem.setSelectionRange) {
558
+ elem.setSelectionRange(pos, pos);
559
+ } else if (elem.createTextRange) {
560
+ var range = elem.createTextRange();
561
+ range.collapse(true);
562
+ range.moveEnd('character', pos);
563
+ range.moveStart('character', pos);
564
+ range.select();
565
+ }
566
+ },
567
+
568
+ /**
569
+ * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes)
570
+ * That allows such code as: <a data-source="{'a': 'b', 'c': 'd'}">
571
+ * safe = true --> means no exception will be thrown
572
+ * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery
573
+ */
574
+ tryParseJson: function(s, safe) {
575
+ if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) {
576
+ if (safe) {
577
+ try {
578
+ /*jslint evil: true*/
579
+ s = (new Function('return ' + s))();
580
+ /*jslint evil: false*/
581
+ } catch (e) {} finally {
582
+ return s;
583
+ }
584
+ } else {
585
+ /*jslint evil: true*/
586
+ s = (new Function('return ' + s))();
587
+ /*jslint evil: false*/
588
+ }
589
+ }
590
+ return s;
591
+ },
592
+
593
+ /**
594
+ * slice object by specified keys
595
+ */
596
+ sliceObj: function(obj, keys, caseSensitive /* default: false */) {
597
+ var key, keyLower, newObj = {};
598
+
599
+ if (!$.isArray(keys) || !keys.length) {
600
+ return newObj;
601
+ }
602
+
603
+ for (var i = 0; i < keys.length; i++) {
604
+ key = keys[i];
605
+ if (obj.hasOwnProperty(key)) {
606
+ newObj[key] = obj[key];
607
+ }
608
+
609
+ if(caseSensitive === true) {
610
+ continue;
611
+ }
612
+
613
+ //when getting data-* attributes via $.data() it's converted to lowercase.
614
+ //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery
615
+ //workaround is code below.
616
+ keyLower = key.toLowerCase();
617
+ if (obj.hasOwnProperty(keyLower)) {
618
+ newObj[key] = obj[keyLower];
619
+ }
620
+ }
621
+
622
+ return newObj;
623
+ },
624
+
625
+ /**
626
+ * exclude complex objects from $.data() before pass to config
627
+ */
628
+ getConfigData: function($element) {
629
+ var data = {};
630
+ $.each($element.data(), function(k, v) {
631
+ if(typeof v !== 'object' || (v && typeof v === 'object' && v.constructor === Object)) {
632
+ data[k] = v;
633
+ }
634
+ });
635
+ return data;
636
+ },
637
+
638
+ objectKeys: function(o) {
639
+ if (Object.keys) {
640
+ return Object.keys(o);
641
+ } else {
642
+ if (o !== Object(o)) {
643
+ throw new TypeError('Object.keys called on a non-object');
644
+ }
645
+ var k=[], p;
646
+ for (p in o) {
647
+ if (Object.prototype.hasOwnProperty.call(o,p)) {
648
+ k.push(p);
649
+ }
650
+ }
651
+ return k;
652
+ }
653
+
654
+ },
655
+
656
+ /**
657
+ method to escape html.
658
+ **/
659
+ escape: function(str) {
660
+ return $('<div>').text(str).html();
661
+ }
662
+ };
663
+ }(window.jQuery));
664
+ /**
665
+ Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.<br>
666
+ This method applied internally in <code>$().editable()</code>. You should subscribe on it's events (save / cancel) to get profit of it.<br>
667
+ Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.<br>
668
+ Applied as jQuery method.
669
+
670
+ @class editableContainer
671
+ @uses editableform
672
+ **/
673
+ (function ($) {
674
+
675
+ var EditableContainer = function (element, options) {
676
+ this.init(element, options);
677
+ };
678
+
679
+ //methods
680
+ EditableContainer.prototype = {
681
+ containerName: null, //tbd in child class
682
+ innerCss: null, //tbd in child class
683
+ init: function(element, options) {
684
+ this.$element = $(element);
685
+ //todo: what is in priority: data or js?
686
+ this.options = $.extend({}, $.fn.editableContainer.defaults, $.fn.editableutils.getConfigData(this.$element), options);
687
+ this.splitOptions();
688
+ this.initContainer();
689
+
690
+ //bind 'destroyed' listener to destroy container when element is removed from dom
691
+ this.$element.on('destroyed', $.proxy(function(){
692
+ this.destroy();
693
+ }, this));
694
+
695
+ //attach document handlers (once)
696
+ if(!$(document).data('editable-handlers-attached')) {
697
+ //close all on escape
698
+ $(document).on('keyup.editable', function (e) {
699
+ if (e.which === 27) {
700
+ $('.editable-open').editableContainer('hide');
701
+ //todo: return focus on element
702
+ }
703
+ });
704
+
705
+ //close containers when click outside
706
+ $(document).on('click.editable', function(e) {
707
+ var $target = $(e.target);
708
+
709
+ //if click inside some editableContainer --> no nothing
710
+ if($target.is('.editable-container') || $target.parents('.editable-container').length || $target.parents('.ui-datepicker-header').length) {
711
+ return;
712
+ } else {
713
+ //close all open containers (except one)
714
+ EditableContainer.prototype.closeOthers(e.target);
715
+ }
716
+ });
717
+
718
+ $(document).data('editable-handlers-attached', true);
719
+ }
720
+ },
721
+
722
+ //split options on containerOptions and formOptions
723
+ splitOptions: function() {
724
+ this.containerOptions = {};
725
+ this.formOptions = {};
726
+ var cDef = $.fn[this.containerName].defaults;
727
+ for(var k in this.options) {
728
+ if(k in cDef) {
729
+ this.containerOptions[k] = this.options[k];
730
+ } else {
731
+ this.formOptions[k] = this.options[k];
732
+ }
733
+ }
734
+ },
735
+
736
+ initContainer: function(){
737
+ this.call(this.containerOptions);
738
+ },
739
+
740
+ initForm: function() {
741
+ this.formOptions.scope = this.$element[0]; //set scope of form callbacks to element
742
+ this.$form = $('<div>')
743
+ .editableform(this.formOptions)
744
+ .on({
745
+ save: $.proxy(this.save, this),
746
+ cancel: $.proxy(function(){ this.hide('cancel'); }, this),
747
+ nochange: $.proxy(function(){ this.hide('nochange'); }, this),
748
+ show: $.proxy(this.setPosition, this), //re-position container every time form is shown (occurs each time after loading state)
749
+ rendering: $.proxy(this.setPosition, this), //this allows to place container correctly when loading shown
750
+ rendered: $.proxy(function(){
751
+ /**
752
+ Fired when container is shown and form is rendered (for select will wait for loading dropdown options)
753
+
754
+ @event shown
755
+ @param {Object} event event object
756
+ @example
757
+ $('#username').on('shown', function() {
758
+ var $tip = $(this).data('editableContainer').tip();
759
+ $tip.find('input').val('overwriting value of input..');
760
+ });
761
+ **/
762
+ this.$element.triggerHandler('shown');
763
+ }, this)
764
+ });
765
+ return this.$form;
766
+ },
767
+
768
+ /*
769
+ Returns jquery object of container
770
+ @method tip()
771
+ */
772
+ tip: function() {
773
+ return this.container().$tip;
774
+ },
775
+
776
+ container: function() {
777
+ return this.$element.data(this.containerName);
778
+ },
779
+
780
+ call: function() {
781
+ this.$element[this.containerName].apply(this.$element, arguments);
782
+ },
783
+
784
+ /**
785
+ Shows container with form
786
+ @method show()
787
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
788
+ **/
789
+ show: function (closeAll) {
790
+ this.$element.addClass('editable-open');
791
+ if(closeAll !== false) {
792
+ //close all open containers (except this)
793
+ this.closeOthers(this.$element[0]);
794
+ }
795
+
796
+ this.innerShow();
797
+ },
798
+
799
+ /* internal show method. To be overwritten in child classes */
800
+ innerShow: function () {
801
+ this.call('show');
802
+ this.tip().addClass('editable-container');
803
+ this.initForm();
804
+ this.tip().find(this.innerCss).empty().append(this.$form);
805
+ this.$form.editableform('render');
806
+ },
807
+
808
+ /**
809
+ Hides container with form
810
+ @method hide()
811
+ @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
812
+ **/
813
+ hide: function(reason) {
814
+ if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) {
815
+ return;
816
+ }
817
+ this.$element.removeClass('editable-open');
818
+ this.innerHide();
819
+ /**
820
+ Fired when container was hidden. It occurs on both save or cancel.
821
+
822
+ @event hidden
823
+ @param {object} event event object
824
+ @param {string} reason Reason caused hiding. Can be <code>save|cancel|onblur|nochange|undefined (=manual)</code>
825
+ @example
826
+ $('#username').on('hidden', function(e, reason) {
827
+ if(reason === 'save' || reason === 'cancel') {
828
+ //auto-open next editable
829
+ $(this).closest('tr').next().find('.editable').editable('show');
830
+ }
831
+ });
832
+ **/
833
+ this.$element.triggerHandler('hidden', reason);
834
+ },
835
+
836
+ /* internal hide method. To be overwritten in child classes */
837
+ innerHide: function () {
838
+ this.call('hide');
839
+ },
840
+
841
+ /**
842
+ Toggles container visibility (show / hide)
843
+ @method toggle()
844
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
845
+ **/
846
+ toggle: function(closeAll) {
847
+ if(this.tip && this.tip().is(':visible')) {
848
+ this.hide();
849
+ } else {
850
+ this.show(closeAll);
851
+ }
852
+ },
853
+
854
+ /*
855
+ Updates the position of container when content changed.
856
+ @method setPosition()
857
+ */
858
+ setPosition: function() {
859
+ //tbd in child class
860
+ },
861
+
862
+ save: function(e, params) {
863
+ this.hide('save');
864
+ /**
865
+ Fired when new value was submitted. You can use <code>$(this).data('editableContainer')</code> inside handler to access to editableContainer instance
866
+
867
+ @event save
868
+ @param {Object} event event object
869
+ @param {Object} params additional params
870
+ @param {mixed} params.newValue submitted value
871
+ @param {Object} params.response ajax response
872
+ @example
873
+ $('#username').on('save', function(e, params) {
874
+ //assuming server response: '{success: true}'
875
+ var pk = $(this).data('editableContainer').options.pk;
876
+ if(params.response && params.response.success) {
877
+ alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
878
+ } else {
879
+ alert('error!');
880
+ }
881
+ });
882
+ **/
883
+ this.$element.triggerHandler('save', params);
884
+ },
885
+
886
+ /**
887
+ Sets new option
888
+
889
+ @method option(key, value)
890
+ @param {string} key
891
+ @param {mixed} value
892
+ **/
893
+ option: function(key, value) {
894
+ this.options[key] = value;
895
+ if(key in this.containerOptions) {
896
+ this.containerOptions[key] = value;
897
+ this.setContainerOption(key, value);
898
+ } else {
899
+ this.formOptions[key] = value;
900
+ if(this.$form) {
901
+ this.$form.editableform('option', key, value);
902
+ }
903
+ }
904
+ },
905
+
906
+ setContainerOption: function(key, value) {
907
+ this.call('option', key, value);
908
+ },
909
+
910
+ /**
911
+ Destroys the container instance
912
+ @method destroy()
913
+ **/
914
+ destroy: function() {
915
+ this.call('destroy');
916
+ },
917
+
918
+ /*
919
+ Closes other containers except one related to passed element.
920
+ Other containers can be cancelled or submitted (depends on onblur option)
921
+ */
922
+ closeOthers: function(element) {
923
+ $('.editable-open').each(function(i, el){
924
+ //do nothing with passed element and it's children
925
+ if(el === element || $(el).find(element).length) {
926
+ return;
927
+ }
928
+
929
+ //otherwise cancel or submit all open containers
930
+ var $el = $(el),
931
+ ec = $el.data('editableContainer');
932
+
933
+ if(!ec) {
934
+ return;
935
+ }
936
+
937
+ if(ec.options.onblur === 'cancel') {
938
+ $el.data('editableContainer').hide('onblur');
939
+ } else if(ec.options.onblur === 'submit') {
940
+ $el.data('editableContainer').tip().find('form').submit();
941
+ }
942
+ });
943
+
944
+ },
945
+
946
+ /**
947
+ Activates input of visible container (e.g. set focus)
948
+ @method activate()
949
+ **/
950
+ activate: function() {
951
+ if(this.tip && this.tip().is(':visible') && this.$form) {
952
+ this.$form.data('editableform').input.activate();
953
+ }
954
+ }
955
+
956
+ };
957
+
958
+ /**
959
+ jQuery method to initialize editableContainer.
960
+
961
+ @method $().editableContainer(options)
962
+ @params {Object} options
963
+ @example
964
+ $('#edit').editableContainer({
965
+ type: 'text',
966
+ url: '/post',
967
+ pk: 1,
968
+ value: 'hello'
969
+ });
970
+ **/
971
+ $.fn.editableContainer = function (option) {
972
+ var args = arguments;
973
+ return this.each(function () {
974
+ var $this = $(this),
975
+ dataKey = 'editableContainer',
976
+ data = $this.data(dataKey),
977
+ options = typeof option === 'object' && option;
978
+
979
+ if (!data) {
980
+ $this.data(dataKey, (data = new EditableContainer(this, options)));
981
+ }
982
+
983
+ if (typeof option === 'string') { //call method
984
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
985
+ }
986
+ });
987
+ };
988
+
989
+ //store constructor
990
+ $.fn.editableContainer.Constructor = EditableContainer;
991
+
992
+ //defaults
993
+ $.fn.editableContainer.defaults = {
994
+ /**
995
+ Initial value of form input
996
+
997
+ @property value
998
+ @type mixed
999
+ @default null
1000
+ @private
1001
+ **/
1002
+ value: null,
1003
+ /**
1004
+ Placement of container relative to element. Can be <code>top|right|bottom|left</code>. Not used for inline container.
1005
+
1006
+ @property placement
1007
+ @type string
1008
+ @default 'top'
1009
+ **/
1010
+ placement: 'top',
1011
+ /**
1012
+ Whether to hide container on save/cancel.
1013
+
1014
+ @property autohide
1015
+ @type boolean
1016
+ @default true
1017
+ @private
1018
+ **/
1019
+ autohide: true,
1020
+ /**
1021
+ Action when user clicks outside the container. Can be <code>cancel|submit|ignore</code>.
1022
+ Setting <code>ignore</code> allows to have several containers open.
1023
+
1024
+ @property onblur
1025
+ @type string
1026
+ @default 'cancel'
1027
+ @since 1.1.1
1028
+ **/
1029
+ onblur: 'cancel'
1030
+ };
1031
+
1032
+ /*
1033
+ * workaround to have 'destroyed' event to destroy popover when element is destroyed
1034
+ * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom
1035
+ */
1036
+ jQuery.event.special.destroyed = {
1037
+ remove: function(o) {
1038
+ if (o.handler) {
1039
+ o.handler();
1040
+ }
1041
+ }
1042
+ };
1043
+
1044
+ }(window.jQuery));
1045
+
1046
+ /**
1047
+ Makes editable any HTML element on the page. Applied as jQuery method.
1048
+
1049
+ @class editable
1050
+ @uses editableContainer
1051
+ **/
1052
+ (function ($) {
1053
+
1054
+ var Editable = function (element, options) {
1055
+ this.$element = $(element);
1056
+ this.options = $.extend({}, $.fn.editable.defaults, $.fn.editableutils.getConfigData(this.$element), options);
1057
+ this.init();
1058
+ };
1059
+
1060
+ Editable.prototype = {
1061
+ constructor: Editable,
1062
+ init: function () {
1063
+ var TypeConstructor,
1064
+ isValueByText = false,
1065
+ doAutotext,
1066
+ finalize;
1067
+
1068
+ //editableContainer must be defined
1069
+ if(!$.fn.editableContainer) {
1070
+ $.error('You must define $.fn.editableContainer via including corresponding file (e.g. editable-popover.js)');
1071
+ return;
1072
+ }
1073
+
1074
+ //name
1075
+ this.options.name = this.options.name || this.$element.attr('id');
1076
+
1077
+ //create input of specified type. Input will be used for converting value, not in form
1078
+ if(typeof $.fn.editabletypes[this.options.type] === 'function') {
1079
+ TypeConstructor = $.fn.editabletypes[this.options.type];
1080
+ this.typeOptions = $.fn.editableutils.sliceObj(this.options, $.fn.editableutils.objectKeys(TypeConstructor.defaults));
1081
+ this.input = new TypeConstructor(this.typeOptions);
1082
+ } else {
1083
+ $.error('Unknown type: '+ this.options.type);
1084
+ return;
1085
+ }
1086
+
1087
+ //set value from settings or by element's text
1088
+ if (this.options.value === undefined || this.options.value === null) {
1089
+ this.value = this.input.html2value($.trim(this.$element.html()));
1090
+ isValueByText = true;
1091
+ } else {
1092
+ /*
1093
+ value can be string when received from 'data-value' attribute
1094
+ for complext objects value can be set as json string in data-value attribute,
1095
+ e.g. data-value="{city: 'Moscow', street: 'Lenina'}"
1096
+ */
1097
+ this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true);
1098
+ if(typeof this.options.value === 'string') {
1099
+ this.value = this.input.str2value(this.options.value);
1100
+ } else {
1101
+ this.value = this.options.value;
1102
+ }
1103
+ }
1104
+
1105
+ //add 'editable' class to every editable element
1106
+ this.$element.addClass('editable');
1107
+
1108
+ //attach handler activating editable. In disabled mode it just prevent default action (useful for links)
1109
+ if(this.options.toggle !== 'manual') {
1110
+ this.$element.addClass('editable-click');
1111
+ this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){
1112
+ e.preventDefault();
1113
+ //stop propagation not required anymore because in document click handler it checks event target
1114
+ //e.stopPropagation();
1115
+
1116
+ if(this.options.toggle === 'mouseenter') {
1117
+ //for hover only show container
1118
+ this.show();
1119
+ } else {
1120
+ //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener
1121
+ var closeAll = (this.options.toggle !== 'click');
1122
+ this.toggle(closeAll);
1123
+ }
1124
+ }, this));
1125
+ } else {
1126
+ this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually
1127
+ }
1128
+
1129
+ //check conditions for autotext:
1130
+ //if value was generated by text or value is empty, no sense to run autotext
1131
+ doAutotext = !isValueByText && this.value !== null && this.value !== undefined;
1132
+ doAutotext &= (this.options.autotext === 'always') || (this.options.autotext === 'auto' && !this.$element.text().length);
1133
+ $.when(doAutotext ? this.render() : true).then($.proxy(function() {
1134
+ if(this.options.disabled) {
1135
+ this.disable();
1136
+ } else {
1137
+ this.enable();
1138
+ }
1139
+ /**
1140
+ Fired when element was initialized by editable method.
1141
+
1142
+ @event init
1143
+ @param {Object} event event object
1144
+ @param {Object} editable editable instance
1145
+ @since 1.2.0
1146
+ **/
1147
+ this.$element.triggerHandler('init', this);
1148
+ }, this));
1149
+ },
1150
+
1151
+ /*
1152
+ Renders value into element's text.
1153
+ Can call custom display method from options.
1154
+ Can return deferred object.
1155
+ @method render()
1156
+ */
1157
+ render: function() {
1158
+ //do not display anything
1159
+ if(this.options.display === false) {
1160
+ return;
1161
+ }
1162
+ //if it is input with source, we pass callback in third param to be called when source is loaded
1163
+ if(this.input.options.hasOwnProperty('source')) {
1164
+ return this.input.value2html(this.value, this.$element[0], this.options.display);
1165
+ //if display method defined --> use it
1166
+ } else if(typeof this.options.display === 'function') {
1167
+ return this.options.display.call(this.$element[0], this.value);
1168
+ //else use input's original value2html() method
1169
+ } else {
1170
+ return this.input.value2html(this.value, this.$element[0]);
1171
+ }
1172
+ },
1173
+
1174
+ /**
1175
+ Enables editable
1176
+ @method enable()
1177
+ **/
1178
+ enable: function() {
1179
+ this.options.disabled = false;
1180
+ this.$element.removeClass('editable-disabled');
1181
+ this.handleEmpty();
1182
+ if(this.options.toggle !== 'manual') {
1183
+ if(this.$element.attr('tabindex') === '-1') {
1184
+ this.$element.removeAttr('tabindex');
1185
+ }
1186
+ }
1187
+ },
1188
+
1189
+ /**
1190
+ Disables editable
1191
+ @method disable()
1192
+ **/
1193
+ disable: function() {
1194
+ this.options.disabled = true;
1195
+ this.hide();
1196
+ this.$element.addClass('editable-disabled');
1197
+ this.handleEmpty();
1198
+ //do not stop focus on this element
1199
+ this.$element.attr('tabindex', -1);
1200
+ },
1201
+
1202
+ /**
1203
+ Toggles enabled / disabled state of editable element
1204
+ @method toggleDisabled()
1205
+ **/
1206
+ toggleDisabled: function() {
1207
+ if(this.options.disabled) {
1208
+ this.enable();
1209
+ } else {
1210
+ this.disable();
1211
+ }
1212
+ },
1213
+
1214
+ /**
1215
+ Sets new option
1216
+
1217
+ @method option(key, value)
1218
+ @param {string|object} key option name or object with several options
1219
+ @param {mixed} value option new value
1220
+ @example
1221
+ $('.editable').editable('option', 'pk', 2);
1222
+ **/
1223
+ option: function(key, value) {
1224
+ //set option(s) by object
1225
+ if(key && typeof key === 'object') {
1226
+ $.each(key, $.proxy(function(k, v){
1227
+ this.option($.trim(k), v);
1228
+ }, this));
1229
+ return;
1230
+ }
1231
+
1232
+ //set option by string
1233
+ this.options[key] = value;
1234
+
1235
+ //disabled
1236
+ if(key === 'disabled') {
1237
+ if(value) {
1238
+ this.disable();
1239
+ } else {
1240
+ this.enable();
1241
+ }
1242
+ return;
1243
+ }
1244
+
1245
+ //value
1246
+ if(key === 'value') {
1247
+ this.setValue(value);
1248
+ }
1249
+
1250
+ //transfer new option to container!
1251
+ if(this.container) {
1252
+ this.container.option(key, value);
1253
+ }
1254
+ },
1255
+
1256
+ /*
1257
+ * set emptytext if element is empty (reverse: remove emptytext if needed)
1258
+ */
1259
+ handleEmpty: function () {
1260
+ //do not handle empty if we do not display anything
1261
+ if(this.options.display === false) {
1262
+ return;
1263
+ }
1264
+
1265
+ var emptyClass = 'editable-empty';
1266
+ //emptytext shown only for enabled
1267
+ if(!this.options.disabled) {
1268
+ if ($.trim(this.$element.text()) === '') {
1269
+ this.$element.addClass(emptyClass).text(this.options.emptytext);
1270
+ } else {
1271
+ this.$element.removeClass(emptyClass);
1272
+ }
1273
+ } else {
1274
+ //below required if element disable property was changed
1275
+ if(this.$element.hasClass(emptyClass)) {
1276
+ this.$element.empty();
1277
+ this.$element.removeClass(emptyClass);
1278
+ }
1279
+ }
1280
+ },
1281
+
1282
+ /**
1283
+ Shows container with form
1284
+ @method show()
1285
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1286
+ **/
1287
+ show: function (closeAll) {
1288
+ if(this.options.disabled) {
1289
+ return;
1290
+ }
1291
+
1292
+ //init editableContainer: popover, tooltip, inline, etc..
1293
+ if(!this.container) {
1294
+ var containerOptions = $.extend({}, this.options, {
1295
+ value: this.value
1296
+ });
1297
+ this.$element.editableContainer(containerOptions);
1298
+ this.$element.on("save.internal", $.proxy(this.save, this));
1299
+ this.container = this.$element.data('editableContainer');
1300
+ } else if(this.container.tip().is(':visible')) {
1301
+ return;
1302
+ }
1303
+
1304
+ //show container
1305
+ this.container.show(closeAll);
1306
+ },
1307
+
1308
+ /**
1309
+ Hides container with form
1310
+ @method hide()
1311
+ **/
1312
+ hide: function () {
1313
+ if(this.container) {
1314
+ this.container.hide();
1315
+ }
1316
+ },
1317
+
1318
+ /**
1319
+ Toggles container visibility (show / hide)
1320
+ @method toggle()
1321
+ @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true.
1322
+ **/
1323
+ toggle: function(closeAll) {
1324
+ if(this.container && this.container.tip().is(':visible')) {
1325
+ this.hide();
1326
+ } else {
1327
+ this.show(closeAll);
1328
+ }
1329
+ },
1330
+
1331
+ /*
1332
+ * called when form was submitted
1333
+ */
1334
+ save: function(e, params) {
1335
+ //if url is not user's function and value was not sent to server and value changed --> mark element with unsaved css.
1336
+ if(typeof this.options.url !== 'function' && this.options.display !== false && params.response === undefined && this.input.value2str(this.value) !== this.input.value2str(params.newValue)) {
1337
+ this.$element.addClass('editable-unsaved');
1338
+ } else {
1339
+ this.$element.removeClass('editable-unsaved');
1340
+ }
1341
+
1342
+ // this.hide();
1343
+ this.setValue(params.newValue);
1344
+
1345
+ /**
1346
+ Fired when new value was submitted. You can use <code>$(this).data('editable')</code> to access to editable instance
1347
+
1348
+ @event save
1349
+ @param {Object} event event object
1350
+ @param {Object} params additional params
1351
+ @param {mixed} params.newValue submitted value
1352
+ @param {Object} params.response ajax response
1353
+ @example
1354
+ $('#username').on('save', function(e, params) {
1355
+ //assuming server response: '{success: true}'
1356
+ var pk = $(this).data('editable').options.pk;
1357
+ if(params.response && params.response.success) {
1358
+ alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!');
1359
+ } else {
1360
+ alert('error!');
1361
+ }
1362
+ });
1363
+ **/
1364
+ //event itself is triggered by editableContainer. Description here is only for documentation
1365
+ },
1366
+
1367
+ validate: function () {
1368
+ if (typeof this.options.validate === 'function') {
1369
+ return this.options.validate.call(this, this.value);
1370
+ }
1371
+ },
1372
+
1373
+ /**
1374
+ Sets new value of editable
1375
+ @method setValue(value, convertStr)
1376
+ @param {mixed} value new value
1377
+ @param {boolean} convertStr whether to convert value from string to internal format
1378
+ **/
1379
+ setValue: function(value, convertStr) {
1380
+ if(convertStr) {
1381
+ this.value = this.input.str2value(value);
1382
+ } else {
1383
+ this.value = value;
1384
+ }
1385
+ if(this.container) {
1386
+ this.container.option('value', this.value);
1387
+ }
1388
+ $.when(this.render())
1389
+ .then($.proxy(function() {
1390
+ this.handleEmpty();
1391
+ }, this));
1392
+ },
1393
+
1394
+ /**
1395
+ Activates input of visible container (e.g. set focus)
1396
+ @method activate()
1397
+ **/
1398
+ activate: function() {
1399
+ if(this.container) {
1400
+ this.container.activate();
1401
+ }
1402
+ }
1403
+ };
1404
+
1405
+ /* EDITABLE PLUGIN DEFINITION
1406
+ * ======================= */
1407
+
1408
+ /**
1409
+ jQuery method to initialize editable element.
1410
+
1411
+ @method $().editable(options)
1412
+ @params {Object} options
1413
+ @example
1414
+ $('#username').editable({
1415
+ type: 'text',
1416
+ url: '/post',
1417
+ pk: 1
1418
+ });
1419
+ **/
1420
+ $.fn.editable = function (option) {
1421
+ //special API methods returning non-jquery object
1422
+ var result = {}, args = arguments, datakey = 'editable';
1423
+ switch (option) {
1424
+ /**
1425
+ Runs client-side validation for all matched editables
1426
+
1427
+ @method validate()
1428
+ @returns {Object} validation errors map
1429
+ @example
1430
+ $('#username, #fullname').editable('validate');
1431
+ // possible result:
1432
+ {
1433
+ username: "username is required",
1434
+ fullname: "fullname should be minimum 3 letters length"
1435
+ }
1436
+ **/
1437
+ case 'validate':
1438
+ this.each(function () {
1439
+ var $this = $(this), data = $this.data(datakey), error;
1440
+ if (data && (error = data.validate())) {
1441
+ result[data.options.name] = error;
1442
+ }
1443
+ });
1444
+ return result;
1445
+
1446
+ /**
1447
+ Returns current values of editable elements. If value is <code>null</code> or <code>undefined</code> it will not be returned
1448
+ @method getValue()
1449
+ @returns {Object} object of element names and values
1450
+ @example
1451
+ $('#username, #fullname').editable('validate');
1452
+ // possible result:
1453
+ {
1454
+ username: "superuser",
1455
+ fullname: "John"
1456
+ }
1457
+ **/
1458
+ case 'getValue':
1459
+ this.each(function () {
1460
+ var $this = $(this), data = $this.data(datakey);
1461
+ if (data && data.value !== undefined && data.value !== null) {
1462
+ result[data.options.name] = data.input.value2submit(data.value);
1463
+ }
1464
+ });
1465
+ return result;
1466
+
1467
+ /**
1468
+ This method collects values from several editable elements and submit them all to server.
1469
+ Internally it runs client-side validation for all fields and submits only in case of success.
1470
+ See <a href="#newrecord">creating new records</a> for details.
1471
+
1472
+ @method submit(options)
1473
+ @param {object} options
1474
+ @param {object} options.url url to submit data
1475
+ @param {object} options.data additional data to submit
1476
+ @param {object} options.ajaxOptions additional ajax options
1477
+ @param {function} options.error(obj) error handler
1478
+ @param {function} options.success(obj,config) success handler
1479
+ @returns {Object} jQuery object
1480
+ **/
1481
+ case 'submit': //collects value, validate and submit to server for creating new record
1482
+ var config = arguments[1] || {},
1483
+ $elems = this,
1484
+ errors = this.editable('validate'),
1485
+ values;
1486
+
1487
+ if($.isEmptyObject(errors)) {
1488
+ values = this.editable('getValue');
1489
+ if(config.data) {
1490
+ $.extend(values, config.data);
1491
+ }
1492
+
1493
+ $.ajax($.extend({
1494
+ url: config.url,
1495
+ data: values,
1496
+ type: 'POST'
1497
+ }, config.ajaxOptions))
1498
+ .success(function(response) {
1499
+ //successful response 200 OK
1500
+ if(typeof config.success === 'function') {
1501
+ config.success.call($elems, response, config);
1502
+ }
1503
+ })
1504
+ .error(function(){ //ajax error
1505
+ if(typeof config.error === 'function') {
1506
+ config.error.apply($elems, arguments);
1507
+ }
1508
+ });
1509
+ } else { //client-side validation error
1510
+ if(typeof config.error === 'function') {
1511
+ config.error.call($elems, errors);
1512
+ }
1513
+ }
1514
+ return this;
1515
+ }
1516
+
1517
+ //return jquery object
1518
+ return this.each(function () {
1519
+ var $this = $(this),
1520
+ data = $this.data(datakey),
1521
+ options = typeof option === 'object' && option;
1522
+
1523
+ if (!data) {
1524
+ $this.data(datakey, (data = new Editable(this, options)));
1525
+ }
1526
+
1527
+ if (typeof option === 'string') { //call method
1528
+ data[option].apply(data, Array.prototype.slice.call(args, 1));
1529
+ }
1530
+ });
1531
+ };
1532
+
1533
+
1534
+ $.fn.editable.defaults = {
1535
+ /**
1536
+ Type of input. Can be <code>text|textarea|select|date|checklist</code> and more
1537
+
1538
+ @property type
1539
+ @type string
1540
+ @default 'text'
1541
+ **/
1542
+ type: 'text',
1543
+ /**
1544
+ Sets disabled state of editable
1545
+
1546
+ @property disabled
1547
+ @type boolean
1548
+ @default false
1549
+ **/
1550
+ disabled: false,
1551
+ /**
1552
+ How to toggle editable. Can be <code>click|dblclick|mouseenter|manual</code>.
1553
+ When set to <code>manual</code> you should manually call <code>show/hide</code> methods of editable.
1554
+ **Note**: if you call <code>show</code> or <code>toggle</code> inside **click** handler of some DOM element,
1555
+ you need to apply <code>e.stopPropagation()</code> because containers are being closed on any click on document.
1556
+
1557
+ @example
1558
+ $('#edit-button').click(function(e) {
1559
+ e.stopPropagation();
1560
+ $('#username').editable('toggle');
1561
+ });
1562
+
1563
+ @property toggle
1564
+ @type string
1565
+ @default 'click'
1566
+ **/
1567
+ toggle: 'click',
1568
+ /**
1569
+ Text shown when element is empty.
1570
+
1571
+ @property emptytext
1572
+ @type string
1573
+ @default 'Empty'
1574
+ **/
1575
+ emptytext: 'Empty',
1576
+ /**
1577
+ Allows to automatically set element's text based on it's value. Can be <code>auto|always|never</code>. Useful for select and date.
1578
+ 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>.
1579
+ <code>auto</code> - text will be automatically set only if element is empty.
1580
+ <code>always|never</code> - always(never) try to set element's text.
1581
+
1582
+ @property autotext
1583
+ @type string
1584
+ @default 'auto'
1585
+ **/
1586
+ autotext: 'auto',
1587
+ /**
1588
+ Initial value of input. Taken from <code>data-value</code> or element's text.
1589
+
1590
+ @property value
1591
+ @type mixed
1592
+ @default element's text
1593
+ **/
1594
+ value: null,
1595
+ /**
1596
+ Callback to perform custom displaying of value in element's text.
1597
+ If <code>null</code>, default input's value2html() will be called.
1598
+ If <code>false</code>, no displaying methods will be called, element's text will no change.
1599
+ Runs under element's scope.
1600
+ Second parameter __sourceData__ is passed for inputs with source (select, checklist).
1601
+
1602
+ @property display
1603
+ @type function|boolean
1604
+ @default null
1605
+ @since 1.2.0
1606
+ @example
1607
+ display: function(value, sourceData) {
1608
+ var escapedValue = $('<div>').text(value).html();
1609
+ $(this).html('<b>'+escapedValue+'</b>');
1610
+ }
1611
+ **/
1612
+ display: null
1613
+ };
1614
+
1615
+ }(window.jQuery));
1616
+
1617
+ /**
1618
+ AbstractInput - base class for all editable inputs.
1619
+ It defines interface to be implemented by any input type.
1620
+ To create your own input you can inherit from this class.
1621
+
1622
+ @class abstractinput
1623
+ **/
1624
+ (function ($) {
1625
+
1626
+ //types
1627
+ $.fn.editabletypes = {};
1628
+
1629
+ var AbstractInput = function () { };
1630
+
1631
+ AbstractInput.prototype = {
1632
+ /**
1633
+ Initializes input
1634
+
1635
+ @method init()
1636
+ **/
1637
+ init: function(type, options, defaults) {
1638
+ this.type = type;
1639
+ this.options = $.extend({}, defaults, options);
1640
+ this.$input = null;
1641
+ this.$clear = null;
1642
+ this.error = null;
1643
+ },
1644
+
1645
+ /**
1646
+ Renders input from tpl. Can return jQuery deferred object.
1647
+
1648
+ @method render()
1649
+ **/
1650
+ render: function() {
1651
+ this.$input = $(this.options.tpl);
1652
+ if(this.options.inputclass) {
1653
+ this.$input.addClass(this.options.inputclass);
1654
+ }
1655
+
1656
+ if (this.options.placeholder) {
1657
+ this.$input.attr('placeholder', this.options.placeholder);
1658
+ }
1659
+ },
1660
+
1661
+ /**
1662
+ Sets element's html by value.
1663
+
1664
+ @method value2html(value, element)
1665
+ @param {mixed} value
1666
+ @param {DOMElement} element
1667
+ **/
1668
+ value2html: function(value, element) {
1669
+ $(element).text(value);
1670
+ },
1671
+
1672
+ /**
1673
+ Converts element's html to value
1674
+
1675
+ @method html2value(html)
1676
+ @param {string} html
1677
+ @returns {mixed}
1678
+ **/
1679
+ html2value: function(html) {
1680
+ return $('<div>').html(html).text();
1681
+ },
1682
+
1683
+ /**
1684
+ Converts value to string (for internal compare). For submitting to server used value2submit().
1685
+
1686
+ @method value2str(value)
1687
+ @param {mixed} value
1688
+ @returns {string}
1689
+ **/
1690
+ value2str: function(value) {
1691
+ return value;
1692
+ },
1693
+
1694
+ /**
1695
+ Converts string received from server into value.
1696
+
1697
+ @method str2value(str)
1698
+ @param {string} str
1699
+ @returns {mixed}
1700
+ **/
1701
+ str2value: function(str) {
1702
+ return str;
1703
+ },
1704
+
1705
+ /**
1706
+ Converts value for submitting to server
1707
+
1708
+ @method value2submit(value)
1709
+ @param {mixed} value
1710
+ @returns {mixed}
1711
+ **/
1712
+ value2submit: function(value) {
1713
+ return value;
1714
+ },
1715
+
1716
+ /**
1717
+ Sets value of input.
1718
+
1719
+ @method value2input(value)
1720
+ @param {mixed} value
1721
+ **/
1722
+ value2input: function(value) {
1723
+ this.$input.val(value);
1724
+ },
1725
+
1726
+ /**
1727
+ Returns value of input. Value can be object (e.g. datepicker)
1728
+
1729
+ @method input2value()
1730
+ **/
1731
+ input2value: function() {
1732
+ return this.$input.val();
1733
+ },
1734
+
1735
+ /**
1736
+ Activates input. For text it sets focus.
1737
+
1738
+ @method activate()
1739
+ **/
1740
+ activate: function() {
1741
+ if(this.$input.is(':visible')) {
1742
+ this.$input.focus();
1743
+ }
1744
+ },
1745
+
1746
+ /**
1747
+ Creates input.
1748
+
1749
+ @method clear()
1750
+ **/
1751
+ clear: function() {
1752
+ this.$input.val(null);
1753
+ },
1754
+
1755
+ /**
1756
+ method to escape html.
1757
+ **/
1758
+ escape: function(str) {
1759
+ return $('<div>').text(str).html();
1760
+ },
1761
+
1762
+ /**
1763
+ attach handler to automatically submit form when value changed (useful when buttons not shown)
1764
+ **/
1765
+ autosubmit: function() {
1766
+
1767
+ }
1768
+ };
1769
+
1770
+ AbstractInput.defaults = {
1771
+ /**
1772
+ HTML template of input. Normally you should not change it.
1773
+
1774
+ @property tpl
1775
+ @type string
1776
+ @default ''
1777
+ **/
1778
+ tpl: '',
1779
+ /**
1780
+ CSS class automatically applied to input
1781
+
1782
+ @property inputclass
1783
+ @type string
1784
+ @default input-medium
1785
+ **/
1786
+ inputclass: 'input-medium',
1787
+ /**
1788
+ Name attribute of input
1789
+
1790
+ @property name
1791
+ @type string
1792
+ @default null
1793
+ **/
1794
+ name: null
1795
+ };
1796
+
1797
+ $.extend($.fn.editabletypes, {abstractinput: AbstractInput});
1798
+
1799
+ }(window.jQuery));
1800
+
1801
+ /**
1802
+ List - abstract class for inputs that have source option loaded from js array or via ajax
1803
+
1804
+ @class list
1805
+ @extends abstractinput
1806
+ **/
1807
+ (function ($) {
1808
+
1809
+ var List = function (options) {
1810
+
1811
+ };
1812
+
1813
+ $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput);
1814
+
1815
+ $.extend(List.prototype, {
1816
+ render: function () {
1817
+ List.superclass.render.call(this);
1818
+ var deferred = $.Deferred();
1819
+ this.error = null;
1820
+ this.sourceData = null;
1821
+ this.prependData = null;
1822
+ this.onSourceReady(function () {
1823
+ this.renderList();
1824
+ deferred.resolve();
1825
+ }, function () {
1826
+ this.error = this.options.sourceError;
1827
+ deferred.resolve();
1828
+ });
1829
+
1830
+ return deferred.promise();
1831
+ },
1832
+
1833
+ html2value: function (html) {
1834
+ return null; //can't set value by text
1835
+ },
1836
+
1837
+ value2html: function (value, element, display) {
1838
+ var deferred = $.Deferred();
1839
+ this.onSourceReady(function () {
1840
+ if(typeof display === 'function') {
1841
+ //custom display method
1842
+ display.call(element, value, this.sourceData);
1843
+ } else {
1844
+ this.value2htmlFinal(value, element);
1845
+ }
1846
+ deferred.resolve();
1847
+ }, function () {
1848
+ //do nothing with element
1849
+ deferred.resolve();
1850
+ });
1851
+
1852
+ return deferred.promise();
1853
+ },
1854
+
1855
+ // ------------- additional functions ------------
1856
+
1857
+ onSourceReady: function (success, error) {
1858
+ //if allready loaded just call success
1859
+ if($.isArray(this.sourceData)) {
1860
+ success.call(this);
1861
+ return;
1862
+ }
1863
+
1864
+ // try parse json in single quotes (for double quotes jquery does automatically)
1865
+ try {
1866
+ this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false);
1867
+ } catch (e) {
1868
+ error.call(this);
1869
+ return;
1870
+ }
1871
+
1872
+ //loading from url
1873
+ if (typeof this.options.source === 'string') {
1874
+ //try to get from cache
1875
+ if(this.options.sourceCache) {
1876
+ var cacheID = this.options.source + (this.options.name ? '-' + this.options.name : ''),
1877
+ cache;
1878
+
1879
+ if (!$(document).data(cacheID)) {
1880
+ $(document).data(cacheID, {});
1881
+ }
1882
+ cache = $(document).data(cacheID);
1883
+
1884
+ //check for cached data
1885
+ if (cache.loading === false && cache.sourceData) { //take source from cache
1886
+ this.sourceData = cache.sourceData;
1887
+ success.call(this);
1888
+ return;
1889
+ } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later
1890
+ cache.callbacks.push($.proxy(function () {
1891
+ this.sourceData = cache.sourceData;
1892
+ success.call(this);
1893
+ }, this));
1894
+
1895
+ //also collecting error callbacks
1896
+ cache.err_callbacks.push($.proxy(error, this));
1897
+ return;
1898
+ } else { //no cache yet, activate it
1899
+ cache.loading = true;
1900
+ cache.callbacks = [];
1901
+ cache.err_callbacks = [];
1902
+ }
1903
+ }
1904
+
1905
+ //loading sourceData from server
1906
+ $.ajax({
1907
+ url: this.options.source,
1908
+ type: 'get',
1909
+ cache: false,
1910
+ data: this.options.name ? {name: this.options.name} : {},
1911
+ dataType: 'json',
1912
+ success: $.proxy(function (data) {
1913
+ if(cache) {
1914
+ cache.loading = false;
1915
+ }
1916
+ this.sourceData = this.makeArray(data);
1917
+ if($.isArray(this.sourceData)) {
1918
+ this.doPrepend();
1919
+ success.call(this);
1920
+ if(cache) {
1921
+ //store result in cache
1922
+ cache.sourceData = this.sourceData;
1923
+ $.each(cache.callbacks, function () { this.call(); }); //run success callbacks for other fields
1924
+ }
1925
+ } else {
1926
+ error.call(this);
1927
+ if(cache) {
1928
+ $.each(cache.err_callbacks, function () { this.call(); }); //run error callbacks for other fields
1929
+ }
1930
+ }
1931
+ }, this),
1932
+ error: $.proxy(function () {
1933
+ error.call(this);
1934
+ if(cache) {
1935
+ cache.loading = false;
1936
+ //run error callbacks for other fields
1937
+ $.each(cache.err_callbacks, function () { this.call(); });
1938
+ }
1939
+ }, this)
1940
+ });
1941
+ } else { //options as json/array
1942
+ this.sourceData = this.makeArray(this.options.source);
1943
+ if($.isArray(this.sourceData)) {
1944
+ this.doPrepend();
1945
+ success.call(this);
1946
+ } else {
1947
+ error.call(this);
1948
+ }
1949
+ }
1950
+ },
1951
+
1952
+ doPrepend: function () {
1953
+ if(this.options.prepend === null || this.options.prepend === undefined) {
1954
+ return;
1955
+ }
1956
+
1957
+ if(!$.isArray(this.prependData)) {
1958
+ //try parse json in single quotes
1959
+ this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true);
1960
+ if (typeof this.options.prepend === 'string') {
1961
+ this.options.prepend = {'': this.options.prepend};
1962
+ }
1963
+ this.prependData = this.makeArray(this.options.prepend);
1964
+ }
1965
+
1966
+ if($.isArray(this.prependData) && $.isArray(this.sourceData)) {
1967
+ this.sourceData = this.prependData.concat(this.sourceData);
1968
+ }
1969
+ },
1970
+
1971
+ /*
1972
+ renders input list
1973
+ */
1974
+ renderList: function() {
1975
+ // this method should be overwritten in child class
1976
+ },
1977
+
1978
+ /*
1979
+ set element's html by value
1980
+ */
1981
+ value2htmlFinal: function(value, element) {
1982
+ // this method should be overwritten in child class
1983
+ },
1984
+
1985
+ /**
1986
+ * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}]
1987
+ */
1988
+ makeArray: function(data) {
1989
+ var count, obj, result = [], iterateEl;
1990
+ if(!data || typeof data === 'string') {
1991
+ return null;
1992
+ }
1993
+
1994
+ if($.isArray(data)) { //array
1995
+ iterateEl = function (k, v) {
1996
+ obj = {value: k, text: v};
1997
+ if(count++ >= 2) {
1998
+ return false;// exit each if object has more than one value
1999
+ }
2000
+ };
2001
+
2002
+ for(var i = 0; i < data.length; i++) {
2003
+ if(typeof data[i] === 'object') {
2004
+ count = 0;
2005
+ $.each(data[i], iterateEl);
2006
+ if(count === 1) {
2007
+ result.push(obj);
2008
+ } else if(count > 1 && data[i].hasOwnProperty('value') && data[i].hasOwnProperty('text')) {
2009
+ result.push(data[i]);
2010
+ } else {
2011
+ //data contains incorrect objects
2012
+ }
2013
+ } else {
2014
+ result.push({value: data[i], text: data[i]});
2015
+ }
2016
+ }
2017
+ } else { //object
2018
+ $.each(data, function (k, v) {
2019
+ result.push({value: k, text: v});
2020
+ });
2021
+ }
2022
+ return result;
2023
+ },
2024
+
2025
+ //search for item by particular value
2026
+ itemByVal: function(val) {
2027
+ if($.isArray(this.sourceData)) {
2028
+ for(var i=0; i<this.sourceData.length; i++){
2029
+ /*jshint eqeqeq: false*/
2030
+ if(this.sourceData[i].value == val) {
2031
+ /*jshint eqeqeq: true*/
2032
+ return this.sourceData[i];
2033
+ }
2034
+ }
2035
+ }
2036
+ }
2037
+
2038
+ });
2039
+
2040
+ List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2041
+ /**
2042
+ Source data for list. If string - considered ajax url to load items. Otherwise should be an array.
2043
+ Array format is: <code>[{value: 1, text: "text"}, {...}]</code><br>
2044
+ For compability it also supports format <code>{value1: "text1", value2: "text2" ...}</code> but it does not guarantee elements order.
2045
+ If source is **string**, results will be cached for fields with the same source and name. See also <code>sourceCache</code> option.
2046
+
2047
+ @property source
2048
+ @type string|array|object
2049
+ @default null
2050
+ **/
2051
+ source:null,
2052
+ /**
2053
+ Data automatically prepended to the beginning of dropdown list.
2054
+
2055
+ @property prepend
2056
+ @type string|array|object
2057
+ @default false
2058
+ **/
2059
+ prepend:false,
2060
+ /**
2061
+ Error message when list cannot be loaded (e.g. ajax error)
2062
+
2063
+ @property sourceError
2064
+ @type string
2065
+ @default Error when loading list
2066
+ **/
2067
+ sourceError: 'Error when loading list',
2068
+ /**
2069
+ if <code>true</code> and source is **string url** - results will be cached for fields with the same source and name.
2070
+ Usefull for editable grids.
2071
+
2072
+ @property sourceCache
2073
+ @type boolean
2074
+ @default true
2075
+ @since 1.2.0
2076
+ **/
2077
+ sourceCache: true
2078
+ });
2079
+
2080
+ $.fn.editabletypes.list = List;
2081
+
2082
+ }(window.jQuery));
2083
+ /**
2084
+ Text input
2085
+
2086
+ @class text
2087
+ @extends abstractinput
2088
+ @final
2089
+ @example
2090
+ <a href="#" id="username" data-type="text" data-pk="1">awesome</a>
2091
+ <script>
2092
+ $(function(){
2093
+ $('#username').editable({
2094
+ url: '/post',
2095
+ title: 'Enter username'
2096
+ });
2097
+ });
2098
+ </script>
2099
+ **/
2100
+ (function ($) {
2101
+ var Text = function (options) {
2102
+ this.init('text', options, Text.defaults);
2103
+ };
2104
+
2105
+ $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput);
2106
+
2107
+ $.extend(Text.prototype, {
2108
+ activate: function() {
2109
+ if(this.$input.is(':visible')) {
2110
+ this.$input.focus();
2111
+ $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2112
+ }
2113
+ }
2114
+ });
2115
+
2116
+ Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2117
+ /**
2118
+ @property tpl
2119
+ @default <input type="text">
2120
+ **/
2121
+ tpl: '<input type="text">',
2122
+ /**
2123
+ Placeholder attribute of input. Shown when input is empty.
2124
+
2125
+ @property placeholder
2126
+ @type string
2127
+ @default null
2128
+ **/
2129
+ placeholder: null
2130
+ });
2131
+
2132
+ $.fn.editabletypes.text = Text;
2133
+
2134
+ }(window.jQuery));
2135
+
2136
+ /**
2137
+ Textarea input
2138
+
2139
+ @class textarea
2140
+ @extends abstractinput
2141
+ @final
2142
+ @example
2143
+ <a href="#" id="comments" data-type="textarea" data-pk="1">awesome comment!</a>
2144
+ <script>
2145
+ $(function(){
2146
+ $('#comments').editable({
2147
+ url: '/post',
2148
+ title: 'Enter comments'
2149
+ });
2150
+ });
2151
+ </script>
2152
+ **/
2153
+ (function ($) {
2154
+
2155
+ var Textarea = function (options) {
2156
+ this.init('textarea', options, Textarea.defaults);
2157
+ };
2158
+
2159
+ $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput);
2160
+
2161
+ $.extend(Textarea.prototype, {
2162
+ render: function () {
2163
+ Textarea.superclass.render.call(this);
2164
+
2165
+ //ctrl + enter
2166
+ this.$input.keydown(function (e) {
2167
+ if (e.ctrlKey && e.which === 13) {
2168
+ $(this).closest('form').submit();
2169
+ }
2170
+ });
2171
+ },
2172
+
2173
+ value2html: function(value, element) {
2174
+ var html = '', lines;
2175
+ if(value) {
2176
+ lines = value.split("\n");
2177
+ for (var i = 0; i < lines.length; i++) {
2178
+ lines[i] = $('<div>').text(lines[i]).html();
2179
+ }
2180
+ html = lines.join('<br>');
2181
+ }
2182
+ $(element).html(html);
2183
+ },
2184
+
2185
+ html2value: function(html) {
2186
+ if(!html) {
2187
+ return '';
2188
+ }
2189
+ var lines = html.split(/<br\s*\/?>/i);
2190
+ for (var i = 0; i < lines.length; i++) {
2191
+ lines[i] = $('<div>').html(lines[i]).text();
2192
+ }
2193
+ return lines.join("\n");
2194
+ },
2195
+
2196
+ activate: function() {
2197
+ if(this.$input.is(':visible')) {
2198
+ $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length);
2199
+ this.$input.focus();
2200
+ }
2201
+ }
2202
+ });
2203
+
2204
+ Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2205
+ /**
2206
+ @property tpl
2207
+ @default <textarea></textarea>
2208
+ **/
2209
+ tpl:'<textarea></textarea>',
2210
+ /**
2211
+ @property inputclass
2212
+ @default input-large
2213
+ **/
2214
+ inputclass: 'input-large',
2215
+ /**
2216
+ Placeholder attribute of input. Shown when input is empty.
2217
+
2218
+ @property placeholder
2219
+ @type string
2220
+ @default null
2221
+ **/
2222
+ placeholder: null
2223
+ });
2224
+
2225
+ $.fn.editabletypes.textarea = Textarea;
2226
+
2227
+ }(window.jQuery));
2228
+
2229
+ /**
2230
+ Select (dropdown)
2231
+
2232
+ @class select
2233
+ @extends list
2234
+ @final
2235
+ @example
2236
+ <a href="#" id="status" data-type="select" data-pk="1" data-url="/post" data-original-title="Select status"></a>
2237
+ <script>
2238
+ $(function(){
2239
+ $('#status').editable({
2240
+ value: 2,
2241
+ source: [
2242
+ {value: 1, text: 'Active'},
2243
+ {value: 2, text: 'Blocked'},
2244
+ {value: 3, text: 'Deleted'}
2245
+ ]
2246
+ }
2247
+ });
2248
+ });
2249
+ </script>
2250
+ **/
2251
+ (function ($) {
2252
+
2253
+ var Select = function (options) {
2254
+ this.init('select', options, Select.defaults);
2255
+ };
2256
+
2257
+ $.fn.editableutils.inherit(Select, $.fn.editabletypes.list);
2258
+
2259
+ $.extend(Select.prototype, {
2260
+ renderList: function() {
2261
+ if(!$.isArray(this.sourceData)) {
2262
+ return;
2263
+ }
2264
+
2265
+ for(var i=0; i<this.sourceData.length; i++) {
2266
+ this.$input.append($('<option>', {value: this.sourceData[i].value}).text(this.sourceData[i].text));
2267
+ }
2268
+
2269
+ //enter submit
2270
+ this.$input.on('keydown.editable', function (e) {
2271
+ if (e.which === 13) {
2272
+ $(this).closest('form').submit();
2273
+ }
2274
+ });
2275
+ },
2276
+
2277
+ value2htmlFinal: function(value, element) {
2278
+ var text = '', item = this.itemByVal(value);
2279
+ if(item) {
2280
+ text = item.text;
2281
+ }
2282
+ Select.superclass.constructor.superclass.value2html(text, element);
2283
+ },
2284
+
2285
+ autosubmit: function() {
2286
+ this.$input.off('keydown.editable').on('change.editable', function(){
2287
+ $(this).closest('form').submit();
2288
+ });
2289
+ }
2290
+ });
2291
+
2292
+ Select.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
2293
+ /**
2294
+ @property tpl
2295
+ @default <select></select>
2296
+ **/
2297
+ tpl:'<select></select>'
2298
+ });
2299
+
2300
+ $.fn.editabletypes.select = Select;
2301
+
2302
+ }(window.jQuery));
2303
+ /**
2304
+ List of checkboxes.
2305
+ Internally value stored as javascript array of values.
2306
+
2307
+ @class checklist
2308
+ @extends list
2309
+ @final
2310
+ @example
2311
+ <a href="#" id="options" data-type="checklist" data-pk="1" data-url="/post" data-original-title="Select options"></a>
2312
+ <script>
2313
+ $(function(){
2314
+ $('#options').editable({
2315
+ value: [2, 3],
2316
+ source: [
2317
+ {value: 1, text: 'option1'},
2318
+ {value: 2, text: 'option2'},
2319
+ {value: 3, text: 'option3'}
2320
+ ]
2321
+ }
2322
+ });
2323
+ });
2324
+ </script>
2325
+ **/
2326
+ (function ($) {
2327
+
2328
+ var Checklist = function (options) {
2329
+ this.init('checklist', options, Checklist.defaults);
2330
+ };
2331
+
2332
+ $.fn.editableutils.inherit(Checklist, $.fn.editabletypes.list);
2333
+
2334
+ $.extend(Checklist.prototype, {
2335
+ renderList: function() {
2336
+ var $label, $div;
2337
+ if(!$.isArray(this.sourceData)) {
2338
+ return;
2339
+ }
2340
+
2341
+ for(var i=0; i<this.sourceData.length; i++) {
2342
+ $label = $('<label>').append($('<input>', {
2343
+ type: 'checkbox',
2344
+ value: this.sourceData[i].value,
2345
+ name: this.options.name
2346
+ }))
2347
+ .append($('<span>').text(' '+this.sourceData[i].text));
2348
+
2349
+ $('<div>').append($label).appendTo(this.$input);
2350
+ }
2351
+ },
2352
+
2353
+ value2str: function(value) {
2354
+ return $.isArray(value) ? value.sort().join($.trim(this.options.separator)) : '';
2355
+ },
2356
+
2357
+ //parse separated string
2358
+ str2value: function(str) {
2359
+ var reg, value = null;
2360
+ if(typeof str === 'string' && str.length) {
2361
+ reg = new RegExp('\\s*'+$.trim(this.options.separator)+'\\s*');
2362
+ value = str.split(reg);
2363
+ } else if($.isArray(str)) {
2364
+ value = str;
2365
+ }
2366
+ return value;
2367
+ },
2368
+
2369
+ //set checked on required checkboxes
2370
+ value2input: function(value) {
2371
+ var $checks = this.$input.find('input[type="checkbox"]');
2372
+ $checks.removeAttr('checked');
2373
+ if($.isArray(value) && value.length) {
2374
+ $checks.each(function(i, el) {
2375
+ var $el = $(el);
2376
+ // cannot use $.inArray as it performs strict comparison
2377
+ $.each(value, function(j, val){
2378
+ /*jslint eqeq: true*/
2379
+ if($el.val() == val) {
2380
+ /*jslint eqeq: false*/
2381
+ $el.attr('checked', 'checked');
2382
+ }
2383
+ });
2384
+ });
2385
+ }
2386
+ },
2387
+
2388
+ input2value: function() {
2389
+ var checked = [];
2390
+ this.$input.find('input:checked').each(function(i, el) {
2391
+ checked.push($(el).val());
2392
+ });
2393
+ return checked;
2394
+ },
2395
+
2396
+ //collect text of checked boxes
2397
+ value2htmlFinal: function(value, element) {
2398
+ var html = [],
2399
+ /*jslint eqeq: true*/
2400
+ checked = $.grep(this.sourceData, function(o){
2401
+ return $.grep(value, function(v){ return v == o.value; }).length;
2402
+ });
2403
+ /*jslint eqeq: false*/
2404
+ if(checked.length) {
2405
+ $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
2406
+ $(element).html(html.join('<br>'));
2407
+ } else {
2408
+ $(element).empty();
2409
+ }
2410
+ },
2411
+
2412
+ activate: function() {
2413
+ this.$input.find('input[type="checkbox"]').first().focus();
2414
+ },
2415
+
2416
+ autosubmit: function() {
2417
+ this.$input.find('input[type="checkbox"]').on('keydown', function(e){
2418
+ if (e.which === 13) {
2419
+ $(this).closest('form').submit();
2420
+ }
2421
+ });
2422
+ }
2423
+ });
2424
+
2425
+ Checklist.defaults = $.extend({}, $.fn.editabletypes.list.defaults, {
2426
+ /**
2427
+ @property tpl
2428
+ @default <div></div>
2429
+ **/
2430
+ tpl:'<div></div>',
2431
+
2432
+ /**
2433
+ @property inputclass
2434
+ @type string
2435
+ @default editable-checklist
2436
+ **/
2437
+ inputclass: 'editable-checklist',
2438
+
2439
+ /**
2440
+ Separator of values when reading from 'data-value' string
2441
+
2442
+ @property separator
2443
+ @type string
2444
+ @default ', '
2445
+ **/
2446
+ separator: ','
2447
+ });
2448
+
2449
+ $.fn.editabletypes.checklist = Checklist;
2450
+
2451
+ }(window.jQuery));
2452
+
2453
+ /**
2454
+ HTML5 input types.
2455
+ Following types are supported:
2456
+
2457
+ * password
2458
+ * email
2459
+ * url
2460
+ * tel
2461
+ * number
2462
+ * range
2463
+
2464
+ Learn more about html5 inputs:
2465
+ http://www.w3.org/wiki/HTML5_form_additions
2466
+ To check browser compatibility please see:
2467
+ https://developer.mozilla.org/en-US/docs/HTML/Element/Input
2468
+
2469
+ @class html5types
2470
+ @extends text
2471
+ @final
2472
+ @since 1.3.0
2473
+ @example
2474
+ <a href="#" id="email" data-type="email" data-pk="1">admin@example.com</a>
2475
+ <script>
2476
+ $(function(){
2477
+ $('#email').editable({
2478
+ url: '/post',
2479
+ title: 'Enter email'
2480
+ });
2481
+ });
2482
+ </script>
2483
+ **/
2484
+
2485
+ /**
2486
+ @property tpl
2487
+ @default depends on type
2488
+ **/
2489
+
2490
+ /*
2491
+ Password
2492
+ */
2493
+ (function ($) {
2494
+ var Password = function (options) {
2495
+ this.init('password', options, Password.defaults);
2496
+ };
2497
+ $.fn.editableutils.inherit(Password, $.fn.editabletypes.text);
2498
+ $.extend(Password.prototype, {
2499
+ //do not display password, show '[hidden]' instead
2500
+ value2html: function(value, element) {
2501
+ if(value) {
2502
+ $(element).text('[hidden]');
2503
+ } else {
2504
+ $(element).empty();
2505
+ }
2506
+ },
2507
+ //as password not displayed, should not set value by html
2508
+ html2value: function(html) {
2509
+ return null;
2510
+ }
2511
+ });
2512
+ Password.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2513
+ tpl: '<input type="password">'
2514
+ });
2515
+ $.fn.editabletypes.password = Password;
2516
+ }(window.jQuery));
2517
+
2518
+
2519
+ /*
2520
+ Email
2521
+ */
2522
+ (function ($) {
2523
+ var Email = function (options) {
2524
+ this.init('email', options, Email.defaults);
2525
+ };
2526
+ $.fn.editableutils.inherit(Email, $.fn.editabletypes.text);
2527
+ Email.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2528
+ tpl: '<input type="email">'
2529
+ });
2530
+ $.fn.editabletypes.email = Email;
2531
+ }(window.jQuery));
2532
+
2533
+
2534
+ /*
2535
+ Url
2536
+ */
2537
+ (function ($) {
2538
+ var Url = function (options) {
2539
+ this.init('url', options, Url.defaults);
2540
+ };
2541
+ $.fn.editableutils.inherit(Url, $.fn.editabletypes.text);
2542
+ Url.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2543
+ tpl: '<input type="url">'
2544
+ });
2545
+ $.fn.editabletypes.url = Url;
2546
+ }(window.jQuery));
2547
+
2548
+
2549
+ /*
2550
+ Tel
2551
+ */
2552
+ (function ($) {
2553
+ var Tel = function (options) {
2554
+ this.init('tel', options, Tel.defaults);
2555
+ };
2556
+ $.fn.editableutils.inherit(Tel, $.fn.editabletypes.text);
2557
+ Tel.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2558
+ tpl: '<input type="tel">'
2559
+ });
2560
+ $.fn.editabletypes.tel = Tel;
2561
+ }(window.jQuery));
2562
+
2563
+
2564
+ /*
2565
+ Number
2566
+ */
2567
+ (function ($) {
2568
+ var NumberInput = function (options) {
2569
+ this.init('number', options, NumberInput.defaults);
2570
+ };
2571
+ $.fn.editableutils.inherit(NumberInput, $.fn.editabletypes.text);
2572
+ $.extend(NumberInput.prototype, {
2573
+ render: function () {
2574
+ NumberInput.superclass.render.call(this);
2575
+
2576
+ if (this.options.min !== null) {
2577
+ this.$input.attr('min', this.options.min);
2578
+ }
2579
+
2580
+ if (this.options.max !== null) {
2581
+ this.$input.attr('max', this.options.max);
2582
+ }
2583
+
2584
+ if (this.options.step !== null) {
2585
+ this.$input.attr('step', this.options.step);
2586
+ }
2587
+ }
2588
+ });
2589
+ NumberInput.defaults = $.extend({}, $.fn.editabletypes.text.defaults, {
2590
+ tpl: '<input type="number">',
2591
+ inputclass: 'input-mini',
2592
+ min: null,
2593
+ max: null,
2594
+ step: null
2595
+ });
2596
+ $.fn.editabletypes.number = NumberInput;
2597
+ }(window.jQuery));
2598
+
2599
+
2600
+ /*
2601
+ Range (inherit from number)
2602
+ */
2603
+ (function ($) {
2604
+ var Range = function (options) {
2605
+ this.init('range', options, Range.defaults);
2606
+ };
2607
+ $.fn.editableutils.inherit(Range, $.fn.editabletypes.number);
2608
+ $.extend(Range.prototype, {
2609
+ render: function () {
2610
+ this.$input = $(this.options.tpl);
2611
+ var $slider = this.$input.filter('input');
2612
+ if(this.options.inputclass) {
2613
+ $slider.addClass(this.options.inputclass);
2614
+ }
2615
+ if (this.options.min !== null) {
2616
+ $slider.attr('min', this.options.min);
2617
+ }
2618
+
2619
+ if (this.options.max !== null) {
2620
+ $slider.attr('max', this.options.max);
2621
+ }
2622
+
2623
+ if (this.options.step !== null) {
2624
+ $slider.attr('step', this.options.step);
2625
+ }
2626
+
2627
+ $slider.on('input', function(){
2628
+ $(this).siblings('output').text($(this).val());
2629
+ });
2630
+ },
2631
+ activate: function() {
2632
+ this.$input.filter('input').focus();
2633
+ }
2634
+ });
2635
+ Range.defaults = $.extend({}, $.fn.editabletypes.number.defaults, {
2636
+ tpl: '<input type="range"><output style="width: 30px; display: inline-block"></output>',
2637
+ inputclass: 'input-medium'
2638
+ });
2639
+ $.fn.editabletypes.range = Range;
2640
+ }(window.jQuery));
2641
+ /*
2642
+ Editableform based on Twitter Bootstrap
2643
+ */
2644
+ (function ($) {
2645
+
2646
+ $.extend($.fn.editableform.Constructor.prototype, {
2647
+ initTemplate: function() {
2648
+ this.$form = $($.fn.editableform.template);
2649
+ this.$form.find('.editable-error-block').addClass('help-block');
2650
+ }
2651
+ });
2652
+
2653
+ //buttons
2654
+ $.fn.editableform.buttons = '<button type="submit" class="btn btn-primary editable-submit"><i class="icon-ok icon-white"></i></button>'+
2655
+ '<button type="button" class="btn editable-cancel"><i class="icon-remove"></i></button>';
2656
+
2657
+ //error classes
2658
+ $.fn.editableform.errorGroupClass = 'error';
2659
+ $.fn.editableform.errorBlockClass = null;
2660
+
2661
+ }(window.jQuery));
2662
+ /**
2663
+ * Editable Inline
2664
+ * ---------------------
2665
+ */
2666
+ (function ($) {
2667
+
2668
+ //extend methods
2669
+ $.extend($.fn.editableContainer.Constructor.prototype, {
2670
+ containerName: 'editableform',
2671
+ innerCss: null,
2672
+
2673
+ initContainer: function(){
2674
+ //no init for container
2675
+ //only convert anim to miliseconds (int)
2676
+ if(!this.options.anim) {
2677
+ this.options.anim = 0;
2678
+ }
2679
+ },
2680
+
2681
+ splitOptions: function() {
2682
+ this.containerOptions = {};
2683
+ this.formOptions = this.options;
2684
+ },
2685
+
2686
+ tip: function() {
2687
+ return this.$form;
2688
+ },
2689
+
2690
+ innerShow: function () {
2691
+ this.$element.hide();
2692
+
2693
+ if(this.$form) {
2694
+ this.$form.remove();
2695
+ }
2696
+
2697
+ this.initForm();
2698
+ this.tip().addClass('editable-container').addClass('editable-inline');
2699
+ this.$form.insertAfter(this.$element);
2700
+ this.$form.show(this.options.anim);
2701
+ this.$form.editableform('render');
2702
+ },
2703
+
2704
+ innerHide: function () {
2705
+ this.$form.hide(this.options.anim, $.proxy(function() {
2706
+ this.$element.show();
2707
+ }, this));
2708
+ },
2709
+
2710
+ destroy: function() {
2711
+ this.tip().remove();
2712
+ }
2713
+ });
2714
+
2715
+ //defaults
2716
+ $.fn.editableContainer.defaults = $.extend({}, $.fn.editableContainer.defaults, {
2717
+ anim: 'fast'
2718
+ });
2719
+
2720
+
2721
+ }(window.jQuery));
2722
+ /**
2723
+ Bootstrap-datepicker.
2724
+ Description and examples: http://vitalets.github.com/bootstrap-datepicker.
2725
+ For localization you can include js file from here: https://github.com/eternicode/bootstrap-datepicker/tree/master/js/locales
2726
+
2727
+ @class date
2728
+ @extends abstractinput
2729
+ @final
2730
+ @example
2731
+ <a href="#" id="dob" data-type="date" data-pk="1" data-url="/post" data-original-title="Select date">15/05/1984</a>
2732
+ <script>
2733
+ $(function(){
2734
+ $('#dob').editable({
2735
+ format: 'yyyy-mm-dd',
2736
+ viewformat: 'dd/mm/yyyy',
2737
+ datepicker: {
2738
+ weekStart: 1
2739
+ }
2740
+ }
2741
+ });
2742
+ });
2743
+ </script>
2744
+ **/
2745
+ (function ($) {
2746
+
2747
+ var Date = function (options) {
2748
+ this.init('date', options, Date.defaults);
2749
+
2750
+ //set popular options directly from settings or data-* attributes
2751
+ var directOptions = $.fn.editableutils.sliceObj(this.options, ['format']);
2752
+
2753
+ //overriding datepicker config (as by default jQuery extend() is not recursive)
2754
+ this.options.datepicker = $.extend({}, Date.defaults.datepicker, directOptions, options.datepicker);
2755
+
2756
+ //by default viewformat equals to format
2757
+ if(!this.options.viewformat) {
2758
+ this.options.viewformat = this.options.datepicker.format;
2759
+ }
2760
+
2761
+ //language
2762
+ this.options.datepicker.language = this.options.datepicker.language || 'en';
2763
+
2764
+ //store DPglobal
2765
+ this.dpg = $.fn.datepicker.DPGlobal;
2766
+
2767
+ //store parsed formats
2768
+ this.parsedFormat = this.dpg.parseFormat(this.options.datepicker.format);
2769
+ this.parsedViewFormat = this.dpg.parseFormat(this.options.viewformat);
2770
+ };
2771
+
2772
+ $.fn.editableutils.inherit(Date, $.fn.editabletypes.abstractinput);
2773
+
2774
+ $.extend(Date.prototype, {
2775
+ render: function () {
2776
+ Date.superclass.render.call(this);
2777
+ this.$input.datepicker(this.options.datepicker);
2778
+
2779
+ if(this.options.clear) {
2780
+ this.$clear = $('<a href="#"></a>').html(this.options.clear).click($.proxy(function(e){
2781
+ e.preventDefault();
2782
+ e.stopPropagation();
2783
+ this.clear();
2784
+ }, this));
2785
+ }
2786
+ },
2787
+
2788
+ value2html: function(value, element) {
2789
+ var text = value ? this.dpg.formatDate(value, this.parsedViewFormat, this.options.datepicker.language) : '';
2790
+ Date.superclass.value2html(text, element);
2791
+ },
2792
+
2793
+ html2value: function(html) {
2794
+ return html ? this.dpg.parseDate(html, this.parsedViewFormat, this.options.datepicker.language) : null;
2795
+ },
2796
+
2797
+ value2str: function(value) {
2798
+ return value ? this.dpg.formatDate(value, this.parsedFormat, this.options.datepicker.language) : '';
2799
+ },
2800
+
2801
+ str2value: function(str) {
2802
+ return str ? this.dpg.parseDate(str, this.parsedFormat, this.options.datepicker.language) : null;
2803
+ },
2804
+
2805
+ value2submit: function(value) {
2806
+ return this.value2str(value);
2807
+ },
2808
+
2809
+ value2input: function(value) {
2810
+ this.$input.datepicker('update', value);
2811
+ },
2812
+
2813
+ input2value: function() {
2814
+ return this.$input.data('datepicker').date;
2815
+ },
2816
+
2817
+ activate: function() {
2818
+ },
2819
+
2820
+ clear: function() {
2821
+ this.$input.data('datepicker').date = null;
2822
+ this.$input.find('.active').removeClass('active');
2823
+ },
2824
+
2825
+ autosubmit: function() {
2826
+ this.$input.on('changeDate', function(e){
2827
+ var $form = $(this).closest('form');
2828
+ setTimeout(function() {
2829
+ $form.submit();
2830
+ }, 200);
2831
+ });
2832
+ }
2833
+
2834
+ });
2835
+
2836
+ Date.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, {
2837
+ /**
2838
+ @property tpl
2839
+ @default <div></div>
2840
+ **/
2841
+ tpl:'<div></div>',
2842
+ /**
2843
+ @property inputclass
2844
+ @default editable-date well
2845
+ **/
2846
+ inputclass: 'editable-date well',
2847
+ /**
2848
+ Format used for sending value to server. Also applied when converting date from <code>data-value</code> attribute.<br>
2849
+ Possible tokens are: <code>d, dd, m, mm, yy, yyyy</code>
2850
+
2851
+ @property format
2852
+ @type string
2853
+ @default yyyy-mm-dd
2854
+ **/
2855
+ format:'yyyy-mm-dd',
2856
+ /**
2857
+ Format used for displaying date. Also applied when converting date from element's text on init.
2858
+ If not specified equals to <code>format</code>
2859
+
2860
+ @property viewformat
2861
+ @type string
2862
+ @default null
2863
+ **/
2864
+ viewformat: null,
2865
+ /**
2866
+ Configuration of datepicker.
2867
+ Full list of options: http://vitalets.github.com/bootstrap-datepicker
2868
+
2869
+ @property datepicker
2870
+ @type object
2871
+ @default {
2872
+ weekStart: 0,
2873
+ startView: 0,
2874
+ autoclose: false
2875
+ }
2876
+ **/
2877
+ datepicker:{
2878
+ weekStart: 0,
2879
+ startView: 0,
2880
+ autoclose: false
2881
+ },
2882
+ /**
2883
+ Text shown as clear date button.
2884
+ If <code>false</code> clear button will not be rendered.
2885
+
2886
+ @property clear
2887
+ @type boolean|string
2888
+ @default 'x clear'
2889
+ **/
2890
+ clear: '&times; clear'
2891
+ });
2892
+
2893
+ $.fn.editabletypes.date = Date;
2894
+
2895
+ }(window.jQuery));