1// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
2
3// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
4//           (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
5//           (c) 2005-2009 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 = Class.create({
44  baseInitialize: function(element, update, options) {
45    element          = $(element);
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    this.oldElementValue = this.element.value;
54
55    if(this.setOptions)
56      this.setOptions(options);
57    else
58      this.options = options || { };
59
60    this.options.paramName    = this.options.paramName || this.element.name;
61    this.options.tokens       = this.options.tokens || [];
62    this.options.frequency    = this.options.frequency || 0.4;
63    this.options.minChars     = this.options.minChars || 1;
64    this.options.onShow       = this.options.onShow ||
65      function(element, update){
66        if(!update.style.position || update.style.position=='absolute') {
67          update.style.position = 'absolute';
68          Position.clone(element, update, {
69            setHeight: false,
70            offsetTop: element.offsetHeight
71          });
72        }
73        Effect.Appear(update,{duration:0.15});
74      };
75    this.options.onHide = this.options.onHide ||
76      function(element, update){ new Effect.Fade(update,{duration:0.15}) };
77
78    if(typeof(this.options.tokens) == 'string')
79      this.options.tokens = new Array(this.options.tokens);
80    // Force carriage returns as token delimiters anyway
81    if (!this.options.tokens.include('\n'))
82      this.options.tokens.push('\n');
83
84    this.observer = null;
85
86    this.element.setAttribute('autocomplete','off');
87
88    Element.hide(this.update);
89
90    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
91    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
92  },
93
94  show: function() {
95    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
96    if(!this.iefix &&
97      (Prototype.Browser.IE) &&
98      (Element.getStyle(this.update, 'position')=='absolute')) {
99      new Insertion.After(this.update,
100       '<iframe id="' + this.update.id + '_iefix" '+
101       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
102       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
103      this.iefix = $(this.update.id+'_iefix');
104    }
105    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
106  },
107
108  fixIEOverlapping: function() {
109    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
110    this.iefix.style.zIndex = 1;
111    this.update.style.zIndex = 2;
112    Element.show(this.iefix);
113  },
114
115  hide: function() {
116    this.stopIndicator();
117    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
118    if(this.iefix) Element.hide(this.iefix);
119  },
120
121  startIndicator: function() {
122    if(this.options.indicator) Element.show(this.options.indicator);
123  },
124
125  stopIndicator: function() {
126    if(this.options.indicator) Element.hide(this.options.indicator);
127  },
128
129  onKeyPress: function(event) {
130    if(this.active)
131      switch(event.keyCode) {
132       case Event.KEY_TAB:
133       case Event.KEY_RETURN:
134         this.selectEntry();
135         Event.stop(event);
136       case Event.KEY_ESC:
137         this.hide();
138         this.active = false;
139         Event.stop(event);
140         return;
141       case Event.KEY_LEFT:
142       case Event.KEY_RIGHT:
143         return;
144       case Event.KEY_UP:
145         this.markPrevious();
146         this.render();
147         Event.stop(event);
148         return;
149       case Event.KEY_DOWN:
150         this.markNext();
151         this.render();
152         Event.stop(event);
153         return;
154      }
155     else
156       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
157         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
158
159    this.changed = true;
160    this.hasFocus = true;
161
162    if(this.observer) clearTimeout(this.observer);
163      this.observer =
164        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
165  },
166
167  activate: function() {
168    this.changed = false;
169    this.hasFocus = true;
170    this.getUpdatedChoices();
171  },
172
173  onHover: function(event) {
174    var element = Event.findElement(event, 'LI');
175    if(this.index != element.autocompleteIndex)
176    {
177        this.index = element.autocompleteIndex;
178        this.render();
179    }
180    Event.stop(event);
181  },
182
183  onClick: function(event) {
184    var element = Event.findElement(event, 'LI');
185    this.index = element.autocompleteIndex;
186    this.selectEntry();
187    this.hide();
188  },
189
190  onBlur: function(event) {
191    // needed to make click events working
192    setTimeout(this.hide.bind(this), 250);
193    this.hasFocus = false;
194    this.active = false;
195  },
196
197  render: function() {
198    if(this.entryCount > 0) {
199      for (var i = 0; i < this.entryCount; i++)
200        this.index==i ?
201          Element.addClassName(this.getEntry(i),"selected") :
202          Element.removeClassName(this.getEntry(i),"selected");
203      if(this.hasFocus) {
204        this.show();
205        this.active = true;
206      }
207    } else {
208      this.active = false;
209      this.hide();
210    }
211  },
212
213  markPrevious: function() {
214    if(this.index > 0) this.index--;
215      else this.index = this.entryCount-1;
216    this.getEntry(this.index).scrollIntoView(true);
217  },
218
219  markNext: function() {
220    if(this.index < this.entryCount-1) this.index++;
221      else this.index = 0;
222    this.getEntry(this.index).scrollIntoView(false);
223  },
224
225  getEntry: function(index) {
226    return this.update.firstChild.childNodes[index];
227  },
228
229  getCurrentEntry: function() {
230    return this.getEntry(this.index);
231  },
232
233  selectEntry: function() {
234    this.active = false;
235    this.updateElement(this.getCurrentEntry());
236  },
237
238  updateElement: function(selectedElement) {
239    if (this.options.updateElement) {
240      this.options.updateElement(selectedElement);
241      return;
242    }
243    var value = '';
244    if (this.options.select) {
245      var nodes = $(selectedElement).select('.' + this.options.select) || [];
246      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
247    } else
248      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
249
250    var bounds = this.getTokenBounds();
251    if (bounds[0] != -1) {
252      var newValue = this.element.value.substr(0, bounds[0]);
253      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
254      if (whitespace)
255        newValue += whitespace[0];
256      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
257    } else {
258      this.element.value = value;
259    }
260    this.oldElementValue = this.element.value;
261    this.element.focus();
262
263    if (this.options.afterUpdateElement)
264      this.options.afterUpdateElement(this.element, selectedElement);
265  },
266
267  updateChoices: function(choices) {
268    if(!this.changed && this.hasFocus) {
269      this.update.innerHTML = choices;
270      Element.cleanWhitespace(this.update);
271      Element.cleanWhitespace(this.update.down());
272
273      if(this.update.firstChild && this.update.down().childNodes) {
274        this.entryCount =
275          this.update.down().childNodes.length;
276        for (var i = 0; i < this.entryCount; i++) {
277          var entry = this.getEntry(i);
278          entry.autocompleteIndex = i;
279          this.addObservers(entry);
280        }
281      } else {
282        this.entryCount = 0;
283      }
284
285      this.stopIndicator();
286      this.index = 0;
287
288      if(this.entryCount==1 && this.options.autoSelect) {
289        this.selectEntry();
290        this.hide();
291      } else {
292        this.render();
293      }
294    }
295  },
296
297  addObservers: function(element) {
298    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
299    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
300  },
301
302  onObserverEvent: function() {
303    this.changed = false;
304    this.tokenBounds = null;
305    if(this.getToken().length>=this.options.minChars) {
306      this.getUpdatedChoices();
307    } else {
308      this.active = false;
309      this.hide();
310    }
311    this.oldElementValue = this.element.value;
312  },
313
314  getToken: function() {
315    var bounds = this.getTokenBounds();
316    return this.element.value.substring(bounds[0], bounds[1]).strip();
317  },
318
319  getTokenBounds: function() {
320    if (null != this.tokenBounds) return this.tokenBounds;
321    var value = this.element.value;
322    if (value.strip().empty()) return [-1, 0];
323    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
324    var offset = (diff == this.oldElementValue.length ? 1 : 0);
325    var prevTokenPos = -1, nextTokenPos = value.length;
326    var tp;
327    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
328      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
329      if (tp > prevTokenPos) prevTokenPos = tp;
330      tp = value.indexOf(this.options.tokens[index], diff + offset);
331      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
332    }
333    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
334  }
335});
336
337Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
338  var boundary = Math.min(newS.length, oldS.length);
339  for (var index = 0; index < boundary; ++index)
340    if (newS[index] != oldS[index])
341      return index;
342  return boundary;
343};
344
345Ajax.Autocompleter = Class.create(Autocompleter.Base, {
346  initialize: function(element, update, url, options) {
347    this.baseInitialize(element, update, options);
348    this.options.asynchronous  = true;
349    this.options.onComplete    = this.onComplete.bind(this);
350    this.options.defaultParams = this.options.parameters || null;
351    this.url                   = url;
352  },
353
354  getUpdatedChoices: function() {
355    this.startIndicator();
356
357    var entry = encodeURIComponent(this.options.paramName) + '=' +
358      encodeURIComponent(this.getToken());
359
360    this.options.parameters = this.options.callback ?
361      this.options.callback(this.element, entry) : entry;
362
363    if(this.options.defaultParams)
364      this.options.parameters += '&' + this.options.defaultParams;
365
366    new Ajax.Request(this.url, this.options);
367  },
368
369  onComplete: function(request) {
370    this.updateChoices(request.responseText);
371  }
372});
373
374// The local array autocompleter. Used when you'd prefer to
375// inject an array of autocompletion options into the page, rather
376// than sending out Ajax queries, which can be quite slow sometimes.
377//
378// The constructor takes four parameters. The first two are, as usual,
379// the id of the monitored textbox, and id of the autocompletion menu.
380// The third is the array you want to autocomplete from, and the fourth
381// is the options block.
382//
383// Extra local autocompletion options:
384// - choices - How many autocompletion choices to offer
385//
386// - partialSearch - If false, the autocompleter will match entered
387//                    text only at the beginning of strings in the
388//                    autocomplete array. Defaults to true, which will
389//                    match text at the beginning of any *word* in the
390//                    strings in the autocomplete array. If you want to
391//                    search anywhere in the string, additionally set
392//                    the option fullSearch to true (default: off).
393//
394// - fullSsearch - Search anywhere in autocomplete array strings.
395//
396// - partialChars - How many characters to enter before triggering
397//                   a partial match (unlike minChars, which defines
398//                   how many characters are required to do any match
399//                   at all). Defaults to 2.
400//
401// - ignoreCase - Whether to ignore case when autocompleting.
402//                 Defaults to true.
403//
404// It's possible to pass in a custom function as the 'selector'
405// option, if you prefer to write your own autocompletion logic.
406// In that case, the other options above will not apply unless
407// you support them.
408
409Autocompleter.Local = Class.create(Autocompleter.Base, {
410  initialize: function(element, update, array, options) {
411    this.baseInitialize(element, update, options);
412    this.options.array = array;
413  },
414
415  getUpdatedChoices: function() {
416    this.updateChoices(this.options.selector(this));
417  },
418
419  setOptions: function(options) {
420    this.options = Object.extend({
421      choices: 10,
422      partialSearch: true,
423      partialChars: 2,
424      ignoreCase: true,
425      fullSearch: false,
426      selector: function(instance) {
427        var ret       = []; // Beginning matches
428        var partial   = []; // Inside matches
429        var entry     = instance.getToken();
430        var count     = 0;
431
432        for (var i = 0; i < instance.options.array.length &&
433          ret.length < instance.options.choices ; i++) {
434
435          var elem = instance.options.array[i];
436          var foundPos = instance.options.ignoreCase ?
437            elem.toLowerCase().indexOf(entry.toLowerCase()) :
438            elem.indexOf(entry);
439
440          while (foundPos != -1) {
441            if (foundPos == 0 && elem.length != entry.length) {
442              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
443                elem.substr(entry.length) + "</li>");
444              break;
445            } else if (entry.length >= instance.options.partialChars &&
446              instance.options.partialSearch && foundPos != -1) {
447              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
448                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
449                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
450                  foundPos + entry.length) + "</li>");
451                break;
452              }
453            }
454
455            foundPos = instance.options.ignoreCase ?
456              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
457              elem.indexOf(entry, foundPos + 1);
458
459          }
460        }
461        if (partial.length)
462          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
463        return "<ul>" + ret.join('') + "</ul>";
464      }
465    }, options || { });
466  }
467});
468
469// AJAX in-place editor and collection editor
470// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
471
472// Use this if you notice weird scrolling problems on some browsers,
473// the DOM might be a bit confused when this gets called so do this
474// waits 1 ms (with setTimeout) until it does the activation
475Field.scrollFreeActivate = function(field) {
476  setTimeout(function() {
477    Field.activate(field);
478  }, 1);
479};
480
481Ajax.InPlaceEditor = Class.create({
482  initialize: function(element, url, options) {
483    this.url = url;
484    this.element = element = $(element);
485    this.prepareOptions();
486    this._controls = { };
487    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
488    Object.extend(this.options, options || { });
489    if (!this.options.formId && this.element.id) {
490      this.options.formId = this.element.id + '-inplaceeditor';
491      if ($(this.options.formId))
492        this.options.formId = '';
493    }
494    if (this.options.externalControl)
495      this.options.externalControl = $(this.options.externalControl);
496    if (!this.options.externalControl)
497      this.options.externalControlOnly = false;
498    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
499    this.element.title = this.options.clickToEditText;
500    this._boundCancelHandler = this.handleFormCancellation.bind(this);
501    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
502    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
503    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
504    this._boundWrapperHandler = this.wrapUp.bind(this);
505    this.registerListeners();
506  },
507  checkForEscapeOrReturn: function(e) {
508    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
509    if (Event.KEY_ESC == e.keyCode)
510      this.handleFormCancellation(e);
511    else if (Event.KEY_RETURN == e.keyCode)
512      this.handleFormSubmission(e);
513  },
514  createControl: function(mode, handler, extraClasses) {
515    var control = this.options[mode + 'Control'];
516    var text = this.options[mode + 'Text'];
517    if ('button' == control) {
518      var btn = document.createElement('input');
519      btn.type = 'submit';
520      btn.value = text;
521      btn.className = 'editor_' + mode + '_button';
522      if ('cancel' == mode)
523        btn.onclick = this._boundCancelHandler;
524      this._form.appendChild(btn);
525      this._controls[mode] = btn;
526    } else if ('link' == control) {
527      var link = document.createElement('a');
528      link.href = '#';
529      link.appendChild(document.createTextNode(text));
530      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
531      link.className = 'editor_' + mode + '_link';
532      if (extraClasses)
533        link.className += ' ' + extraClasses;
534      this._form.appendChild(link);
535      this._controls[mode] = link;
536    }
537  },
538  createEditField: function() {
539    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
540    var fld;
541    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
542      fld = document.createElement('input');
543      fld.type = 'text';
544      var size = this.options.size || this.options.cols || 0;
545      if (0 < size) fld.size = size;
546    } else {
547      fld = document.createElement('textarea');
548      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
549      fld.cols = this.options.cols || 40;
550    }
551    fld.name = this.options.paramName;
552    fld.value = text; // No HTML breaks conversion anymore
553    fld.className = 'editor_field';
554    if (this.options.submitOnBlur)
555      fld.onblur = this._boundSubmitHandler;
556    this._controls.editor = fld;
557    if (this.options.loadTextURL)
558      this.loadExternalText();
559    this._form.appendChild(this._controls.editor);
560  },
561  createForm: function() {
562    var ipe = this;
563    function addText(mode, condition) {
564      var text = ipe.options['text' + mode + 'Controls'];
565      if (!text || condition === false) return;
566      ipe._form.appendChild(document.createTextNode(text));
567    };
568    this._form = $(document.createElement('form'));
569    this._form.id = this.options.formId;
570    this._form.addClassName(this.options.formClassName);
571    this._form.onsubmit = this._boundSubmitHandler;
572    this.createEditField();
573    if ('textarea' == this._controls.editor.tagName.toLowerCase())
574      this._form.appendChild(document.createElement('br'));
575    if (this.options.onFormCustomization)
576      this.options.onFormCustomization(this, this._form);
577    addText('Before', this.options.okControl || this.options.cancelControl);
578    this.createControl('ok', this._boundSubmitHandler);
579    addText('Between', this.options.okControl && this.options.cancelControl);
580    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
581    addText('After', this.options.okControl || this.options.cancelControl);
582  },
583  destroy: function() {
584    if (this._oldInnerHTML)
585      this.element.innerHTML = this._oldInnerHTML;
586    this.leaveEditMode();
587    this.unregisterListeners();
588  },
589  enterEditMode: function(e) {
590    if (this._saving || this._editing) return;
591    this._editing = true;
592    this.triggerCallback('onEnterEditMode');
593    if (this.options.externalControl)
594      this.options.externalControl.hide();
595    this.element.hide();
596    this.createForm();
597    this.element.parentNode.insertBefore(this._form, this.element);
598    if (!this.options.loadTextURL)
599      this.postProcessEditField();
600    if (e) Event.stop(e);
601  },
602  enterHover: function(e) {
603    if (this.options.hoverClassName)
604      this.element.addClassName(this.options.hoverClassName);
605    if (this._saving) return;
606    this.triggerCallback('onEnterHover');
607  },
608  getText: function() {
609    return this.element.innerHTML.unescapeHTML();
610  },
611  handleAJAXFailure: function(transport) {
612    this.triggerCallback('onFailure', transport);
613    if (this._oldInnerHTML) {
614      this.element.innerHTML = this._oldInnerHTML;
615      this._oldInnerHTML = null;
616    }
617  },
618  handleFormCancellation: function(e) {
619    this.wrapUp();
620    if (e) Event.stop(e);
621  },
622  handleFormSubmission: function(e) {
623    var form = this._form;
624    var value = $F(this._controls.editor);
625    this.prepareSubmission();
626    var params = this.options.callback(form, value) || '';
627    if (Object.isString(params))
628      params = params.toQueryParams();
629    params.editorId = this.element.id;
630    if (this.options.htmlResponse) {
631      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
632      Object.extend(options, {
633        parameters: params,
634        onComplete: this._boundWrapperHandler,
635        onFailure: this._boundFailureHandler
636      });
637      new Ajax.Updater({ success: this.element }, this.url, options);
638    } else {
639      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
640      Object.extend(options, {
641        parameters: params,
642        onComplete: this._boundWrapperHandler,
643        onFailure: this._boundFailureHandler
644      });
645      new Ajax.Request(this.url, options);
646    }
647    if (e) Event.stop(e);
648  },
649  leaveEditMode: function() {
650    this.element.removeClassName(this.options.savingClassName);
651    this.removeForm();
652    this.leaveHover();
653    this.element.style.backgroundColor = this._originalBackground;
654    this.element.show();
655    if (this.options.externalControl)
656      this.options.externalControl.show();
657    this._saving = false;
658    this._editing = false;
659    this._oldInnerHTML = null;
660    this.triggerCallback('onLeaveEditMode');
661  },
662  leaveHover: function(e) {
663    if (this.options.hoverClassName)
664      this.element.removeClassName(this.options.hoverClassName);
665    if (this._saving) return;
666    this.triggerCallback('onLeaveHover');
667  },
668  loadExternalText: function() {
669    this._form.addClassName(this.options.loadingClassName);
670    this._controls.editor.disabled = true;
671    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
672    Object.extend(options, {
673      parameters: 'editorId=' + encodeURIComponent(this.element.id),
674      onComplete: Prototype.emptyFunction,
675      onSuccess: function(transport) {
676        this._form.removeClassName(this.options.loadingClassName);
677        var text = transport.responseText;
678        if (this.options.stripLoadedTextTags)
679          text = text.stripTags();
680        this._controls.editor.value = text;
681        this._controls.editor.disabled = false;
682        this.postProcessEditField();
683      }.bind(this),
684      onFailure: this._boundFailureHandler
685    });
686    new Ajax.Request(this.options.loadTextURL, options);
687  },
688  postProcessEditField: function() {
689    var fpc = this.options.fieldPostCreation;
690    if (fpc)
691      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
692  },
693  prepareOptions: function() {
694    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
695    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
696    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
697      Object.extend(this.options, defs);
698    }.bind(this));
699  },
700  prepareSubmission: function() {
701    this._saving = true;
702    this.removeForm();
703    this.leaveHover();
704    this.showSaving();
705  },
706  registerListeners: function() {
707    this._listeners = { };
708    var listener;
709    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
710      listener = this[pair.value].bind(this);
711      this._listeners[pair.key] = listener;
712      if (!this.options.externalControlOnly)
713        this.element.observe(pair.key, listener);
714      if (this.options.externalControl)
715        this.options.externalControl.observe(pair.key, listener);
716    }.bind(this));
717  },
718  removeForm: function() {
719    if (!this._form) return;
720    this._form.remove();
721    this._form = null;
722    this._controls = { };
723  },
724  showSaving: function() {
725    this._oldInnerHTML = this.element.innerHTML;
726    this.element.innerHTML = this.options.savingText;
727    this.element.addClassName(this.options.savingClassName);
728    this.element.style.backgroundColor = this._originalBackground;
729    this.element.show();
730  },
731  triggerCallback: function(cbName, arg) {
732    if ('function' == typeof this.options[cbName]) {
733      this.options[cbName](this, arg);
734    }
735  },
736  unregisterListeners: function() {
737    $H(this._listeners).each(function(pair) {
738      if (!this.options.externalControlOnly)
739        this.element.stopObserving(pair.key, pair.value);
740      if (this.options.externalControl)
741        this.options.externalControl.stopObserving(pair.key, pair.value);
742    }.bind(this));
743  },
744  wrapUp: function(transport) {
745    this.leaveEditMode();
746    // Can't use triggerCallback due to backward compatibility: requires
747    // binding + direct element
748    this._boundComplete(transport, this.element);
749  }
750});
751
752Object.extend(Ajax.InPlaceEditor.prototype, {
753  dispose: Ajax.InPlaceEditor.prototype.destroy
754});
755
756Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
757  initialize: function($super, element, url, options) {
758    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
759    $super(element, url, options);
760  },
761
762  createEditField: function() {
763    var list = document.createElement('select');
764    list.name = this.options.paramName;
765    list.size = 1;
766    this._controls.editor = list;
767    this._collection = this.options.collection || [];
768    if (this.options.loadCollectionURL)
769      this.loadCollection();
770    else
771      this.checkForExternalText();
772    this._form.appendChild(this._controls.editor);
773  },
774
775  loadCollection: function() {
776    this._form.addClassName(this.options.loadingClassName);
777    this.showLoadingText(this.options.loadingCollectionText);
778    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
779    Object.extend(options, {
780      parameters: 'editorId=' + encodeURIComponent(this.element.id),
781      onComplete: Prototype.emptyFunction,
782      onSuccess: function(transport) {
783        var js = transport.responseText.strip();
784        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
785          throw('Server returned an invalid collection representation.');
786        this._collection = eval(js);
787        this.checkForExternalText();
788      }.bind(this),
789      onFailure: this.onFailure
790    });
791    new Ajax.Request(this.options.loadCollectionURL, options);
792  },
793
794  showLoadingText: function(text) {
795    this._controls.editor.disabled = true;
796    var tempOption = this._controls.editor.firstChild;
797    if (!tempOption) {
798      tempOption = document.createElement('option');
799      tempOption.value = '';
800      this._controls.editor.appendChild(tempOption);
801      tempOption.selected = true;
802    }
803    tempOption.update((text || '').stripScripts().stripTags());
804  },
805
806  checkForExternalText: function() {
807    this._text = this.getText();
808    if (this.options.loadTextURL)
809      this.loadExternalText();
810    else
811      this.buildOptionList();
812  },
813
814  loadExternalText: function() {
815    this.showLoadingText(this.options.loadingText);
816    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
817    Object.extend(options, {
818      parameters: 'editorId=' + encodeURIComponent(this.element.id),
819      onComplete: Prototype.emptyFunction,
820      onSuccess: function(transport) {
821        this._text = transport.responseText.strip();
822        this.buildOptionList();
823      }.bind(this),
824      onFailure: this.onFailure
825    });
826    new Ajax.Request(this.options.loadTextURL, options);
827  },
828
829  buildOptionList: function() {
830    this._form.removeClassName(this.options.loadingClassName);
831    this._collection = this._collection.map(function(entry) {
832      return 2 === entry.length ? entry : [entry, entry].flatten();
833    });
834    var marker = ('value' in this.options) ? this.options.value : this._text;
835    var textFound = this._collection.any(function(entry) {
836      return entry[0] == marker;
837    }.bind(this));
838    this._controls.editor.update('');
839    var option;
840    this._collection.each(function(entry, index) {
841      option = document.createElement('option');
842      option.value = entry[0];
843      option.selected = textFound ? entry[0] == marker : 0 == index;
844      option.appendChild(document.createTextNode(entry[1]));
845      this._controls.editor.appendChild(option);
846    }.bind(this));
847    this._controls.editor.disabled = false;
848    Field.scrollFreeActivate(this._controls.editor);
849  }
850});
851
852//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
853//**** This only  exists for a while,  in order to  let ****
854//**** users adapt to  the new API.  Read up on the new ****
855//**** API and convert your code to it ASAP!            ****
856
857Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
858  if (!options) return;
859  function fallback(name, expr) {
860    if (name in options || expr === undefined) return;
861    options[name] = expr;
862  };
863  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
864    options.cancelLink == options.cancelButton == false ? false : undefined)));
865  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
866    options.okLink == options.okButton == false ? false : undefined)));
867  fallback('highlightColor', options.highlightcolor);
868  fallback('highlightEndColor', options.highlightendcolor);
869};
870
871Object.extend(Ajax.InPlaceEditor, {
872  DefaultOptions: {
873    ajaxOptions: { },
874    autoRows: 3,                                // Use when multi-line w/ rows == 1
875    cancelControl: 'link',                      // 'link'|'button'|false
876    cancelText: 'cancel',
877    clickToEditText: 'Click to edit',
878    externalControl: null,                      // id|elt
879    externalControlOnly: false,
880    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
881    formClassName: 'inplaceeditor-form',
882    formId: null,                               // id|elt
883    highlightColor: '#ffff99',
884    highlightEndColor: '#ffffff',
885    hoverClassName: '',
886    htmlResponse: true,
887    loadingClassName: 'inplaceeditor-loading',
888    loadingText: 'Loading...',
889    okControl: 'button',                        // 'link'|'button'|false
890    okText: 'ok',
891    paramName: 'value',
892    rows: 1,                                    // If 1 and multi-line, uses autoRows
893    savingClassName: 'inplaceeditor-saving',
894    savingText: 'Saving...',
895    size: 0,
896    stripLoadedTextTags: false,
897    submitOnBlur: false,
898    textAfterControls: '',
899    textBeforeControls: '',
900    textBetweenControls: ''
901  },
902  DefaultCallbacks: {
903    callback: function(form) {
904      return Form.serialize(form);
905    },
906    onComplete: function(transport, element) {
907      // For backward compatibility, this one is bound to the IPE, and passes
908      // the element directly.  It was too often customized, so we don't break it.
909      new Effect.Highlight(element, {
910        startcolor: this.options.highlightColor, keepBackgroundImage: true });
911    },
912    onEnterEditMode: null,
913    onEnterHover: function(ipe) {
914      ipe.element.style.backgroundColor = ipe.options.highlightColor;
915      if (ipe._effect)
916        ipe._effect.cancel();
917    },
918    onFailure: function(transport, ipe) {
919      alert('Error communication with the server: ' + transport.responseText.stripTags());
920    },
921    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
922    onLeaveEditMode: null,
923    onLeaveHover: function(ipe) {
924      ipe._effect = new Effect.Highlight(ipe.element, {
925        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
926        restorecolor: ipe._originalBackground, keepBackgroundImage: true
927      });
928    }
929  },
930  Listeners: {
931    click: 'enterEditMode',
932    keydown: 'checkForEscapeOrReturn',
933    mouseover: 'enterHover',
934    mouseout: 'leaveHover'
935  }
936});
937
938Ajax.InPlaceCollectionEditor.DefaultOptions = {
939  loadingCollectionText: 'Loading options...'
940};
941
942// Delayed observer, like Form.Element.Observer,
943// but waits for delay after last key input
944// Ideal for live-search fields
945
946Form.Element.DelayedObserver = Class.create({
947  initialize: function(element, delay, callback) {
948    this.delay     = delay || 0.5;
949    this.element   = $(element);
950    this.callback  = callback;
951    this.timer     = null;
952    this.lastValue = $F(this.element);
953    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
954  },
955  delayedListener: function(event) {
956    if(this.lastValue == $F(this.element)) return;
957    if(this.timer) clearTimeout(this.timer);
958    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
959    this.lastValue = $F(this.element);
960  },
961  onTimerEvent: function() {
962    this.timer = null;
963    this.callback(this.element, $F(this.element));
964  }
965});