ab_admin 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +4 -2
  3. data/app/assets/javascripts/ab_admin/components/google_translate.js.coffee +1 -1
  4. data/app/assets/javascripts/ab_admin/components/init_nested_filelds.js.coffee +2 -2
  5. data/app/assets/javascripts/ab_admin/core/batch_actions.js.coffee +3 -3
  6. data/app/assets/javascripts/ab_admin/core/init.js.coffee +5 -3
  7. data/app/assets/javascripts/ab_admin/core/search_form.js.coffee +2 -2
  8. data/app/assets/javascripts/ab_admin/core/ui_utils.js.coffee +14 -2
  9. data/app/assets/javascripts/ab_admin/core/utils.js.coffee +4 -3
  10. data/app/assets/javascripts/ab_admin/main.js +3 -1
  11. data/app/assets/stylesheets/ab_admin/bootstrap_and_overrides.css.scss +5 -4
  12. data/app/assets/stylesheets/ab_admin/components/_text_styles.css.scss +47 -0
  13. data/app/assets/stylesheets/ab_admin/main.css.scss +1 -1
  14. data/app/controllers/admin/base_controller.rb +1 -1
  15. data/app/views/admin/base/edit.js.erb +1 -1
  16. data/app/views/admin/base/new.js.erb +1 -1
  17. data/app/views/admin/base/update.js.erb +1 -1
  18. data/app/views/admin/fileupload/_container.html.slim +4 -2
  19. data/app/views/admin/fileupload/_file.html.slim +1 -1
  20. data/app/views/admin/fileupload/_ftmpl.html.slim +1 -1
  21. data/app/views/admin/manager/_search_form.html.slim +1 -1
  22. data/app/views/admin/shared/_batch_actions.html.slim +9 -10
  23. data/app/views/layouts/admin/application.html.slim +3 -5
  24. data/config/locales/ru.yml +3 -2
  25. data/features/menu.feature +7 -2
  26. data/features/step_definitions/menu_steps.rb +7 -0
  27. data/lib/ab_admin.rb +3 -3
  28. data/lib/ab_admin/controllers/head_options.rb +2 -2
  29. data/lib/ab_admin/menu_builder.rb +2 -1
  30. data/lib/ab_admin/models/attachment_file.rb +1 -2
  31. data/lib/ab_admin/models/user.rb +1 -1
  32. data/lib/ab_admin/version.rb +1 -1
  33. data/lib/ab_admin/views/admin_helpers.rb +7 -4
  34. data/lib/ab_admin/views/admin_navigation_helpers.rb +4 -4
  35. data/lib/ab_admin/views/form_builder.rb +2 -2
  36. data/lib/generators/template.rb +0 -1
  37. data/spec/ab_admin_spec.rb +2 -2
  38. data/vendor/assets/images/ab_admin/clear.png +0 -0
  39. data/vendor/assets/images/ab_admin/loading.gif +0 -0
  40. data/vendor/assets/javascripts/ab_admin/bootstrap-editable-inline.js +2895 -0
  41. data/vendor/assets/javascripts/ab_admin/bootstrap-editable.js +4523 -0
  42. data/vendor/assets/javascripts/ab_admin/jquery_nested_form.js.coffee +19 -1
  43. data/vendor/assets/javascripts/jquery/jquery-ui.min.js +15 -0
  44. data/vendor/assets/javascripts/jquery/jquery.min.js +4 -0
  45. data/vendor/assets/stylesheets/ab_admin/bootstrap-editable.scss +461 -0
  46. metadata +12 -4
@@ -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));