ruby-openid 2.2.3 → 2.3.0

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +1 -2
  4. data/examples/rails_openid/Gemfile +41 -0
  5. data/examples/rails_openid/README.rdoc +261 -0
  6. data/examples/rails_openid/Rakefile +4 -7
  7. data/examples/rails_openid/app/assets/images/rails.png +0 -0
  8. data/examples/rails_openid/app/assets/javascripts/application.js +15 -0
  9. data/examples/rails_openid/app/assets/stylesheets/application.css +13 -0
  10. data/examples/rails_openid/app/controllers/application_controller.rb +3 -0
  11. data/examples/rails_openid/app/controllers/consumer_controller.rb +1 -0
  12. data/examples/rails_openid/app/helpers/application_helper.rb +0 -1
  13. data/examples/rails_openid/app/views/consumer/{index.rhtml → index.html.erb} +0 -0
  14. data/examples/rails_openid/app/views/layouts/{server.rhtml → server.html.erb} +4 -2
  15. data/examples/rails_openid/app/views/login/{index.rhtml → index.html.erb} +0 -0
  16. data/examples/rails_openid/app/views/server/{decide.rhtml → decide.html.erb} +1 -0
  17. data/examples/rails_openid/config.ru +4 -0
  18. data/examples/rails_openid/config/application.rb +62 -0
  19. data/examples/rails_openid/config/boot.rb +4 -17
  20. data/examples/rails_openid/config/database.yml +15 -64
  21. data/examples/rails_openid/config/environment.rb +4 -53
  22. data/examples/rails_openid/config/environments/development.rb +32 -14
  23. data/examples/rails_openid/config/environments/production.rb +61 -13
  24. data/examples/rails_openid/config/environments/test.rb +33 -15
  25. data/examples/rails_openid/config/initializers/backtrace_silencers.rb +7 -0
  26. data/examples/rails_openid/config/initializers/inflections.rb +15 -0
  27. data/examples/rails_openid/config/initializers/mime_types.rb +5 -0
  28. data/examples/rails_openid/config/initializers/rails_root.rb +1 -0
  29. data/examples/rails_openid/config/initializers/secret_token.rb +7 -0
  30. data/examples/rails_openid/config/initializers/session_store.rb +8 -0
  31. data/examples/rails_openid/config/initializers/wrap_parameters.rb +14 -0
  32. data/examples/rails_openid/config/locales/en.yml +5 -0
  33. data/examples/rails_openid/config/routes.rb +65 -18
  34. data/examples/rails_openid/db/development.sqlite3 +0 -0
  35. data/examples/rails_openid/db/seeds.rb +7 -0
  36. data/examples/rails_openid/doc/README_FOR_APP +1 -1
  37. data/examples/rails_openid/log/development.log +2052 -0
  38. data/examples/rails_openid/public/404.html +23 -5
  39. data/examples/rails_openid/public/422.html +26 -0
  40. data/examples/rails_openid/public/500.html +22 -5
  41. data/examples/rails_openid/public/javascripts/application.js +2 -0
  42. data/examples/rails_openid/public/javascripts/controls.js +586 -373
  43. data/examples/rails_openid/public/javascripts/dragdrop.js +575 -186
  44. data/examples/rails_openid/public/javascripts/effects.js +763 -489
  45. data/examples/rails_openid/public/javascripts/prototype.js +3420 -885
  46. data/examples/rails_openid/public/robots.txt +5 -1
  47. data/examples/rails_openid/script/rails +6 -0
  48. data/examples/rails_openid/test/performance/browsing_test.rb +12 -0
  49. data/examples/rails_openid/test/test_helper.rb +7 -22
  50. data/lib/openid/association.rb +1 -1
  51. data/lib/openid/consumer/checkid_request.rb +1 -1
  52. data/lib/openid/consumer/discovery.rb +1 -1
  53. data/lib/openid/consumer/html_parse.rb +3 -1
  54. data/lib/openid/consumer/idres.rb +1 -1
  55. data/lib/openid/extensions/ax.rb +2 -3
  56. data/lib/openid/extensions/ui.rb +3 -3
  57. data/lib/openid/extras.rb +2 -2
  58. data/lib/openid/server.rb +2 -2
  59. data/lib/openid/store/memory.rb +1 -2
  60. data/lib/openid/store/nonce.rb +1 -1
  61. data/lib/openid/trustroot.rb +1 -1
  62. data/lib/openid/util.rb +2 -2
  63. data/lib/openid/version.rb +1 -1
  64. data/lib/openid/yadis/xrds.rb +1 -1
  65. data/test/test_accept.rb +20 -21
  66. data/test/test_association.rb +4 -8
  67. data/test/test_associationmanager.rb +1 -1
  68. data/test/test_ax.rb +0 -1
  69. data/test/test_checkid_request.rb +7 -8
  70. data/test/test_dh.rb +1 -1
  71. data/test/test_discover.rb +7 -8
  72. data/test/test_extension.rb +1 -1
  73. data/test/test_fetchers.rb +7 -11
  74. data/test/test_filters.rb +0 -4
  75. data/test/test_idres.rb +5 -5
  76. data/test/test_kvpost.rb +0 -1
  77. data/test/test_message.rb +10 -11
  78. data/test/test_parsehtml.rb +0 -1
  79. data/test/test_server.rb +11 -30
  80. data/test/test_stores.rb +2 -2
  81. data/test/test_trustroot.rb +1 -1
  82. data/test/test_urinorm.rb +1 -1
  83. data/test/test_xrds.rb +1 -1
  84. data/test/test_yadis_discovery.rb +0 -2
  85. metadata +34 -33
  86. data/examples/rails_openid/app/controllers/application.rb +0 -4
  87. data/examples/rails_openid/script/about +0 -3
  88. data/examples/rails_openid/script/breakpointer +0 -3
  89. data/examples/rails_openid/script/console +0 -3
  90. data/examples/rails_openid/script/destroy +0 -3
  91. data/examples/rails_openid/script/generate +0 -3
  92. data/examples/rails_openid/script/performance/benchmarker +0 -3
  93. data/examples/rails_openid/script/performance/profiler +0 -3
  94. data/examples/rails_openid/script/plugin +0 -3
  95. data/examples/rails_openid/script/process/reaper +0 -3
  96. data/examples/rails_openid/script/process/spawner +0 -3
  97. data/examples/rails_openid/script/process/spinner +0 -3
  98. data/examples/rails_openid/script/runner +0 -3
  99. data/examples/rails_openid/script/server +0 -3
@@ -1,8 +1,26 @@
1
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2
- "http://www.w3.org/TR/html4/loose.dtd">
1
+ <!DOCTYPE html>
3
2
  <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
4
19
  <body>
