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