best_in_placeish 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ .DS_Store
2
+ .bundle
3
+ pkg/*
4
+
5
+ Gemfile.lock
6
+ test_app/.bundle
7
+ test_app/db/*.sqlite3
8
+ test_app/log/*.log
9
+ test_app/tmp/**/*
10
+
11
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in best_in_place.gemspec
4
+ gemspec
5
+
6
+ gem 'sqlite3-ruby', :require => 'sqlite3'
data/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # Best In Placeish
2
+ Forked from the rails-3.0 branch of best_in_place, this includes a couple of key enhancements for Rails 3.0.x users
3
+
4
+ **Additional features not found in best_in_place-0.2.2:**
5
+
6
+ - Integrated **:display_as** functionality from master branch of best_in_place
7
+ - Added purr notifications for successful updates (see updated usage notes for example)
8
+
9
+ Most of the read-me below is a direct copy of best_in_place's read-me, as the vast majority of the usage is the same, though the added features are also noted below.
10
+
11
+
12
+ ##Description
13
+
14
+ **Best in Place** is a jQuery based AJAX Inplace-Editor that takes profit of RESTful server-side controllers to allow users to edit stuff with
15
+ no need of forms. If the server has standard defined REST methods, particularly those to UPDATE your objects (HTTP PUT), adding the
16
+ Javascript file to the application will make all the fields with the proper defined classes in-place editable.
17
+
18
+ The editor works by PUTting the updated value to the server and GETting the updated record afterwards to display the updated value.
19
+
20
+ ---
21
+
22
+ ##Features
23
+
24
+ - Compatible with text **inputs**
25
+ - Compatible with **textarea**
26
+ - Compatible with **select** dropdown with custom collections
27
+ - Compatible with custom boolean values (same usage of **checkboxes**)
28
+ - Sanitize HTML and trim spaces of user's input on user's choice
29
+ - Displays server-side **validation** errors
30
+ - Allows external activator
31
+ - ESC key destroys changes (requires user confirmation)
32
+ - Autogrowing textarea
33
+
34
+ ---
35
+
36
+ ##Usage of Rails 3 Gem
37
+
38
+ **best_in_place object, field, OPTIONS**
39
+
40
+ Params:
41
+
42
+ - **object** (Mandatory): The Object parameter represents the object you are about to modify
43
+ - **field** (Mandatory): The field (passed as symbol) is the attribute of the Object you are going to display/edit.
44
+
45
+ Options:
46
+
47
+ - **:type** Accepts [:input, :textarea, :select, :checkbox] or if undefined it defaults to :input.
48
+ - **:collection**: In case you are using the :select type then you must specify the collection of values to select from. If you are
49
+ using the :checkbox type you can specify the two values it can take, otherwise it will default to Yes and No.
50
+ - **:path**: URL to which the updating action will be sent. If not defined it defaults to the :object path.
51
+ - **:nil**: The nil param defines the content displayed in case no value is defined for that field. It can be something like "click me to edit".
52
+ If undefined it will show *"-"*.
53
+ - **:activator**: The DOM object that can activate the field. If not defined the field will become editable by clicking on it.
54
+ - **:sanitize**: True by default. If set to false the input/textarea will accept html tags.
55
+ - **:html_args**: Hash of html arguments, such as maxlength, default-value etc.
56
+ - **:display_as**: A model method which will be called in order to display this field.
57
+
58
+
59
+ Examples (code in the views):
60
+
61
+ ### Input
62
+
63
+ <%= best_in_place @user, :name, :type => :input %>
64
+
65
+ <%= best_in_place @user, :name, :type => :input, :nil => "Click me to add content!" %>
66
+
67
+ ### Textarea
68
+
69
+ <%= best_in_place @user, :description, :type => :textarea %>
70
+
71
+ ### Select
72
+
73
+ <%= best_in_place @user, :country, :type => :select, :collection => [[1, "Spain"], [2, "Italy"], [3, "Germany"], [4, "France"]] %>
74
+
75
+ Of course it can take an instance or global variable for the collection, just remember the structure [[key, value], [key, value],...].
76
+ The key can be a string or an integer.
77
+
78
+ ### Checkbox
79
+
80
+ <%= best_in_place @user, :receive_emails, :type => :checkbox, :collection => ["No, thanks", "Yes, of course!"] %>
81
+
82
+ The first value is always the negative boolean value and the second the positive. Structure: ["false value", "true value"].
83
+ If not defined, it will default to *Yes* and *No* options.
84
+
85
+ ### Display server validation errors
86
+
87
+ If you are using a Rails application, your controllers should respond to json. **best_in_placeish adds an additional step here, requiring a hash containing your success message to be displayed using Purr.**
88
+ Example:
89
+ Class UsersController
90
+ respond_to :html, :json
91
+
92
+ def update
93
+ @user = User.find(params[:id])
94
+ respond_with(@user) do |format|
95
+ if @user.update_attributes(params[:user])
96
+ format.html { redirect_to(@user) }
97
+ format.json { render :json => { :success=>"User was successfully updated." }, :status => :ok }
98
+ else
99
+ format.html { render :action => "edit" }
100
+ format.json { render :json => @user.errors.full_messages, :status => :unprocessable_entity }
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+
107
+ At the same time, you must define the restrictions, validations and error messages in the model, as the example below:
108
+
109
+ class User < ActiveRecord::Base
110
+ validates :name,
111
+ :length => { :minimum => 2, :maximum => 24, :message => "has invalid length"},
112
+ :presence => {:message => "can't be blank"}
113
+ validates :last_name,
114
+ :length => { :minimum => 2, :maximum => 24, :message => "has invalid length"},
115
+ :presence => {:message => "can't be blank"}
116
+ validates :address,
117
+ :length => { :minimum => 5, :message => "too short length"},
118
+ :presence => {:message => "can't be blank"}
119
+ validates :email,
120
+ :presence => {:message => "can't be blank"},
121
+ :format => {:with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :message => "has wrong email format"}
122
+ validates :zip, :numericality => true, :length => { :minimum => 5 }
123
+ end
124
+
125
+ When the user tries to introduce invalid data, the error messages defined in the model will be displayed in pop-up windows using the jQuery.purr plugin.
126
+
127
+ ---
128
+
129
+ ## Custom display methods
130
+
131
+ ### Using `display_as`
132
+
133
+ You can use custom methods in your model in order to
134
+ decide how a certain field has to be displayed. You can write something like:
135
+
136
+ = best_in_place @user, :description, :type => :textarea, :display_as => :mk_description
137
+
138
+ Then instead of using `@user.description` to show the actual value, best in
139
+ place will call `@user.mk_description`. This can be used for any kind of
140
+ custom formatting, text with markdown, etc...
141
+
142
+ ##Installation
143
+
144
+ It works by simply copying and loading the files from the folder **/public/javascripts** to your application and loading them in your layouts
145
+ in the following order:
146
+
147
+ - jquery-1.4.4.js
148
+ - jquery.purr.js
149
+ - **best_in_place.js**
150
+
151
+ The last one you can copy it (and keeping up to date to the last version) by running the following generator in your application's root.
152
+ Remember to do it every time you update the gem (or you will see no change).
153
+
154
+ rails g best_in_place:setup
155
+
156
+ To be able to use the script the following block must be added as well:
157
+
158
+ $(document).ready(function() {
159
+ /* Activating Best In Place */
160
+ jQuery(".best_in_place").best_in_place()
161
+ });
162
+
163
+ In order to use the Rails 3 gem, just add the following line to the gemfile:
164
+
165
+ gem "best_in_placeish"
166
+
167
+ ----
168
+
169
+ ## Security
170
+
171
+ If the script is used with the Rails Gem no html tags will be allowed unless the sanitize option is set to true, in that case only the tags [*b i u s a strong em p h1 h2 h3 h4 h5 ul li ol hr pre span img*] will be allowed. If the script is used without the gem and with frameworks other than Rails, then you should make sure you are providing the csrf authenticity params as meta tags and you should always escape undesired html tags such as script, object and so forth.
172
+
173
+ <meta name="csrf-param" content="authenticity_token"/>
174
+ <meta name="csrf-token" content="YOUR UNIQUE TOKEN HERE"/>
175
+
176
+
177
+ ---
178
+
179
+ ##Changelog
180
+
181
+ - v.0.1.0 Initial deploy
182
+ - v.0.1.2 Fixing errors in collections (taken value[0] instead of index) and fixing test_app controller responses
183
+ - v.0.1.3 Bug in Rails Helper. Key wrongly considered an Integer.
184
+ - v.0.1.4 Adding two new parameters for further customization urlObject and nilValue and making input update on blur.
185
+ - v.0.1.5 **Attention: this release is not backwards compatible**. Changing params from list to option hash, helper's refactoring,
186
+ fixing bug with objects inside namespaces, adding feature for passing an external activator handler as param. Adding feature
187
+ of key ESCAPE for destroying changes before they are made permanent (in inputs and textarea).
188
+ - v.0.1.6-0.1.7 Avoiding request when the input is not modified and allowing the user to not sanitize input data.
189
+ - v.0.1.8 jslint compliant, sanitizing tags in the gem, getting right csrf params, controlling size of textarea (elastic script, for autogrowing textarea)
190
+
191
+ ##Authors, License and Stuff
192
+
193
+ Code by [Bernat Farrero](http://bernatfarrero.com) from [Itnig Web Services](http://itnig.net) (it was based on the [original project](http://github.com/janv/rest_in_place/) of Jan Varwig) and released under [MIT license](http://www.opensource.org/licenses/mit-license.php).
194
+
195
+ Many thanks to the contributors: [Roger Campos](http://github.com/rogercampos) and [Jack Senechal](https://github.com/jacksenechal)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ require "best_in_place/utils"
2
+ require "best_in_place/helper"
3
+
4
+ module BestInPlace
5
+ autoload :TestHelpers, "best_in_place/test_helpers"
6
+ end
7
+
8
+ ActionView::Base.send(:include, BestInPlace::BestInPlaceHelpers)
@@ -0,0 +1,16 @@
1
+ module BestInPlace
2
+ module DisplayMethods
3
+ extend self
4
+
5
+ @@table = Hash.new { |h,k| h[k] = Hash.new(&h.default_proc) }
6
+
7
+ def lookup(klass, attr)
8
+ foo = @@table[klass.to_s][attr.to_s]
9
+ foo == {} ? nil : foo
10
+ end
11
+
12
+ def add(klass, attr, display_as)
13
+ @@table[klass.to_s][attr.to_s] = display_as.to_sym
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ module BestInPlace
2
+ module BestInPlaceHelpers
3
+
4
+ def best_in_place(object, field, opts = {})
5
+ opts[:type] ||= :input
6
+ opts[:collection] ||= []
7
+ field = field.to_s
8
+ if opts[:display_as]
9
+ value = object.send(opts[:display_as])
10
+ else
11
+ value = object.send(field).blank? ? "" : object.send(field)
12
+ end
13
+ collection = nil
14
+ if opts[:type] == :select && !opts[:collection].blank?
15
+ v = object.send(field)
16
+ value = Hash[opts[:collection]][!!(v =~ /^[0-9]+$/) ? v.to_i : v]
17
+ collection = opts[:collection].to_json
18
+ end
19
+ if opts[:type] == :checkbox
20
+ fieldValue = !!object.send(field)
21
+ if opts[:collection].blank? || opts[:collection].size != 2
22
+ opts[:collection] = ["No", "Yes"]
23
+ end
24
+ value = fieldValue ? opts[:collection][1] : opts[:collection][0]
25
+ collection = opts[:collection].to_json
26
+ end
27
+ out = "<span class='best_in_place'"
28
+ out << " id='#{BestInPlace::Utils.build_best_in_place_id(object, field)}'"
29
+ out << " data-url='#{opts[:path].blank? ? url_for(object).to_s : url_for(opts[:path])}'"
30
+ out << " data-object='#{object.class.to_s.gsub("::", "_").underscore}'"
31
+ out << " data-collection='#{collection}'" unless collection.blank?
32
+ out << " data-attribute='#{field}'"
33
+ out << " data-activator='#{opts[:activator]}'" unless opts[:activator].blank?
34
+ out << " data-nil='#{opts[:nil].to_s}'" unless opts[:nil].blank?
35
+ out << " data-type='#{opts[:type].to_s}'"
36
+ out << " data-inner-class='#{opts[:inner_class].to_s}'" if opts[:inner_class]
37
+ out << " data-html-attrs='#{opts[:html_attrs].to_json}'" unless opts[:html_attrs].blank?
38
+ if !opts[:sanitize].nil? && !opts[:sanitize]
39
+ out << " data-sanitize='false'>"
40
+ out << sanitize(value.to_s, :tags => %w(b i u s a strong em p h1 h2 h3 h4 h5 ul li ol hr pre span img br), :attributes => %w(id class href))
41
+ else
42
+ out << ">#{sanitize(value.to_s, :tags => nil, :attributes => nil)}"
43
+ end
44
+ out << "</span>"
45
+ raw out
46
+ end
47
+
48
+ def best_in_place_if(condition, object, field, opts={})
49
+ if condition
50
+ best_in_place(object, field, opts)
51
+ else
52
+ object.send field
53
+ end
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,39 @@
1
+ module BestInPlace
2
+ module TestHelpers
3
+
4
+ def bip_area(model, attr, new_value)
5
+ id = BestInPlace::Utils.build_best_in_place_id model, attr
6
+ page.execute_script <<-JS
7
+ $("##{id}").click();
8
+ $("##{id} form textarea").val('#{new_value}');
9
+ $("##{id} form textarea").blur();
10
+ JS
11
+ end
12
+
13
+ def bip_text(model, attr, new_value)
14
+ id = BestInPlace::Utils.build_best_in_place_id model, attr
15
+ page.execute_script <<-JS
16
+ $("##{id}").click();
17
+ $("##{id} input[name='#{attr}']").val('#{new_value}');
18
+ $("##{id} form").submit();
19
+ JS
20
+ end
21
+
22
+ def bip_bool(model, attr)
23
+ id = BestInPlace::Utils.build_best_in_place_id model, attr
24
+ page.execute_script("$('##{id}').click();")
25
+ end
26
+
27
+ def bip_select(model, attr, name)
28
+ id = BestInPlace::Utils.build_best_in_place_id model, attr
29
+ page.execute_script <<-JS
30
+ (function() {
31
+ $("##{id}").click();
32
+ var opt_value = $("##{id} select option:contains('#{name}')").attr('value');
33
+ $("##{id} select option[value='" + opt_value + "']").attr('selected', true);
34
+ $("##{id} select").change();
35
+ })();
36
+ JS
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ module BestInPlace
2
+ class Utils
3
+
4
+ def self.build_best_in_place_id(object, field)
5
+ if object.is_a?(Symbol) || object.is_a?(String)
6
+ return "best_in_place_#{object}_#{field}"
7
+ end
8
+
9
+ id = "best_in_place_#{object.class.to_s.demodulize.underscore}"
10
+ id << "_#{object.id}" if object.class.ancestors.include?(ActiveRecord::Base)
11
+ id << "_#{field}"
12
+ id
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module BestInPlace
2
+ VERSION = "0.2.2"
3
+ end
@@ -0,0 +1,492 @@
1
+ /*
2
+ BestInPlace (for jQuery)
3
+ version: 0.1.0 (01/01/2011)
4
+ @requires jQuery >= v1.4
5
+ @requires jQuery.purr to display pop-up windows
6
+
7
+ By Bernat Farrero based on the work of Jan Varwig.
8
+ Examples at http://bernatfarrero.com
9
+
10
+ Licensed under the MIT:
11
+ http://www.opensource.org/licenses/mit-license.php
12
+
13
+ Usage:
14
+
15
+ Attention.
16
+ The format of the JSON object given to the select inputs is the following:
17
+ [["key", "value"],["key", "value"]]
18
+ The format of the JSON object given to the checkbox inputs is the following:
19
+ ["falseValue", "trueValue"]
20
+ */
21
+
22
+ function BestInPlaceEditor(e) {
23
+ this.element = jQuery(e);
24
+ this.initOptions();
25
+ this.bindForm();
26
+ this.initNil();
27
+ $(this.activator).bind('click', {editor: this}, this.clickHandler);
28
+ }
29
+
30
+ BestInPlaceEditor.prototype = {
31
+ // Public Interface Functions //////////////////////////////////////////////
32
+
33
+ activate : function() {
34
+ var elem = this.isNil ? "" : this.element.html();
35
+ this.oldValue = elem;
36
+ $(this.activator).unbind("click", this.clickHandler);
37
+ this.activateForm();
38
+ },
39
+
40
+ abort : function() {
41
+ if (this.isNil) this.element.html(this.nil);
42
+ else this.element.html(this.oldValue);
43
+ $(this.activator).bind('click', {editor: this}, this.clickHandler);
44
+ },
45
+
46
+ update : function() {
47
+ var editor = this;
48
+ if (this.formType in {"input":1, "textarea":1} && this.getValue() == this.oldValue)
49
+ { // Avoid request if no change is made
50
+ this.abort();
51
+ return true;
52
+ }
53
+ this.isNil = false;
54
+ editor.ajax({
55
+ "type" : "post",
56
+ "dataType" : "text",
57
+ "data" : editor.requestData(),
58
+ "success" : function(data, request){ editor.loadSuccessCallback(data, request); },
59
+ "error" : function(request, error){ editor.loadErrorCallback(request, error); }
60
+ });
61
+ if (this.formType == "select") {
62
+ var value = this.getValue();
63
+ $.each(this.values, function(i, v) {
64
+ if (value == v[0]) {
65
+ editor.element.html(v[1]);
66
+ }
67
+ }
68
+ );
69
+ } else if (this.formType == "checkbox") {
70
+ editor.element.html(this.getValue() ? this.values[1] : this.values[0]);
71
+ } else {
72
+ editor.element.html(this.getValue() != "" ? this.getValue() : this.nil);
73
+ }
74
+ },
75
+
76
+ activateForm : function() {
77
+ alert("The form was not properly initialized. activateForm is unbound");
78
+ },
79
+
80
+ // Helper Functions ////////////////////////////////////////////////////////
81
+
82
+ initOptions : function() {
83
+ // Try parent supplied info
84
+ var self = this;
85
+ self.element.parents().each(function(){
86
+ self.url = self.url || jQuery(this).attr("data-url");
87
+ self.collection = self.collection || jQuery(this).attr("data-collection");
88
+ self.formType = self.formType || jQuery(this).attr("data-type");
89
+ self.objectName = self.objectName || jQuery(this).attr("data-object");
90
+ self.attributeName = self.attributeName || jQuery(this).attr("data-attribute");
91
+ self.nil = self.nil || jQuery(this).attr("data-nil");
92
+ self.inner_class = self.inner_class || jQuery(this).attr("data-inner-class");
93
+ self.html_attrs = self.html_attrs || jQuery(this).attr("data-html-attrs");
94
+ });
95
+
96
+ // Try Rails-id based if parents did not explicitly supply something
97
+ self.element.parents().each(function(){
98
+ var res = this.id.match(/^(\w+)_(\d+)$/i);
99
+ if (res) {
100
+ self.objectName = self.objectName || res[1];
101
+ }
102
+ });
103
+
104
+ // Load own attributes (overrides all others)
105
+ self.url = self.element.attr("data-url") || self.url || document.location.pathname;
106
+ self.collection = self.element.attr("data-collection") || self.collection;
107
+ self.formType = self.element.attr("data-type") || self.formtype || "input";
108
+ self.objectName = self.element.attr("data-object") || self.objectName;
109
+ self.attributeName = self.element.attr("data-attribute") || self.attributeName;
110
+ self.activator = self.element.attr("data-activator") || self.element;
111
+ self.nil = self.element.attr("data-nil") || self.nil || "-";
112
+ self.inner_class = self.element.attr("data-inner-class") || self.inner_class || null;
113
+ self.html_attrs = self.element.attr("data-html-attrs") || self.html_attrs;
114
+
115
+ if (!self.element.attr("data-sanitize")) {
116
+ self.sanitize = true;
117
+ }
118
+ else {
119
+ self.sanitize = (self.element.attr("data-sanitize") == "true");
120
+ }
121
+
122
+ if ((self.formType == "select" || self.formType == "checkbox") && self.collection !== null)
123
+ {
124
+ self.values = jQuery.parseJSON(self.collection);
125
+ }
126
+ },
127
+
128
+ bindForm : function() {
129
+ this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm;
130
+ this.getValue = BestInPlaceEditor.forms[this.formType].getValue;
131
+ },
132
+
133
+ initNil: function() {
134
+ if (this.element.html() == "")
135
+ {
136
+ this.isNil = true
137
+ this.element.html(this.nil)
138
+ }
139
+ },
140
+
141
+ getValue : function() {
142
+ alert("The form was not properly initialized. getValue is unbound");
143
+ },
144
+
145
+ // Trim and Strips HTML from text
146
+ sanitizeValue : function(s) {
147
+ if (this.sanitize)
148
+ {
149
+ var tmp = document.createElement("DIV");
150
+ tmp.innerHTML = s;
151
+ s = tmp.textContent || tmp.innerText;
152
+ }
153
+ return jQuery.trim(s);
154
+ },
155
+
156
+ /* Generate the data sent in the POST request */
157
+ requestData : function() {
158
+ // To prevent xss attacks, a csrf token must be defined as a meta attribute
159
+ csrf_token = $('meta[name=csrf-token]').attr('content');
160
+ csrf_param = $('meta[name=csrf-param]').attr('content');
161
+
162
+ var data = "_method=put";
163
+ data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(this.getValue());
164
+
165
+ if (csrf_param !== undefined && csrf_token !== undefined) {
166
+ data += "&" + csrf_param + "=" + encodeURIComponent(csrf_token);
167
+ }
168
+ return data;
169
+ },
170
+
171
+ ajax : function(options) {
172
+ options.url = this.url;
173
+ options.beforeSend = function(xhr){ xhr.setRequestHeader("Accept", "application/json"); };
174
+ return jQuery.ajax(options);
175
+ },
176
+
177
+ // Handlers ////////////////////////////////////////////////////////////////
178
+
179
+ loadSuccessCallback : function(data) {
180
+ var response = $.parseJSON($.trim(data));
181
+ if (response != null && response.hasOwnProperty("display_as")) {
182
+ this.element.html(response["display_as"]);
183
+ }
184
+ this.element.trigger($.Event("ajax:success"), data);
185
+
186
+ $.each(jQuery.parseJSON(data), function(index, value) {
187
+ if( typeof(value) == "object") {value = index + " " + value.toString(); }
188
+ var container = $("<span class='flash-success'></span>").html(value);
189
+ container.purr();
190
+ });
191
+
192
+ // Binding back after being clicked
193
+ $(this.activator).bind('click', {editor: this}, this.clickHandler);
194
+ },
195
+
196
+ loadErrorCallback : function(request, error) {
197
+ this.element.html(this.oldValue);
198
+
199
+ // var response = $.parseJSON(request.responseText);
200
+
201
+ // Display all error messages from server side validation
202
+ $.each(jQuery.parseJSON(request.responseText), function(index, value) {
203
+ if( typeof(value) == "object") {value = index + " " + value.toString(); }
204
+ var container = $("<span class='flash-error'></span>").html(value);
205
+ container.purr();
206
+ });
207
+
208
+ // Binding back after being clicked
209
+ $(this.activator).bind('click', {editor: this}, this.clickHandler);
210
+ },
211
+
212
+ clickHandler : function(event) {
213
+ event.data.editor.activate();
214
+ },
215
+
216
+ setHtmlAttributes : function() {
217
+ var formField = this.element.find(this.formType);
218
+ var attrs = jQuery.parseJSON(this.html_attrs);
219
+ for(var key in attrs){
220
+ formField.attr(key, attrs[key]);
221
+ }
222
+ }
223
+ };
224
+
225
+
226
+ BestInPlaceEditor.forms = {
227
+ "input" : {
228
+ activateForm : function() {
229
+ var output = '<form class="form_in_place" action="javascript:void(0)" style="display:inline;">';
230
+ output += '<input type="text" name="'+ this.attributeName + '" id="'+ this.attributeName + '" value="' + this.sanitizeValue(this.oldValue) + '"';
231
+ if (this.inner_class != null) {
232
+ output += ' class="' + this.inner_class + '"';
233
+ }
234
+ output += '></form>'
235
+ this.element.html(output);
236
+ this.setHtmlAttributes();
237
+ this.element.find('input')[0].select();
238
+ this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler);
239
+ this.element.find("input").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler);
240
+ this.element.find("input").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler);
241
+ },
242
+
243
+ getValue : function() {
244
+ return this.sanitizeValue(this.element.find("input").val());
245
+ },
246
+
247
+ inputBlurHandler : function(event) {
248
+ event.data.editor.update();
249
+ },
250
+
251
+ submitHandler : function(event) {
252
+ event.data.editor.update();
253
+ },
254
+
255
+ keyupHandler : function(event) {
256
+ if (event.keyCode == 27) {
257
+ event.data.editor.abort();
258
+ }
259
+ }
260
+ },
261
+
262
+ "select" : {
263
+ activateForm : function() {
264
+ var output = "<form action='javascript:void(0)' style='display:inline;'><select>";
265
+ var selected = "";
266
+ var oldValue = this.oldValue;
267
+ $.each(this.values, function(index, value) {
268
+ selected = (value[1] == oldValue ? "selected='selected'" : "");
269
+ output += "<option value='" + value[0] + "' " + selected + ">" + value[1] + "</option>";
270
+ });
271
+ output += "</select></form>";
272
+ this.element.html(output);
273
+ this.setHtmlAttributes();
274
+ this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler);
275
+ this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler);
276
+ this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler);
277
+ this.element.find("select")[0].focus();
278
+ },
279
+
280
+ getValue : function() {
281
+ return this.sanitizeValue(this.element.find("select").val());
282
+ },
283
+
284
+ blurHandler : function(event) {
285
+ event.data.editor.update();
286
+ },
287
+
288
+ keyupHandler : function(event) {
289
+ if (event.keyCode == 27) event.data.editor.abort();
290
+ }
291
+ },
292
+
293
+ "checkbox" : {
294
+ activateForm : function() {
295
+ var newValue = Boolean(this.oldValue != this.values[1]);
296
+ var output = newValue ? this.values[1] : this.values[0];
297
+ this.element.html(output);
298
+ this.setHtmlAttributes();
299
+ this.update();
300
+ },
301
+
302
+ getValue : function() {
303
+ return Boolean(this.element.html() == this.values[1]);
304
+ }
305
+ },
306
+
307
+ "textarea" : {
308
+ activateForm : function() {
309
+ // grab width and height of text
310
+ width = this.element.css('width');
311
+ height = this.element.css('height');
312
+
313
+ // construct the form
314
+ var output = '<form action="javascript:void(0)" style="display:inline;"><textarea>';
315
+ output += this.sanitizeValue(this.oldValue);
316
+ output += '</textarea></form>';
317
+ this.element.html(output);
318
+ this.setHtmlAttributes();
319
+
320
+ // set width and height of textarea
321
+ jQuery(this.element.find("textarea")[0]).css({ 'min-width': width, 'min-height': height });
322
+ jQuery(this.element.find("textarea")[0]).elastic();
323
+
324
+ this.element.find("textarea")[0].focus();
325
+ this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler);
326
+ this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler);
327
+ },
328
+
329
+ getValue : function() {
330
+ return this.sanitizeValue(this.element.find("textarea").val());
331
+ },
332
+
333
+ blurHandler : function(event) {
334
+ event.data.editor.update();
335
+ },
336
+
337
+ keyupHandler : function(event) {
338
+ if (event.keyCode == 27) {
339
+ BestInPlaceEditor.forms.textarea.abort(event.data.editor);
340
+ }
341
+ },
342
+
343
+ abort : function(editor) {
344
+ if (confirm("Are you sure you want to discard your changes?")) {
345
+ editor.abort();
346
+ }
347
+ }
348
+ }
349
+ };
350
+
351
+ jQuery.fn.best_in_place = function() {
352
+ this.each(function(){
353
+ if (!jQuery(this).data('bestInPlaceEditor')) {
354
+ jQuery(this).data('bestInPlaceEditor', new BestInPlaceEditor(this));
355
+ }
356
+ });
357
+ return this;
358
+ };
359
+
360
+
361
+
362
+ /**
363
+ * @name Elastic
364
+ * @descripton Elastic is Jquery plugin that grow and shrink your textareas automaticliy
365
+ * @version 1.6.5
366
+ * @requires Jquery 1.2.6+
367
+ *
368
+ * @author Jan Jarfalk
369
+ * @author-email jan.jarfalk@unwrongest.com
370
+ * @author-website http://www.unwrongest.com
371
+ *
372
+ * @licens MIT License - http://www.opensource.org/licenses/mit-license.php
373
+ */
374
+
375
+ (function(jQuery){
376
+ jQuery.fn.extend({
377
+ elastic: function() {
378
+ // We will create a div clone of the textarea
379
+ // by copying these attributes from the textarea to the div.
380
+ var mimics = [
381
+ 'paddingTop',
382
+ 'paddingRight',
383
+ 'paddingBottom',
384
+ 'paddingLeft',
385
+ 'fontSize',
386
+ 'lineHeight',
387
+ 'fontFamily',
388
+ 'width',
389
+ 'fontWeight'];
390
+
391
+ return this.each( function() {
392
+
393
+ // Elastic only works on textareas
394
+ if ( this.type != 'textarea' ) {
395
+ return false;
396
+ }
397
+
398
+ var $textarea = jQuery(this),
399
+ $twin = jQuery('<div />').css({'position': 'absolute','display':'none','word-wrap':'break-word'}),
400
+ lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'),
401
+ minheight = parseInt($textarea.css('height'),10) || lineHeight*3,
402
+ maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE,
403
+ goalheight = 0,
404
+ i = 0;
405
+
406
+ // Opera returns max-height of -1 if not set
407
+ if (maxheight < 0) { maxheight = Number.MAX_VALUE; }
408
+
409
+ // Append the twin to the DOM
410
+ // We are going to meassure the height of this, not the textarea.
411
+ $twin.appendTo($textarea.parent());
412
+
413
+ // Copy the essential styles (mimics) from the textarea to the twin
414
+ var i = mimics.length;
415
+ while(i--){
416
+ $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString()));
417
+ }
418
+
419
+
420
+ // Sets a given height and overflow state on the textarea
421
+ function setHeightAndOverflow(height, overflow){
422
+ curratedHeight = Math.floor(parseInt(height,10));
423
+ if($textarea.height() != curratedHeight){
424
+ $textarea.css({'height': curratedHeight + 'px','overflow':overflow});
425
+
426
+ }
427
+ }
428
+
429
+
430
+ // This function will update the height of the textarea if necessary
431
+ function update() {
432
+
433
+ // Get curated content from the textarea.
434
+ var textareaContent = $textarea.val().replace(/&/g,'&amp;').replace(/ /g, '&nbsp;').replace(/<|>/g, '&gt;').replace(/\n/g, '<br />');
435
+
436
+ // Compare curated content with curated twin.
437
+ var twinContent = $twin.html().replace(/<br>/ig,'<br />');
438
+
439
+ if(textareaContent+'&nbsp;' != twinContent){
440
+
441
+ // Add an extra white space so new rows are added when you are at the end of a row.
442
+ $twin.html(textareaContent+'&nbsp;');
443
+
444
+ // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height
445
+ if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){
446
+
447
+ var goalheight = $twin.height()+lineHeight;
448
+ if(goalheight >= maxheight) {
449
+ setHeightAndOverflow(maxheight,'auto');
450
+ } else if(goalheight <= minheight) {
451
+ setHeightAndOverflow(minheight,'hidden');
452
+ } else {
453
+ setHeightAndOverflow(goalheight,'hidden');
454
+ }
455
+
456
+ }
457
+
458
+ }
459
+
460
+ }
461
+
462
+ // Hide scrollbars
463
+ $textarea.css({'overflow':'hidden'});
464
+
465
+ // Update textarea size on keyup, change, cut and paste
466
+ $textarea.bind('keyup change cut paste', function(){
467
+ update();
468
+ });
469
+
470
+ // Compact textarea on blur
471
+ // Lets animate this....
472
+ $textarea.bind('blur',function(){
473
+ if($twin.height() < maxheight){
474
+ if($twin.height() > minheight) {
475
+ $textarea.height($twin.height());
476
+ } else {
477
+ $textarea.height(minheight);
478
+ }
479
+ }
480
+ });
481
+
482
+ // And this line is to catch the browser paste event
483
+ $textarea.live('input paste',function(e){ setTimeout( update, 250); });
484
+
485
+ // Run update once when elastic is initialized
486
+ update();
487
+
488
+ });
489
+
490
+ }
491
+ });
492
+ })(jQuery);