5
- <h1>File not found</h1>
6
- <p>Change this error message for pages not found in public/404.html</p>
20
+ <!-- This file lives in public/404.html -->
21
+ <div class="dialog">
22
+ <h1>The page you were looking for doesn't exist.</h1>
23
+ <p>You may have mistyped the address or the page may have moved.</p>
24
+ </div>
7
25
  </body>
8
- </html>
26
+ </html>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/422.html -->
21
+ <div class="dialog">
22
+ <h1>The change you wanted was rejected.</h1>
23
+ <p>Maybe you tried to change something you didn't have access to.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -1,8 +1,25 @@
1
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2
- "http://www.w3.org/TR/html4/loose.dtd">
1
+ <!DOCTYPE html>
3
2
  <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
4
19
  <body>
5
- <h1>Application error (Apache)</h1>
6
- <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
20
+ <!-- This file lives in public/500.html -->
21
+ <div class="dialog">
22
+ <h1>We're sorry, but something went wrong.</h1>
23
+ </div>
7
24
  </body>
8
- </html>
25
+ </html>
@@ -0,0 +1,2 @@
1
+ // Place your application-specific JavaScript functions and classes here
2
+ // This file is automatically included by javascript_include_tag :defaults
@@ -1,21 +1,22 @@
1
- // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
- // (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3
- // (c) 2005 Jon Tirsen (http://www.tirsen.com)
1
+ // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
+ // (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3
+ // (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
4
4
  // Contributors:
5
5
  // Richard Livsey
6
6
  // Rahul Bhargava
7
7
  // Rob Wills
8
- //
9
- // See scriptaculous.js for full license.
8
+ //
9
+ // script.aculo.us is freely distributable under the terms of an MIT-style license.
10
+ // For details, see the script.aculo.us web site: http://script.aculo.us/
10
11
 
11
- // Autocompleter.Base handles all the autocompletion functionality
12
+ // Autocompleter.Base handles all the autocompletion functionality
12
13
  // that's independent of the data source for autocompletion. This
13
14
  // includes drawing the autocompletion menu, observing keyboard
14
15
  // and mouse events, and similar.
15
16
  //
16
- // Specific autocompleters need to provide, at the very least,
17
+ // Specific autocompleters need to provide, at the very least,
17
18
  // a getUpdatedChoices function that will be invoked every time
18
- // the text inside the monitored textbox changes. This method
19
+ // the text inside the monitored textbox changes. This method
19
20
  // should get the text for which to provide autocompletion by
20
21
  // invoking this.getToken(), NOT by directly accessing
21
22
  // this.element.value. This is to allow incremental tokenized
@@ -29,62 +30,71 @@
29
30
  // will incrementally autocomplete with a comma as the token.
30
31
  // Additionally, ',' in the above example can be replaced with
31
32
  // a token array, e.g. { tokens: [',', '\n'] } which
32
- // enables autocompletion on multiple tokens. This is most
33
- // useful when one of the tokens is \n (a newline), as it
33
+ // enables autocompletion on multiple tokens. This is most
34
+ // useful when one of the tokens is \n (a newline), as it
34
35
  // allows smart autocompletion after linebreaks.
35
36
 
