Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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
};