]> git.openstreetmap.org Git - rails.git/blob - public/javascripts/controls.js
Don't try and update links on nodes whose names match member functions
[rails.git] / public / javascripts / controls.js
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)
4 // Contributors:
5 //  Richard Livsey
6 //  Rahul Bhargava
7 //  Rob Wills
8 // 
9 // See scriptaculous.js for full license.
10
11 // Autocompleter.Base handles all the autocompletion functionality 
12 // that's independent of the data source for autocompletion. This
13 // includes drawing the autocompletion menu, observing keyboard
14 // and mouse events, and similar.
15 //
16 // Specific autocompleters need to provide, at the very least, 
17 // a getUpdatedChoices function that will be invoked every time
18 // the text inside the monitored textbox changes. This method 
19 // should get the text for which to provide autocompletion by
20 // invoking this.getToken(), NOT by directly accessing
21 // this.element.value. This is to allow incremental tokenized
22 // autocompletion. Specific auto-completion logic (AJAX, etc)
23 // belongs in getUpdatedChoices.
24 //
25 // Tokenized incremental autocompletion is enabled automatically
26 // when an autocompleter is instantiated with the 'tokens' option
27 // in the options parameter, e.g.:
28 // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
29 // will incrementally autocomplete with a comma as the token.
30 // Additionally, ',' in the above example can be replaced with
31 // 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 
34 // allows smart autocompletion after linebreaks.
35
36 var Autocompleter = {}
37 Autocompleter.Base = function() {};
38 Autocompleter.Base.prototype = {
39   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;     
46     this.entryCount  = 0;
47
48     if (this.setOptions)
49       this.setOptions(options);
50     else
51       this.options = options || {};
52
53     this.options.paramName    = this.options.paramName || this.element.name;
54     this.options.tokens       = this.options.tokens || [];
55     this.options.frequency    = this.options.frequency || 0.4;
56     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}) };
67
68     if (typeof(this.options.tokens) == 'string') 
69       this.options.tokens = new Array(this.options.tokens);
70
71     this.observer = null;
72     
73     this.element.setAttribute('autocomplete','off');
74
75     Element.hide(this.update);
76
77     Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
78     Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
79   },
80
81   show: function() {
82     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) &&
86       (Element.getStyle(this.update, 'position')=='absolute')) {
87       new Insertion.After(this.update, 
88        '<iframe id="' + this.update.id + '_iefix" '+
89        'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
90        'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
91       this.iefix = $(this.update.id+'_iefix');
92     }
93     if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
94   },
95   
96   fixIEOverlapping: function() {
97     Position.clone(this.update, this.iefix);
98     this.iefix.style.zIndex = 1;
99     this.update.style.zIndex = 2;
100     Element.show(this.iefix);
101   },
102
103   hide: function() {
104     this.stopIndicator();
105     if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
106     if(this.iefix) Element.hide(this.iefix);
107   },
108
109   startIndicator: function() {
110     if(this.options.indicator) Element.show(this.options.indicator);
111   },
112
113   stopIndicator: function() {
114     if(this.options.indicator) Element.hide(this.options.indicator);
115   },
116
117   onKeyPress: function(event) {
118     if(this.active)
119       switch(event.keyCode) {
120        case Event.KEY_TAB:
121        case Event.KEY_RETURN:
122          this.selectEntry();
123          Event.stop(event);
124        case Event.KEY_ESC:
125          this.hide();
126          this.active = false;
127          Event.stop(event);
128          return;
129        case Event.KEY_LEFT:
130        case Event.KEY_RIGHT:
131          return;
132        case Event.KEY_UP:
133          this.markPrevious();
134          this.render();
135          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
136          return;
137        case Event.KEY_DOWN:
138          this.markNext();
139          this.render();
140          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
141          return;
142       }
143      else 
144        if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
145          (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
146
147     this.changed = true;
148     this.hasFocus = true;
149
150     if(this.observer) clearTimeout(this.observer);
151       this.observer = 
152         setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
153   },
154
155   activate: function() {
156     this.changed = false;
157     this.hasFocus = true;
158     this.getUpdatedChoices();
159   },
160
161   onHover: function(event) {
162     var element = Event.findElement(event, 'LI');
163     if(this.index != element.autocompleteIndex) 
164     {
165         this.index = element.autocompleteIndex;
166         this.render();
167     }
168     Event.stop(event);
169   },
170   
171   onClick: function(event) {
172     var element = Event.findElement(event, 'LI');
173     this.index = element.autocompleteIndex;
174     this.selectEntry();
175     this.hide();
176   },
177   
178   onBlur: function(event) {
179     // needed to make click events working
180     setTimeout(this.hide.bind(this), 250);
181     this.hasFocus = false;
182     this.active = false;     
183   }, 
184   
185   render: function() {
186     if(this.entryCount > 0) {
187       for (var i = 0; i < this.entryCount; i++)
188         this.index==i ? 
189           Element.addClassName(this.getEntry(i),"selected") : 
190           Element.removeClassName(this.getEntry(i),"selected");
191         
192       if(this.hasFocus) { 
193         this.show();
194         this.active = true;
195       }
196     } else {
197       this.active = false;
198       this.hide();
199     }
200   },
201   
202   markPrevious: function() {
203     if(this.index > 0) this.index--
204       else this.index = this.entryCount-1;
205   },
206   
207   markNext: function() {
208     if(this.index < this.entryCount-1) this.index++
209       else this.index = 0;
210   },
211   
212   getEntry: function(index) {
213     return this.update.firstChild.childNodes[index];
214   },
215   
216   getCurrentEntry: function() {
217     return this.getEntry(this.index);
218   },
219   
220   selectEntry: function() {
221     this.active = false;
222     this.updateElement(this.getCurrentEntry());
223   },
224
225   updateElement: function(selectedElement) {
226     if (this.options.updateElement) {
227       this.options.updateElement(selectedElement);
228       return;
229     }
230     var value = '';
231     if (this.options.select) {
232       var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
233       if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
234     } else
235       value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
236     
237     var lastTokenPos = this.findLastToken();
238     if (lastTokenPos != -1) {
239       var newValue = this.element.value.substr(0, lastTokenPos + 1);
240       var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
241       if (whitespace)
242         newValue += whitespace[0];
243       this.element.value = newValue + value;
244     } else {
245       this.element.value = value;
246     }
247     this.element.focus();
248     
249     if (this.options.afterUpdateElement)
250       this.options.afterUpdateElement(this.element, selectedElement);
251   },
252
253   updateChoices: function(choices) {
254     if(!this.changed && this.hasFocus) {
255       this.update.innerHTML = choices;
256       Element.cleanWhitespace(this.update);
257       Element.cleanWhitespace(this.update.firstChild);
258
259       if(this.update.firstChild && this.update.firstChild.childNodes) {
260         this.entryCount = 
261           this.update.firstChild.childNodes.length;
262         for (var i = 0; i < this.entryCount; i++) {
263           var entry = this.getEntry(i);
264           entry.autocompleteIndex = i;
265           this.addObservers(entry);
266         }
267       } else { 
268         this.entryCount = 0;
269       }
270
271       this.stopIndicator();
272
273       this.index = 0;
274       this.render();
275     }
276   },
277
278   addObservers: function(element) {
279     Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
280     Event.observe(element, "click", this.onClick.bindAsEventListener(this));
281   },
282
283   onObserverEvent: function() {
284     this.changed = false;   
285     if(this.getToken().length>=this.options.minChars) {
286       this.startIndicator();
287       this.getUpdatedChoices();
288     } else {
289       this.active = false;
290       this.hide();
291     }
292   },
293
294   getToken: function() {
295     var tokenPos = this.findLastToken();
296     if (tokenPos != -1)
297       var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
298     else
299       var ret = this.element.value;
300
301     return /\n/.test(ret) ? '' : ret;
302   },
303
304   findLastToken: function() {
305     var lastTokenPos = -1;
306
307     for (var i=0; i<this.options.tokens.length; i++) {
308       var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
309       if (thisTokenPos > lastTokenPos)
310         lastTokenPos = thisTokenPos;
311     }
312     return lastTokenPos;
313   }
314 }
315
316 Ajax.Autocompleter = Class.create();
317 Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
318   initialize: function(element, update, url, options) {
319     this.baseInitialize(element, update, options);
320     this.options.asynchronous  = true;
321     this.options.onComplete    = this.onComplete.bind(this);
322     this.options.defaultParams = this.options.parameters || null;
323     this.url                   = url;
324   },
325
326   getUpdatedChoices: function() {
327     entry = encodeURIComponent(this.options.paramName) + '=' + 
328       encodeURIComponent(this.getToken());
329
330     this.options.parameters = this.options.callback ?
331       this.options.callback(this.element, entry) : entry;
332
333     if(this.options.defaultParams) 
334       this.options.parameters += '&' + this.options.defaultParams;
335
336     new Ajax.Request(this.url, this.options);
337   },
338
339   onComplete: function(request) {
340     this.updateChoices(request.responseText);
341   }
342
343 });
344
345 // The local array autocompleter. Used when you'd prefer to
346 // inject an array of autocompletion options into the page, rather
347 // than sending out Ajax queries, which can be quite slow sometimes.
348 //
349 // The constructor takes four parameters. The first two are, as usual,
350 // the id of the monitored textbox, and id of the autocompletion menu.
351 // The third is the array you want to autocomplete from, and the fourth
352 // is the options block.
353 //
354 // Extra local autocompletion options:
355 // - choices - How many autocompletion choices to offer
356 //
357 // - partialSearch - If false, the autocompleter will match entered
358 //                    text only at the beginning of strings in the 
359 //                    autocomplete array. Defaults to true, which will
360 //                    match text at the beginning of any *word* in the
361 //                    strings in the autocomplete array. If you want to
362 //                    search anywhere in the string, additionally set
363 //                    the option fullSearch to true (default: off).
364 //
365 // - fullSsearch - Search anywhere in autocomplete array strings.
366 //
367 // - partialChars - How many characters to enter before triggering
368 //                   a partial match (unlike minChars, which defines
369 //                   how many characters are required to do any match
370 //                   at all). Defaults to 2.
371 //
372 // - ignoreCase - Whether to ignore case when autocompleting.
373 //                 Defaults to true.
374 //
375 // It's possible to pass in a custom function as the 'selector' 
376 // option, if you prefer to write your own autocompletion logic.
377 // In that case, the other options above will not apply unless
378 // you support them.
379
380 Autocompleter.Local = Class.create();
381 Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
382   initialize: function(element, update, array, options) {
383     this.baseInitialize(element, update, options);
384     this.options.array = array;
385   },
386
387   getUpdatedChoices: function() {
388     this.updateChoices(this.options.selector(this));
389   },
390
391   setOptions: function(options) {
392     this.options = Object.extend({
393       choices: 10,
394       partialSearch: true,
395       partialChars: 2,
396       ignoreCase: true,
397       fullSearch: false,
398       selector: function(instance) {
399         var ret       = []; // Beginning matches
400         var partial   = []; // Inside matches
401         var entry     = instance.getToken();
402         var count     = 0;
403
404         for (var i = 0; i < instance.options.array.length &&  
405           ret.length < instance.options.choices ; i++) { 
406
407           var elem = instance.options.array[i];
408           var foundPos = instance.options.ignoreCase ? 
409             elem.toLowerCase().indexOf(entry.toLowerCase()) : 
410             elem.indexOf(entry);
411
412           while (foundPos != -1) {
413             if (foundPos == 0 && elem.length != entry.length) { 
414               ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
415                 elem.substr(entry.length) + "</li>");
416               break;
417             } else if (entry.length >= instance.options.partialChars && 
418               instance.options.partialSearch && foundPos != -1) {
419               if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
420                 partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
421                   elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
422                   foundPos + entry.length) + "</li>");
423                 break;
424               }
425             }
426
427             foundPos = instance.options.ignoreCase ? 
428               elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
429               elem.indexOf(entry, foundPos + 1);
430
431           }
432         }
433         if (partial.length)
434           ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
435         return "<ul>" + ret.join('') + "</ul>";
436       }
437     }, options || {});
438   }
439 });
440
441 // AJAX in-place editor
442 //
443 // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
444
445 // Use this if you notice weird scrolling problems on some browsers,
446 // the DOM might be a bit confused when this gets called so do this
447 // waits 1 ms (with setTimeout) until it does the activation
448 Field.scrollFreeActivate = function(field) {
449   setTimeout(function() {
450     Field.activate(field);
451   }, 1);
452 }
453
454 Ajax.InPlaceEditor = Class.create();
455 Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
456 Ajax.InPlaceEditor.prototype = {
457   initialize: function(element, url, options) {
458     this.url = url;
459     this.element = $(element);
460
461     this.options = Object.extend({
462       okButton: true,
463       okText: "ok",
464       cancelLink: true,
465       cancelText: "cancel",
466       savingText: "Saving...",
467       clickToEditText: "Click to edit",
468       okText: "ok",
469       rows: 1,
470       onComplete: function(transport, element) {
471         new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
472       },
473       onFailure: function(transport) {
474         alert("Error communicating with the server: " + transport.responseText.stripTags());
475       },
476       callback: function(form) {
477         return Form.serialize(form);
478       },
479       handleLineBreaks: true,
480       loadingText: 'Loading...',
481       savingClassName: 'inplaceeditor-saving',
482       loadingClassName: 'inplaceeditor-loading',
483       formClassName: 'inplaceeditor-form',
484       highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
485       highlightendcolor: "#FFFFFF",
486       externalControl: null,
487       submitOnBlur: false,
488       ajaxOptions: {},
489       evalScripts: false
490     }, options || {});
491
492     if(!this.options.formId && this.element.id) {
493       this.options.formId = this.element.id + "-inplaceeditor";
494       if ($(this.options.formId)) {
495         // there's already a form with that name, don't specify an id
496         this.options.formId = null;
497       }
498     }
499     
500     if (this.options.externalControl) {
501       this.options.externalControl = $(this.options.externalControl);
502     }
503     
504     this.originalBackground = Element.getStyle(this.element, 'background-color');
505     if (!this.originalBackground) {
506       this.originalBackground = "transparent";
507     }
508     
509     this.element.title = this.options.clickToEditText;
510     
511     this.onclickListener = this.enterEditMode.bindAsEventListener(this);
512     this.mouseoverListener = this.enterHover.bindAsEventListener(this);
513     this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
514     Event.observe(this.element, 'click', this.onclickListener);
515     Event.observe(this.element, 'mouseover', this.mouseoverListener);
516     Event.observe(this.element, 'mouseout', this.mouseoutListener);
517     if (this.options.externalControl) {
518       Event.observe(this.options.externalControl, 'click', this.onclickListener);
519       Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
520       Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
521     }
522   },
523   enterEditMode: function(evt) {
524     if (this.saving) return;
525     if (this.editing) return;
526     this.editing = true;
527     this.onEnterEditMode();
528     if (this.options.externalControl) {
529       Element.hide(this.options.externalControl);
530     }
531     Element.hide(this.element);
532     this.createForm();
533     this.element.parentNode.insertBefore(this.form, this.element);
534     Field.scrollFreeActivate(this.editField);
535     // stop the event to avoid a page refresh in Safari
536     if (evt) {
537       Event.stop(evt);
538     }
539     return false;
540   },
541   createForm: function() {
542     this.form = document.createElement("form");
543     this.form.id = this.options.formId;
544     Element.addClassName(this.form, this.options.formClassName)
545     this.form.onsubmit = this.onSubmit.bind(this);
546
547     this.createEditField();
548
549     if (this.options.textarea) {
550       var br = document.createElement("br");
551       this.form.appendChild(br);
552     }
553
554     if (this.options.okButton) {
555       okButton = document.createElement("input");
556       okButton.type = "submit";
557       okButton.value = this.options.okText;
558       okButton.className = 'editor_ok_button';
559       this.form.appendChild(okButton);
560     }
561
562     if (this.options.cancelLink) {
563       cancelLink = document.createElement("a");
564       cancelLink.href = "#";
565       cancelLink.appendChild(document.createTextNode(this.options.cancelText));
566       cancelLink.onclick = this.onclickCancel.bind(this);
567       cancelLink.className = 'editor_cancel';      
568       this.form.appendChild(cancelLink);
569     }
570   },
571   hasHTMLLineBreaks: function(string) {
572     if (!this.options.handleLineBreaks) return false;
573     return string.match(/<br/i) || string.match(/<p>/i);
574   },
575   convertHTMLLineBreaks: function(string) {
576     return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
577   },
578   createEditField: function() {
579     var text;
580     if(this.options.loadTextURL) {
581       text = this.options.loadingText;
582     } else {
583       text = this.getText();
584     }
585
586     var obj = this;
587     
588     if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
589       this.options.textarea = false;
590       var textField = document.createElement("input");
591       textField.obj = this;
592       textField.type = "text";
593       textField.name = "value";
594       textField.value = text;
595       textField.style.backgroundColor = this.options.highlightcolor;
596       textField.className = 'editor_field';
597       var size = this.options.size || this.options.cols || 0;
598       if (size != 0) textField.size = size;
599       if (this.options.submitOnBlur)
600         textField.onblur = this.onSubmit.bind(this);
601       this.editField = textField;
602     } else {
603       this.options.textarea = true;
604       var textArea = document.createElement("textarea");
605       textArea.obj = this;
606       textArea.name = "value";
607       textArea.value = this.convertHTMLLineBreaks(text);
608       textArea.rows = this.options.rows;
609       textArea.cols = this.options.cols || 40;
610       textArea.className = 'editor_field';      
611       if (this.options.submitOnBlur)
612         textArea.onblur = this.onSubmit.bind(this);
613       this.editField = textArea;
614     }
615     
616     if(this.options.loadTextURL) {
617       this.loadExternalText();
618     }
619     this.form.appendChild(this.editField);
620   },
621   getText: function() {
622     return this.element.innerHTML;
623   },
624   loadExternalText: function() {
625     Element.addClassName(this.form, this.options.loadingClassName);
626     this.editField.disabled = true;
627     new Ajax.Request(
628       this.options.loadTextURL,
629       Object.extend({
630         asynchronous: true,
631         onComplete: this.onLoadedExternalText.bind(this)
632       }, this.options.ajaxOptions)
633     );
634   },
635   onLoadedExternalText: function(transport) {
636     Element.removeClassName(this.form, this.options.loadingClassName);
637     this.editField.disabled = false;
638     this.editField.value = transport.responseText.stripTags();
639   },
640   onclickCancel: function() {
641     this.onComplete();
642     this.leaveEditMode();
643     return false;
644   },
645   onFailure: function(transport) {
646     this.options.onFailure(transport);
647     if (this.oldInnerHTML) {
648       this.element.innerHTML = this.oldInnerHTML;
649       this.oldInnerHTML = null;
650     }
651     return false;
652   },
653   onSubmit: function() {
654     // onLoading resets these so we need to save them away for the Ajax call
655     var form = this.form;
656     var value = this.editField.value;
657     
658     // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
659     // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
660     // to be displayed indefinitely
661     this.onLoading();
662     
663     if (this.options.evalScripts) {
664       new Ajax.Request(
665         this.url, Object.extend({
666           parameters: this.options.callback(form, value),
667           onComplete: this.onComplete.bind(this),
668           onFailure: this.onFailure.bind(this),
669           asynchronous:true, 
670           evalScripts:true
671         }, this.options.ajaxOptions));
672     } else  {
673       new Ajax.Updater(
674         { success: this.element,
675           // don't update on failure (this could be an option)
676           failure: null }, 
677         this.url, Object.extend({
678           parameters: this.options.callback(form, value),
679           onComplete: this.onComplete.bind(this),
680           onFailure: this.onFailure.bind(this)
681         }, this.options.ajaxOptions));
682     }
683     // stop the event to avoid a page refresh in Safari
684     if (arguments.length > 1) {
685       Event.stop(arguments[0]);
686     }
687     return false;
688   },
689   onLoading: function() {
690     this.saving = true;
691     this.removeForm();
692     this.leaveHover();
693     this.showSaving();
694   },
695   showSaving: function() {
696     this.oldInnerHTML = this.element.innerHTML;
697     this.element.innerHTML = this.options.savingText;
698     Element.addClassName(this.element, this.options.savingClassName);
699     this.element.style.backgroundColor = this.originalBackground;
700     Element.show(this.element);
701   },
702   removeForm: function() {
703     if(this.form) {
704       if (this.form.parentNode) Element.remove(this.form);
705       this.form = null;
706     }
707   },
708   enterHover: function() {
709     if (this.saving) return;
710     this.element.style.backgroundColor = this.options.highlightcolor;
711     if (this.effect) {
712       this.effect.cancel();
713     }
714     Element.addClassName(this.element, this.options.hoverClassName)
715   },
716   leaveHover: function() {
717     if (this.options.backgroundColor) {
718       this.element.style.backgroundColor = this.oldBackground;
719     }
720     Element.removeClassName(this.element, this.options.hoverClassName)
721     if (this.saving) return;
722     this.effect = new Effect.Highlight(this.element, {
723       startcolor: this.options.highlightcolor,
724       endcolor: this.options.highlightendcolor,
725       restorecolor: this.originalBackground
726     });
727   },
728   leaveEditMode: function() {
729     Element.removeClassName(this.element, this.options.savingClassName);
730     this.removeForm();
731     this.leaveHover();
732     this.element.style.backgroundColor = this.originalBackground;
733     Element.show(this.element);
734     if (this.options.externalControl) {
735       Element.show(this.options.externalControl);
736     }
737     this.editing = false;
738     this.saving = false;
739     this.oldInnerHTML = null;
740     this.onLeaveEditMode();
741   },
742   onComplete: function(transport) {
743     this.leaveEditMode();
744     this.options.onComplete.bind(this)(transport, this.element);
745   },
746   onEnterEditMode: function() {},
747   onLeaveEditMode: function() {},
748   dispose: function() {
749     if (this.oldInnerHTML) {
750       this.element.innerHTML = this.oldInnerHTML;
751     }
752     this.leaveEditMode();
753     Event.stopObserving(this.element, 'click', this.onclickListener);
754     Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
755     Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
756     if (this.options.externalControl) {
757       Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
758       Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
759       Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
760     }
761   }
762 };
763
764 Ajax.InPlaceCollectionEditor = Class.create();
765 Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
766 Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
767   createEditField: function() {
768     if (!this.cached_selectTag) {
769       var selectTag = document.createElement("select");
770       var collection = this.options.collection || [];
771       var optionTag;
772       collection.each(function(e,i) {
773         optionTag = document.createElement("option");
774         optionTag.value = (e instanceof Array) ? e[0] : e;
775         if(this.options.value==optionTag.value) optionTag.selected = true;
776         optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
777         selectTag.appendChild(optionTag);
778       }.bind(this));
779       this.cached_selectTag = selectTag;
780     }
781
782     this.editField = this.cached_selectTag;
783     if(this.options.loadTextURL) this.loadExternalText();
784     this.form.appendChild(this.editField);
785     this.options.callback = function(form, value) {
786       return "value=" + encodeURIComponent(value);
787     }
788   }
789 });
790
791 // Delayed observer, like Form.Element.Observer, 
792 // but waits for delay after last key input
793 // Ideal for live-search fields
794
795 Form.Element.DelayedObserver = Class.create();
796 Form.Element.DelayedObserver.prototype = {
797   initialize: function(element, delay, callback) {
798     this.delay     = delay || 0.5;
799     this.element   = $(element);
800     this.callback  = callback;
801     this.timer     = null;
802     this.lastValue = $F(this.element); 
803     Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
804   },
805   delayedListener: function(event) {
806     if(this.lastValue == $F(this.element)) return;
807     if(this.timer) clearTimeout(this.timer);
808     this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
809     this.lastValue = $F(this.element);
810   },
811   onTimerEvent: function() {
812     this.timer = null;
813     this.callback(this.element, $F(this.element));
814   }
815 };