36
- var Autocompleter = {}
37
- Autocompleter.Base = function() {};
38
- Autocompleter.Base.prototype = {
37
+ if(typeof Effect == 'undefined')
38
+ throw("controls.js requires including script.aculo.us' effects.js library");
39
+
40
+ var Autocompleter = { };
41
+ Autocompleter.Base = Class.create({
39
42
  baseInitialize: function(element, update, options) {
40
- this.element = $(element);
41
- this.update = $(update);
42
- this.hasFocus = false;
43
- this.changed = false;
44
- this.active = false;
45
- this.index = 0;
43
+ element = $(element);
44
+ this.element = element;
45
+ this.update = $(update);
46
+ this.hasFocus = false;
47
+ this.changed = false;
48
+ this.active = false;
49
+ this.index = 0;
46
50
  this.entryCount = 0;
51
+ this.oldElementValue = this.element.value;
47
52
 
48
- if (this.setOptions)
53
+ if(this.setOptions)
49
54
  this.setOptions(options);
50
55
  else
51
- this.options = options || {};
56
+ this.options = options || { };
52
57
 
53
58
  this.options.paramName = this.options.paramName || this.element.name;
54
59
  this.options.tokens = this.options.tokens || [];
55
60
  this.options.frequency = this.options.frequency || 0.4;
56
61
  this.options.minChars = this.options.minChars || 1;
57
- this.options.onShow = this.options.onShow ||
58
- function(element, update){
59
- if(!update.style.position || update.style.position=='absolute') {
60
- update.style.position = 'absolute';
61
- Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
62
- }
63
- Effect.Appear(update,{duration:0.15});
64
- };
65
- this.options.onHide = this.options.onHide ||
66
- function(element, update){ new Effect.Fade(update,{duration:0.15}) };
62
+ this.options.onShow = this.options.onShow ||
63
+ function(element, update){
64
+ if(!update.style.position || update.style.position=='absolute') {
65
+ update.style.position = 'absolute';
66
+ Position.clone(element, update, {
67
+ setHeight: false,
68
+ offsetTop: element.offsetHeight
69
+ });
70
+ }
71
+ Effect.Appear(update,{duration:0.15});
72
+ };
73
+ this.options.onHide = this.options.onHide ||
74
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
67
75
 
68
- if (typeof(this.options.tokens) == 'string')
76
+ if(typeof(this.options.tokens) == 'string')
69
77
  this.options.tokens = new Array(this.options.tokens);
78
+ // Force carriage returns as token delimiters anyway
79
+ if (!this.options.tokens.include('\n'))
80
+ this.options.tokens.push('\n');
70
81
 
71
82
  this.observer = null;
72
-
83
+
73
84
  this.element.setAttribute('autocomplete','off');
74
85
 
75
86
  Element.hide(this.update);
76
87
 
77
- Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
78
- Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
88
+ Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
89
+ Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
79
90
  },
80
91
 
81
92
  show: function() {
82
93
  if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
83
- if(!this.iefix &&
84
- (navigator.appVersion.indexOf('MSIE')>0) &&
85
- (navigator.userAgent.indexOf('Opera')<0) &&
94
+ if(!this.iefix &&
95
+ (Prototype.Browser.IE) &&
86
96
  (Element.getStyle(this.update, 'position')=='absolute')) {
87
- new Insertion.After(this.update,
97
+ new Insertion.After(this.update,
88
98
  '<iframe id="' + this.update.id + '_iefix" '+
89
99
  'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
90
100
  'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
@@ -92,9 +102,9 @@ Autocompleter.Base.prototype = {
92
102
  }
93
103
  if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
94
104
  },
95
-
105
+
96
106
  fixIEOverlapping: function() {
97
- Position.clone(this.update, this.iefix);
107
+ Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
98
108
  this.iefix.style.zIndex = 1;
99
109
  this.update.style.zIndex = 2;
100
110
  Element.show(this.iefix);
@@ -132,58 +142,63 @@ Autocompleter.Base.prototype = {
132
142
  case Event.KEY_UP:
133
143
  this.markPrevious();
134
144
  this.render();
135
- if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
145
+ Event.stop(event);
136
146
  return;
137
147
  case Event.KEY_DOWN:
138
148
  this.markNext();
139
149
  this.render();
140
- if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
150
+ Event.stop(event);
141
151
  return;
142
152
  }
143
- else
144
- if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
145
- return;
153
+ else
154
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
155
+ (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
146
156
 
147
157
  this.changed = true;
148
158
  this.hasFocus = true;
149
159
 
150
160
  if(this.observer) clearTimeout(this.observer);
151
- this.observer =
161
+ this.observer =
152
162
  setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
153
163
  },
154
164
 
165
+ activate: function() {
166
+ this.changed = false;
167
+ this.hasFocus = true;
168
+ this.getUpdatedChoices();
169
+ },
170
+
155
171
  onHover: function(event) {
156
172
  var element = Event.findElement(event, 'LI');
157
- if(this.index != element.autocompleteIndex)
173
+ if(this.index != element.autocompleteIndex)
158
174
  {
159
175
  this.index = element.autocompleteIndex;
160
176
  this.render();
161
177
  }
162
178
  Event.stop(event);
163
179
  },
164
-
180
+
165
181
  onClick: function(event) {
166
182
  var element = Event.findElement(event, 'LI');
167
183
  this.index = element.autocompleteIndex;
168
184
  this.selectEntry();
169
185
  this.hide();
170
186
  },
171
-
187
+
172
188
  onBlur: function(event) {
173
189
  // needed to make click events working
174
190
  setTimeout(this.hide.bind(this), 250);
175
191
  this.hasFocus = false;
176
- this.active = false;
177
- },
178
-
192
+ this.active = false;
193
+ },
194
+
179
195
  render: function() {
180
196
  if(this.entryCount > 0) {
181
197
  for (var i = 0; i < this.entryCount; i++)
182
- this.index==i ?
183
- Element.addClassName(this.getEntry(i),"selected") :
198
+ this.index==i ?
199
+ Element.addClassName(this.getEntry(i),"selected") :
184
200
  Element.removeClassName(this.getEntry(i),"selected");
185
-
186
- if(this.hasFocus) {
201
+ if(this.hasFocus) {
187
202
  this.show();
188
203
  this.active = true;
189
204
  }
@@ -192,25 +207,27 @@ Autocompleter.Base.prototype = {
192
207
  this.hide();
193
208
  }
194
209
  },
195
-
210
+
196
211
  markPrevious: function() {
197
- if(this.index > 0) this.index--
212
+ if(this.index > 0) this.index--;
198
213
  else this.index = this.entryCount-1;
214
+ this.getEntry(this.index).scrollIntoView(true);
199
215
  },
200
-
216
+
201
217
  markNext: function() {
202
- if(this.index < this.entryCount-1) this.index++
218
+ if(this.index < this.entryCount-1) this.index++;
203
219
  else this.index = 0;
220
+ this.getEntry(this.index).scrollIntoView(false);
204
221
  },
205
-
222
+
206
223
  getEntry: function(index) {
207
224
  return this.update.firstChild.childNodes[index];
208
225
  },
209
-
226
+
210
227
  getCurrentEntry: function() {
211
228
  return this.getEntry(this.index);
212
229
  },
213
-
230
+
214
231
  selectEntry: function() {
215
232
  this.active = false;
216
233
  this.updateElement(this.getCurrentEntry());
@@ -221,20 +238,26 @@ Autocompleter.Base.prototype = {
221
238
  this.options.updateElement(selectedElement);
222
239
  return;
223
240
  }
224
-
225
- var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
226
- var lastTokenPos = this.findLastToken();
227
- if (lastTokenPos != -1) {
228
- var newValue = this.element.value.substr(0, lastTokenPos + 1);
229
- var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
241
+ var value = '';
242
+ if (this.options.select) {
243
+ var nodes = $(selectedElement).select('.' + this.options.select) || [];
244
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
245
+ } else
246
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
247
+
248
+ var bounds = this.getTokenBounds();
249
+ if (bounds[0] != -1) {
250
+ var newValue = this.element.value.substr(0, bounds[0]);
251
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
230
252
  if (whitespace)
231
253
  newValue += whitespace[0];
232
- this.element.value = newValue + value;
254
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
233
255
  } else {
234
256
  this.element.value = value;
235
257
  }
258
+ this.oldElementValue = this.element.value;
236
259
  this.element.focus();
237
-
260
+
238
261
  if (this.options.afterUpdateElement)
239
262
  this.options.afterUpdateElement(this.element, selectedElement);
240
263
  },
@@ -243,24 +266,29 @@ Autocompleter.Base.prototype = {
243
266
  if(!this.changed && this.hasFocus) {
244
267
  this.update.innerHTML = choices;
245
268
  Element.cleanWhitespace(this.update);
246
- Element.cleanWhitespace(this.update.firstChild);
269
+ Element.cleanWhitespace(this.update.down());
247
270
 
248
- if(this.update.firstChild && this.update.firstChild.childNodes) {
249
- this.entryCount =
250
- this.update.firstChild.childNodes.length;
271
+ if(this.update.firstChild && this.update.down().childNodes) {
272
+ this.entryCount =
273
+ this.update.down().childNodes.length;
251
274
  for (var i = 0; i < this.entryCount; i++) {
252
275
  var entry = this.getEntry(i);
253
276
  entry.autocompleteIndex = i;
254
277
  this.addObservers(entry);
255
278
  }
256
- } else {
279
+ } else {
257
280
  this.entryCount = 0;
258
281
  }
259
282
 
260
283
  this.stopIndicator();
261
-
262
284
  this.index = 0;
263
- this.render();
285
+
286
+ if(this.entryCount==1 && this.options.autoSelect) {
287
+ this.selectEntry();
288
+ this.hide();
289
+ } else {
290
+ this.render();
291
+ }
264
292
  }
265
293
  },
266
294
 
@@ -270,42 +298,51 @@ Autocompleter.Base.prototype = {
270
298
  },
271
299
 
272
300
  onObserverEvent: function() {
273
- this.changed = false;
301
+ this.changed = false;
302
+ this.tokenBounds = null;
274
303
  if(this.getToken().length>=this.options.minChars) {
275
- this.startIndicator();
276
304
  this.getUpdatedChoices();
277
305
  } else {
278
306
  this.active = false;
279
307
  this.hide();
280
308
  }
309
+ this.oldElementValue = this.element.value;
281
310
  },
282
311
 
283
312
  getToken: function() {
284
- var tokenPos = this.findLastToken();
285
- if (tokenPos != -1)
286
- var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
287
- else
288
- var ret = this.element.value;
289
-
290
- return /\n/.test(ret) ? '' : ret;
291
- },
292
-
293
- findLastToken: function() {
294
- var lastTokenPos = -1;
295
-
296
- for (var i=0; i<this.options.tokens.length; i++) {
297
- var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
298
- if (thisTokenPos > lastTokenPos)
299
- lastTokenPos = thisTokenPos;
313
+ var bounds = this.getTokenBounds();
314
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
315
+ },
316
+
317
+ getTokenBounds: function() {
318
+ if (null != this.tokenBounds) return this.tokenBounds;
319
+ var value = this.element.value;
320
+ if (value.strip().empty()) return [-1, 0];
321
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
322
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
323
+ var prevTokenPos = -1, nextTokenPos = value.length;
324
+ var tp;
325
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
326
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
327
+ if (tp > prevTokenPos) prevTokenPos = tp;
328
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
329
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
300
330
  }
301
- return lastTokenPos;
331
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
302
332
  }
303
- }
333
+ });
304
334
 
305
- Ajax.Autocompleter = Class.create();
306
- Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
335
+ Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
336
+ var boundary = Math.min(newS.length, oldS.length);
337
+ for (var index = 0; index < boundary; ++index)
338
+ if (newS[index] != oldS[index])
339
+ return index;
340
+ return boundary;
341
+ };
342
+
343
+ Ajax.Autocompleter = Class.create(Autocompleter.Base, {
307
344
  initialize: function(element, update, url, options) {
308
- this.baseInitialize(element, update, options);
345
+ this.baseInitialize(element, update, options);
309
346
  this.options.asynchronous = true;
310
347
  this.options.onComplete = this.onComplete.bind(this);
311
348
  this.options.defaultParams = this.options.parameters || null;
@@ -313,13 +350,15 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
313
350
  },
314
351
 
315
352
  getUpdatedChoices: function() {
316
- entry = encodeURIComponent(this.options.paramName) + '=' +
353
+ this.startIndicator();
354
+
355
+ var entry = encodeURIComponent(this.options.paramName) + '=' +
317
356
  encodeURIComponent(this.getToken());
318
357
 
319
358
  this.options.parameters = this.options.callback ?
320
359
  this.options.callback(this.element, entry) : entry;
321
360
 
322
- if(this.options.defaultParams)
361
+ if(this.options.defaultParams)
323
362
  this.options.parameters += '&' + this.options.defaultParams;
324
363
 
325
364
  new Ajax.Request(this.url, this.options);
@@ -328,7 +367,6 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
328
367
  onComplete: function(request) {
329
368
  this.updateChoices(request.responseText);
330
369
  }
331
-
332
370
  });
333
371
 
334
372
  // The local array autocompleter. Used when you'd prefer to
@@ -344,7 +382,7 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
344
382
  // - choices - How many autocompletion choices to offer
345
383
  //
346
384
  // - partialSearch - If false, the autocompleter will match entered
347
- // text only at the beginning of strings in the
385
+ // text only at the beginning of strings in the
348
386
  // autocomplete array. Defaults to true, which will
349
387
  // match text at the beginning of any *word* in the
350
388
  // strings in the autocomplete array. If you want to
@@ -361,13 +399,12 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
361
399
  // - ignoreCase - Whether to ignore case when autocompleting.
362
400
  // Defaults to true.
363
401
  //
364
- // It's possible to pass in a custom function as the 'selector'
402
+ // It's possible to pass in a custom function as the 'selector'
365
403
  // option, if you prefer to write your own autocompletion logic.
366
404
  // In that case, the other options above will not apply unless
367
405
  // you support them.
368
406
 
369
- Autocompleter.Local = Class.create();
370
- Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
407
+ Autocompleter.Local = Class.create(Autocompleter.Base, {
371
408
  initialize: function(element, update, array, options) {
372
409
  this.baseInitialize(element, update, options);
373
410
  this.options.array = array;
@@ -390,20 +427,20 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
390
427
  var entry = instance.getToken();
391
428
  var count = 0;
392
429
 
393
- for (var i = 0; i < instance.options.array.length &&
394
- ret.length < instance.options.choices ; i++) {
430
+ for (var i = 0; i < instance.options.array.length &&
431
+ ret.length < instance.options.choices ; i++) {
395
432
 
396
433
  var elem = instance.options.array[i];
397
- var foundPos = instance.options.ignoreCase ?
398
- elem.toLowerCase().indexOf(entry.toLowerCase()) :
434
+ var foundPos = instance.options.ignoreCase ?
435
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
399
436
  elem.indexOf(entry);
400
437
 
401
438
  while (foundPos != -1) {
402
- if (foundPos == 0 && elem.length != entry.length) {
403
- ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
439
+ if (foundPos == 0 && elem.length != entry.length) {
440
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
404
441
  elem.substr(entry.length) + "</li>");
405
442
  break;
406
- } else if (entry.length >= instance.options.partialChars &&
443
+ } else if (entry.length >= instance.options.partialChars &&
407
444
  instance.options.partialSearch && foundPos != -1) {
408
445
  if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
409
446
  partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
@@ -413,23 +450,22 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
413
450
  }
414
451
  }
415
452
 
416
- foundPos = instance.options.ignoreCase ?
417
- elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
453
+ foundPos = instance.options.ignoreCase ?
454
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
418
455
  elem.indexOf(entry, foundPos + 1);
419
456
 
420
457
  }
421
458
  }
422
459
  if (partial.length)
423
- ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
460
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
424
461
  return "<ul>" + ret.join('') + "</ul>";
425
462
  }
426
- }, options || {});
463
+ }, options || { });
427
464
  }
428
465
  });
429
466
 
430
- // AJAX in-place editor
431
- //
432
- // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
467
+ // AJAX in-place editor and collection editor
468
+ // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
433
469
 
434
470
  // Use this if you notice weird scrolling problems on some browsers,
435
471
  // the DOM might be a bit confused when this gets called so do this
@@ -438,303 +474,480 @@ Field.scrollFreeActivate = function(field) {
438
474
  setTimeout(function() {
439
475
  Field.activate(field);
440
476
  }, 1);
441
- }
477
+ };
442
478
 
443
- Ajax.InPlaceEditor = Class.create();
444
- Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
445
- Ajax.InPlaceEditor.prototype = {
479
+ Ajax.InPlaceEditor = Class.create({
446
480
  initialize: function(element, url, options) {
447
481
  this.url = url;
448
- this.element = $(element);
449
-
450
- this.options = Object.extend({
451
- okText: "ok",
452
- cancelText: "cancel",
453
- savingText: "Saving...",
454
- clickToEditText: "Click to edit",
455
- okText: "ok",
456
- rows: 1,
457
- onComplete: function(transport, element) {
458
- new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
459
- },
460
- onFailure: function(transport) {
461
- alert("Error communicating with the server: " + transport.responseText.stripTags());
462
- },
463
- callback: function(form) {
464
- return Form.serialize(form);
465
- },
466
- handleLineBreaks: true,
467
- loadingText: 'Loading...',
468
- savingClassName: 'inplaceeditor-saving',
469
- loadingClassName: 'inplaceeditor-loading',
470
- formClassName: 'inplaceeditor-form',
471
- highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
472
- highlightendcolor: "#FFFFFF",
473
- externalControl: null,
474
- ajaxOptions: {}
475
- }, options || {});
476
-
477
- if(!this.options.formId && this.element.id) {
478
- this.options.formId = this.element.id + "-inplaceeditor";
479
- if ($(this.options.formId)) {
480
- // there's already a form with that name, don't specify an id
481
- this.options.formId = null;
482
- }
482
+ this.element = element = $(element);
483
+ this.prepareOptions();
484
+ this._controls = { };
485
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
486
+ Object.extend(this.options, options || { });
487
+ if (!this.options.formId && this.element.id) {
488
+ this.options.formId = this.element.id + '-inplaceeditor';
489
+ if ($(this.options.formId))
490
+ this.options.formId = '';
483
491
  }
484
-
485
- if (this.options.externalControl) {
492
+ if (this.options.externalControl)
486
493
  this.options.externalControl = $(this.options.externalControl);
487
- }
488
-
489
- this.originalBackground = Element.getStyle(this.element, 'background-color');
490
- if (!this.originalBackground) {
491
- this.originalBackground = "transparent";
492
- }
493
-
494
+ if (!this.options.externalControl)
495
+ this.options.externalControlOnly = false;
496
+ this._originalBackground = this.element.getStyle('background-color') || 'transparent';
494
497
  this.element.title = this.options.clickToEditText;
495
-
496
- this.onclickListener = this.enterEditMode.bindAsEventListener(this);
497
- this.mouseoverListener = this.enterHover.bindAsEventListener(this);
498
- this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
499
- Event.observe(this.element, 'click', this.onclickListener);
500
- Event.observe(this.element, 'mouseover', this.mouseoverListener);
501
- Event.observe(this.element, 'mouseout', this.mouseoutListener);
502
- if (this.options.externalControl) {
503
- Event.observe(this.options.externalControl, 'click', this.onclickListener);
504
- Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
505
- Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
498
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
499
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
500
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
501
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
502
+ this._boundWrapperHandler = this.wrapUp.bind(this);
503
+ this.registerListeners();
504
+ },
505
+ checkForEscapeOrReturn: function(e) {
506
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
507
+ if (Event.KEY_ESC == e.keyCode)
508
+ this.handleFormCancellation(e);
509
+ else if (Event.KEY_RETURN == e.keyCode)
510
+ this.handleFormSubmission(e);
511
+ },
512
+ createControl: function(mode, handler, extraClasses) {
513
+ var control = this.options[mode + 'Control'];
514
+ var text = this.options[mode + 'Text'];
515
+ if ('button' == control) {
516
+ var btn = document.createElement('input');
517
+ btn.type = 'submit';
518
+ btn.value = text;
519
+ btn.className = 'editor_' + mode + '_button';
520
+ if ('cancel' == mode)
521
+ btn.onclick = this._boundCancelHandler;
522
+ this._form.appendChild(btn);
523
+ this._controls[mode] = btn;
524
+ } else if ('link' == control) {
525
+ var link = document.createElement('a');
526
+ link.href = '#';
527
+ link.appendChild(document.createTextNode(text));
528
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
529
+ link.className = 'editor_' + mode + '_link';
530
+ if (extraClasses)
531
+ link.className += ' ' + extraClasses;
532
+ this._form.appendChild(link);
533
+ this._controls[mode] = link;
506
534
  }
507
535
  },
508
- enterEditMode: function(evt) {
509
- if (this.saving) return;
510
- if (this.editing) return;
511
- this.editing = true;
512
- this.onEnterEditMode();
513
- if (this.options.externalControl) {
514
- Element.hide(this.options.externalControl);
515
- }
516
- Element.hide(this.element);
517
- this.createForm();
518
- this.element.parentNode.insertBefore(this.form, this.element);
519
- Field.scrollFreeActivate(this.editField);
520
- // stop the event to avoid a page refresh in Safari
521
- if (evt) {
522
- Event.stop(evt);
536
+ createEditField: function() {
537
+ var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
538
+ var fld;
539
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
540
+ fld = document.createElement('input');
541
+ fld.type = 'text';
542
+ var size = this.options.size || this.options.cols || 0;
543
+ if (0 < size) fld.size = size;
544
+ } else {
545
+ fld = document.createElement('textarea');
546
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
547
+ fld.cols = this.options.cols || 40;
523
548
  }
524
- return false;
549
+ fld.name = this.options.paramName;
550
+ fld.value = text; // No HTML breaks conversion anymore
551
+ fld.className = 'editor_field';
552
+ if (this.options.submitOnBlur)
553
+ fld.onblur = this._boundSubmitHandler;
554
+ this._controls.editor = fld;
555
+ if (this.options.loadTextURL)
556
+ this.loadExternalText();
557
+ this._form.appendChild(this._controls.editor);
525
558
  },
526
559
  createForm: function() {
527
- this.form = document.createElement("form");
528
- this.form.id = this.options.formId;
529
- Element.addClassName(this.form, this.options.formClassName)
530
- this.form.onsubmit = this.onSubmit.bind(this);
531
-
560
+ var ipe = this;
561
+ function addText(mode, condition) {
562
+ var text = ipe.options['text' + mode + 'Controls'];
563
+ if (!text || condition === false) return;
564
+ ipe._form.appendChild(document.createTextNode(text));
565
+ };
566
+ this._form = $(document.createElement('form'));
567
+ this._form.id = this.options.formId;
568
+ this._form.addClassName(this.options.formClassName);
569
+ this._form.onsubmit = this._boundSubmitHandler;
532
570
  this.createEditField();
533
-
534
- if (this.options.textarea) {
535
- var br = document.createElement("br");
536
- this.form.appendChild(br);
537
- }
538
-
539
- okButton = document.createElement("input");
540
- okButton.type = "submit";
541
- okButton.value = this.options.okText;
542
- this.form.appendChild(okButton);
543
-
544
- cancelLink = document.createElement("a");
545
- cancelLink.href = "#";
546
- cancelLink.appendChild(document.createTextNode(this.options.cancelText));
547
- cancelLink.onclick = this.onclickCancel.bind(this);
548
- this.form.appendChild(cancelLink);
571
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
572
+ this._form.appendChild(document.createElement('br'));
573
+ if (this.options.onFormCustomization)
574
+ this.options.onFormCustomization(this, this._form);
575
+ addText('Before', this.options.okControl || this.options.cancelControl);
576
+ this.createControl('ok', this._boundSubmitHandler);
577
+ addText('Between', this.options.okControl && this.options.cancelControl);
578
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
579
+ addText('After', this.options.okControl || this.options.cancelControl);
580
+ },
581
+ destroy: function() {
582
+ if (this._oldInnerHTML)
583
+ this.element.innerHTML = this._oldInnerHTML;
584
+ this.leaveEditMode();
585
+ this.unregisterListeners();
586
+ },
587
+ enterEditMode: function(e) {
588
+ if (this._saving || this._editing) return;
589
+ this._editing = true;
590
+ this.triggerCallback('onEnterEditMode');
591
+ if (this.options.externalControl)
592
+ this.options.externalControl.hide();
593
+ this.element.hide();
594
+ this.createForm();
595
+ this.element.parentNode.insertBefore(this._form, this.element);
596
+ if (!this.options.loadTextURL)
597
+ this.postProcessEditField();
598
+ if (e) Event.stop(e);
549
599
  },
550
- hasHTMLLineBreaks: function(string) {
551
- if (!this.options.handleLineBreaks) return false;
552
- return string.match(/<br/i) || string.match(/<p>/i);
600
+ enterHover: function(e) {
601
+ if (this.options.hoverClassName)
602
+ this.element.addClassName(this.options.hoverClassName);
603
+ if (this._saving) return;
604
+ this.triggerCallback('onEnterHover');
553
605
  },
554
- convertHTMLLineBreaks: function(string) {
555
- return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
606
+ getText: function() {
607
+ return this.element.innerHTML.unescapeHTML();
556
608
  },
557
- createEditField: function() {
558
- var text;
559
- if(this.options.loadTextURL) {
560
- text = this.options.loadingText;
561
- } else {
562
- text = this.getText();
609
+ handleAJAXFailure: function(transport) {
610
+ this.triggerCallback('onFailure', transport);
611
+ if (this._oldInnerHTML) {
612
+ this.element.innerHTML = this._oldInnerHTML;
613
+ this._oldInnerHTML = null;
563
614
  }
564
-
565
- if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
566
- this.options.textarea = false;
567
- var textField = document.createElement("input");
568
- textField.type = "text";
569
- textField.name = "value";
570
- textField.value = text;
571
- textField.style.backgroundColor = this.options.highlightcolor;
572
- var size = this.options.size || this.options.cols || 0;
573
- if (size != 0) textField.size = size;
574
- this.editField = textField;
615
+ },
616
+ handleFormCancellation: function(e) {
617
+ this.wrapUp();
618
+ if (e) Event.stop(e);
619
+ },
620
+ handleFormSubmission: function(e) {
621
+ var form = this._form;
622
+ var value = $F(this._controls.editor);
623
+ this.prepareSubmission();
624
+ var params = this.options.callback(form, value) || '';
625
+ if (Object.isString(params))
626
+ params = params.toQueryParams();
627
+ params.editorId = this.element.id;
628
+ if (this.options.htmlResponse) {
629
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
630
+ Object.extend(options, {
631
+ parameters: params,
632
+ onComplete: this._boundWrapperHandler,
633
+ onFailure: this._boundFailureHandler
634
+ });
635
+ new Ajax.Updater({ success: this.element }, this.url, options);
575
636
  } else {
576
- this.options.textarea = true;
577
- var textArea = document.createElement("textarea");
578
- textArea.name = "value";
579
- textArea.value = this.convertHTMLLineBreaks(text);
580
- textArea.rows = this.options.rows;
581
- textArea.cols = this.options.cols || 40;
582
- this.editField = textArea;
583
- }
584
-
585
- if(this.options.loadTextURL) {
586
- this.loadExternalText();
637
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
638
+ Object.extend(options, {
639
+ parameters: params,
640
+ onComplete: this._boundWrapperHandler,
641
+ onFailure: this._boundFailureHandler
642
+ });
643
+ new Ajax.Request(this.url, options);
587
644
  }
588
- this.form.appendChild(this.editField);
645
+ if (e) Event.stop(e);
589
646
  },
590
- getText: function() {
591
- return this.element.innerHTML;
647
+ leaveEditMode: function() {
648
+ this.element.removeClassName(this.options.savingClassName);
649
+ this.removeForm();
650
+ this.leaveHover();
651
+ this.element.style.backgroundColor = this._originalBackground;
652
+ this.element.show();
653
+ if (this.options.externalControl)
654
+ this.options.externalControl.show();
655
+ this._saving = false;
656
+ this._editing = false;
657
+ this._oldInnerHTML = null;
658
+ this.triggerCallback('onLeaveEditMode');
659
+ },
660
+ leaveHover: function(e) {
661
+ if (this.options.hoverClassName)
662
+ this.element.removeClassName(this.options.hoverClassName);
663
+ if (this._saving) return;
664
+ this.triggerCallback('onLeaveHover');
592
665
  },
593
666
  loadExternalText: function() {
594
- Element.addClassName(this.form, this.options.loadingClassName);
595
- this.editField.disabled = true;
596
- new Ajax.Request(
597
- this.options.loadTextURL,
598
- Object.extend({
599
- asynchronous: true,
600
- onComplete: this.onLoadedExternalText.bind(this)
601
- }, this.options.ajaxOptions)
602
- );
603
- },
604
- onLoadedExternalText: function(transport) {
605
- Element.removeClassName(this.form, this.options.loadingClassName);
606
- this.editField.disabled = false;
607
- this.editField.value = transport.responseText.stripTags();
608
- },
609
- onclickCancel: function() {
610
- this.onComplete();
611
- this.leaveEditMode();
612
- return false;
613
- },
614
- onFailure: function(transport) {
615
- this.options.onFailure(transport);
616
- if (this.oldInnerHTML) {
617
- this.element.innerHTML = this.oldInnerHTML;
618
- this.oldInnerHTML = null;
619
- }
620
- return false;
621
- },
622
- onSubmit: function() {
623
- // onLoading resets these so we need to save them away for the Ajax call
624
- var form = this.form;
625
- var value = this.editField.value;
626
-
627
- // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
628
- // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
629
- // to be displayed indefinitely
630
- this.onLoading();
631
-
632
- new Ajax.Updater(
633
- {
634
- success: this.element,
635
- // don't update on failure (this could be an option)
636
- failure: null
637
- },
638
- this.url,
639
- Object.extend({
640
- parameters: this.options.callback(form, value),
641
- onComplete: this.onComplete.bind(this),
642
- onFailure: this.onFailure.bind(this)
643
- }, this.options.ajaxOptions)
644
- );
645
- // stop the event to avoid a page refresh in Safari
646
- if (arguments.length > 1) {
647
- Event.stop(arguments[0]);
648
- }
649
- return false;
650
- },
651
- onLoading: function() {
652
- this.saving = true;
667
+ this._form.addClassName(this.options.loadingClassName);
668
+ this._controls.editor.disabled = true;
669
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
670
+ Object.extend(options, {
671
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
672
+ onComplete: Prototype.emptyFunction,
673
+ onSuccess: function(transport) {
674
+ this._form.removeClassName(this.options.loadingClassName);
675
+ var text = transport.responseText;
676
+ if (this.options.stripLoadedTextTags)
677
+ text = text.stripTags();
678
+ this._controls.editor.value = text;
679
+ this._controls.editor.disabled = false;
680
+ this.postProcessEditField();
681
+ }.bind(this),
682
+ onFailure: this._boundFailureHandler
683
+ });
684
+ new Ajax.Request(this.options.loadTextURL, options);
685
+ },
686
+ postProcessEditField: function() {
687
+ var fpc = this.options.fieldPostCreation;
688
+ if (fpc)
689
+ $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
690
+ },
691
+ prepareOptions: function() {
692
+ this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
693
+ Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
694
+ [this._extraDefaultOptions].flatten().compact().each(function(defs) {
695
+ Object.extend(this.options, defs);
696
+ }.bind(this));
697
+ },
698
+ prepareSubmission: function() {
699
+ this._saving = true;
653
700
  this.removeForm();
654
701
  this.leaveHover();
655
702
  this.showSaving();
656
703
  },
704
+ registerListeners: function() {
705
+ this._listeners = { };
706
+ var listener;
707
+ $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
708
+ listener = this[pair.value].bind(this);
709
+ this._listeners[pair.key] = listener;
710
+ if (!this.options.externalControlOnly)
711
+ this.element.observe(pair.key, listener);
712
+ if (this.options.externalControl)
713
+ this.options.externalControl.observe(pair.key, listener);
714
+ }.bind(this));
715
+ },
716
+ removeForm: function() {
717
+ if (!this._form) return;
718
+ this._form.remove();
719
+ this._form = null;
720
+ this._controls = { };
721
+ },
657
722
  showSaving: function() {
658
- this.oldInnerHTML = this.element.innerHTML;
723
+ this._oldInnerHTML = this.element.innerHTML;
659
724
  this.element.innerHTML = this.options.savingText;
660
- Element.addClassName(this.element, this.options.savingClassName);
661
- this.element.style.backgroundColor = this.originalBackground;
662
- Element.show(this.element);
725
+ this.element.addClassName(this.options.savingClassName);
726
+ this.element.style.backgroundColor = this._originalBackground;
727
+ this.element.show();
663
728
  },
664
- removeForm: function() {
665
- if(this.form) {
666
- if (this.form.parentNode) Element.remove(this.form);
667
- this.form = null;
729
+ triggerCallback: function(cbName, arg) {
730
+ if ('function' == typeof this.options[cbName]) {
731
+ this.options[cbName](this, arg);
668
732
  }
669
733
  },
670
- enterHover: function() {
671
- if (this.saving) return;
672
- this.element.style.backgroundColor = this.options.highlightcolor;
673
- if (this.effect) {
674
- this.effect.cancel();
675
- }
676
- Element.addClassName(this.element, this.options.hoverClassName)
734
+ unregisterListeners: function() {
735
+ $H(this._listeners).each(function(pair) {
736
+ if (!this.options.externalControlOnly)
737
+ this.element.stopObserving(pair.key, pair.value);
738
+ if (this.options.externalControl)
739
+ this.options.externalControl.stopObserving(pair.key, pair.value);
740
+ }.bind(this));
677
741
  },
678
- leaveHover: function() {
679
- if (this.options.backgroundColor) {
680
- this.element.style.backgroundColor = this.oldBackground;
681
- }
682
- Element.removeClassName(this.element, this.options.hoverClassName)
683
- if (this.saving) return;
684
- this.effect = new Effect.Highlight(this.element, {
685
- startcolor: this.options.highlightcolor,
686
- endcolor: this.options.highlightendcolor,
687
- restorecolor: this.originalBackground
742
+ wrapUp: function(transport) {
743
+ this.leaveEditMode();
744
+ // Can't use triggerCallback due to backward compatibility: requires
745
+ // binding + direct element
746
+ this._boundComplete(transport, this.element);
747
+ }
748
+ });
749
+
750
+ Object.extend(Ajax.InPlaceEditor.prototype, {
751
+ dispose: Ajax.InPlaceEditor.prototype.destroy
752
+ });
753
+
754
+ Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
755
+ initialize: function($super, element, url, options) {
756
+ this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
757
+ $super(element, url, options);
758
+ },
759
+
760
+ createEditField: function() {
761
+ var list = document.createElement('select');
762
+ list.name = this.options.paramName;
763
+ list.size = 1;
764
+ this._controls.editor = list;
765
+ this._collection = this.options.collection || [];
766
+ if (this.options.loadCollectionURL)
767
+ this.loadCollection();
768
+ else
769
+ this.checkForExternalText();
770
+ this._form.appendChild(this._controls.editor);
771
+ },
772
+
773
+ loadCollection: function() {
774
+ this._form.addClassName(this.options.loadingClassName);
775
+ this.showLoadingText(this.options.loadingCollectionText);
776
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
777
+ Object.extend(options, {
778
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
779
+ onComplete: Prototype.emptyFunction,
780
+ onSuccess: function(transport) {
781
+ var js = transport.responseText.strip();
782
+ if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
783
+ throw('Server returned an invalid collection representation.');
784
+ this._collection = eval(js);
785
+ this.checkForExternalText();
786
+ }.bind(this),
787
+ onFailure: this.onFailure
688
788
  });
789
+ new Ajax.Request(this.options.loadCollectionURL, options);
689
790
  },
690
- leaveEditMode: function() {
691
- Element.removeClassName(this.element, this.options.savingClassName);
692
- this.removeForm();
693
- this.leaveHover();
694
- this.element.style.backgroundColor = this.originalBackground;
695
- Element.show(this.element);
696
- if (this.options.externalControl) {
697
- Element.show(this.options.externalControl);
791
+
792
+ showLoadingText: function(text) {
793
+ this._controls.editor.disabled = true;
794
+ var tempOption = this._controls.editor.firstChild;
795
+ if (!tempOption) {
796
+ tempOption = document.createElement('option');
797
+ tempOption.value = '';
798
+ this._controls.editor.appendChild(tempOption);
799
+ tempOption.selected = true;
698
800
  }
699
- this.editing = false;
700
- this.saving = false;
701
- this.oldInnerHTML = null;
702
- this.onLeaveEditMode();
801
+ tempOption.update((text || '').stripScripts().stripTags());
703
802
  },
704
- onComplete: function(transport) {
705
- this.leaveEditMode();
706
- this.options.onComplete.bind(this)(transport, this.element);
803
+
804
+ checkForExternalText: function() {
805
+ this._text = this.getText();
806
+ if (this.options.loadTextURL)
807
+ this.loadExternalText();
808
+ else
809
+ this.buildOptionList();
707
810
  },
708
- onEnterEditMode: function() {},
709
- onLeaveEditMode: function() {},
710
- dispose: function() {
711
- if (this.oldInnerHTML) {
712
- this.element.innerHTML = this.oldInnerHTML;
713
- }
714
- this.leaveEditMode();
715
- Event.stopObserving(this.element, 'click', this.onclickListener);
716
- Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
717
- Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
718
- if (this.options.externalControl) {
719
- Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
720
- Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
721
- Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
811
+
812
+ loadExternalText: function() {
813
+ this.showLoadingText(this.options.loadingText);
814
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
815
+ Object.extend(options, {
816
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
817
+ onComplete: Prototype.emptyFunction,
818
+ onSuccess: function(transport) {
819
+ this._text = transport.responseText.strip();
820
+ this.buildOptionList();
821
+ }.bind(this),
822
+ onFailure: this.onFailure
823
+ });
824
+ new Ajax.Request(this.options.loadTextURL, options);
825
+ },
826
+
827
+ buildOptionList: function() {
828
+ this._form.removeClassName(this.options.loadingClassName);
829
+ this._collection = this._collection.map(function(entry) {
830
+ return 2 === entry.length ? entry : [entry, entry].flatten();
831
+ });
832
+ var marker = ('value' in this.options) ? this.options.value : this._text;
833
+ var textFound = this._collection.any(function(entry) {
834
+ return entry[0] == marker;
835
+ }.bind(this));
836
+ this._controls.editor.update('');
837
+ var option;
838
+ this._collection.each(function(entry, index) {
839
+ option = document.createElement('option');
840
+ option.value = entry[0];
841
+ option.selected = textFound ? entry[0] == marker : 0 == index;
842
+ option.appendChild(document.createTextNode(entry[1]));
843
+ this._controls.editor.appendChild(option);
844
+ }.bind(this));
845
+ this._controls.editor.disabled = false;
846
+ Field.scrollFreeActivate(this._controls.editor);
847
+ }
848
+ });
849
+
850
+ //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
851
+ //**** This only exists for a while, in order to let ****
852
+ //**** users adapt to the new API. Read up on the new ****
853
+ //**** API and convert your code to it ASAP! ****
854
+
855
+ Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
856
+ if (!options) return;
857
+ function fallback(name, expr) {
858
+ if (name in options || expr === undefined) return;
859
+ options[name] = expr;
860
+ };
861
+ fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
862
+ options.cancelLink == options.cancelButton == false ? false : undefined)));
863
+ fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
864
+ options.okLink == options.okButton == false ? false : undefined)));
865
+ fallback('highlightColor', options.highlightcolor);
866
+ fallback('highlightEndColor', options.highlightendcolor);
867
+ };
868
+
869
+ Object.extend(Ajax.InPlaceEditor, {
870
+ DefaultOptions: {
871
+ ajaxOptions: { },
872
+ autoRows: 3, // Use when multi-line w/ rows == 1
873
+ cancelControl: 'link', // 'link'|'button'|false
874
+ cancelText: 'cancel',
875
+ clickToEditText: 'Click to edit',
876
+ externalControl: null, // id|elt
877
+ externalControlOnly: false,
878
+ fieldPostCreation: 'activate', // 'activate'|'focus'|false
879
+ formClassName: 'inplaceeditor-form',
880
+ formId: null, // id|elt
881
+ highlightColor: '#ffff99',
882
+ highlightEndColor: '#ffffff',
883
+ hoverClassName: '',
884
+ htmlResponse: true,
885
+ loadingClassName: 'inplaceeditor-loading',
886
+ loadingText: 'Loading...',
887
+ okControl: 'button', // 'link'|'button'|false
888
+ okText: 'ok',
889
+ paramName: 'value',
890
+ rows: 1, // If 1 and multi-line, uses autoRows
891
+ savingClassName: 'inplaceeditor-saving',
892
+ savingText: 'Saving...',
893
+ size: 0,
894
+ stripLoadedTextTags: false,
895
+ submitOnBlur: false,
896
+ textAfterControls: '',
897
+ textBeforeControls: '',
898
+ textBetweenControls: ''
899
+ },
900
+ DefaultCallbacks: {
901
+ callback: function(form) {
902
+ return Form.serialize(form);
903
+ },
904
+ onComplete: function(transport, element) {
905
+ // For backward compatibility, this one is bound to the IPE, and passes
906
+ // the element directly. It was too often customized, so we don't break it.
907
+ new Effect.Highlight(element, {
908
+ startcolor: this.options.highlightColor, keepBackgroundImage: true });
909
+ },
910
+ onEnterEditMode: null,
911
+ onEnterHover: function(ipe) {
912
+ ipe.element.style.backgroundColor = ipe.options.highlightColor;
913
+ if (ipe._effect)
914
+ ipe._effect.cancel();
915
+ },
916
+ onFailure: function(transport, ipe) {
917
+ alert('Error communication with the server: ' + transport.responseText.stripTags());
918
+ },
919
+ onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
920
+ onLeaveEditMode: null,
921
+ onLeaveHover: function(ipe) {
922
+ ipe._effect = new Effect.Highlight(ipe.element, {
923
+ startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
924
+ restorecolor: ipe._originalBackground, keepBackgroundImage: true
925
+ });
722
926
  }
927
+ },
928
+ Listeners: {
929
+ click: 'enterEditMode',
930
+ keydown: 'checkForEscapeOrReturn',
931
+ mouseover: 'enterHover',
932
+ mouseout: 'leaveHover'
723
933
  }
934
+ });
935
+
936
+ Ajax.InPlaceCollectionEditor.DefaultOptions = {
937
+ loadingCollectionText: 'Loading options...'
724
938
  };
725
939
 
726
- // Delayed observer, like Form.Element.Observer,
940
+ // Delayed observer, like Form.Element.Observer,
727
941
  // but waits for delay after last key input
728
942
  // Ideal for live-search fields
729
943
 
730
- Form.Element.DelayedObserver = Class.create();
731
- Form.Element.DelayedObserver.prototype = {
944
+ Form.Element.DelayedObserver = Class.create({
732
945
  initialize: function(element, delay, callback) {
733
946
  this.delay = delay || 0.5;
734
947
  this.element = $(element);
735
948
  this.callback = callback;
736
949
  this.timer = null;
737
- this.lastValue = $F(this.element);
950
+ this.lastValue = $F(this.element);
738
951
  Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
739
952
  },
740
953
  delayedListener: function(event) {
@@ -747,4 +960,4 @@ Form.Element.DelayedObserver.prototype = {
747
960
  this.timer = null;
748
961
  this.callback(this.element, $F(this.element));
749
962
  }
750
- };
963
+ });