controls.js 27.3 KB
Newer Older
D
David Heinemeier Hansson 已提交
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 4 5 6 7
//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
D
David Heinemeier Hansson 已提交
8
// 
9
// See scriptaculous.js for full license.
D
David Heinemeier Hansson 已提交
10

11 12 13 14 15 16 17 18 19
// Autocompleter.Base handles all the autocompletion functionality 
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least, 
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method 
// should get the text for which to provide autocompletion by
20
// invoking this.getToken(), NOT by directly accessing
21 22 23 24 25 26 27 28 29 30
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
31
// a token array, e.g. { tokens: [',', '\n'] } which
32 33 34 35 36 37 38
// enables autocompletion on multiple tokens. This is most 
// useful when one of the tokens is \n (a newline), as it 
// allows smart autocompletion after linebreaks.

var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
39
  baseInitialize: function(element, update, options) {
D
David Heinemeier Hansson 已提交
40 41
    this.element     = $(element); 
    this.update      = $(update);  
42
    this.hasFocus    = false; 
D
David Heinemeier Hansson 已提交
43 44 45
    this.changed     = false; 
    this.active      = false; 
    this.index       = 0;     
46
    this.entryCount  = 0;
D
David Heinemeier Hansson 已提交
47

48 49 50
    if (this.setOptions)
      this.setOptions(options);
    else
51 52 53 54
      this.options = options || {};

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
D
David Heinemeier Hansson 已提交
55
    this.options.frequency    = this.options.frequency || 0.4;
56
    this.options.minChars     = this.options.minChars || 1;
57 58 59 60
    this.options.onShow       = this.options.onShow || 
    function(element, update){ 
      if(!update.style.position || update.style.position=='absolute') {
        update.style.position = 'absolute';
61
        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
62
      }
63
      Effect.Appear(update,{duration:0.15});
64
    };
D
David Heinemeier Hansson 已提交
65
    this.options.onHide = this.options.onHide || 
66 67 68 69
    function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if (typeof(this.options.tokens) == 'string') 
      this.options.tokens = new Array(this.options.tokens);
70

D
David Heinemeier Hansson 已提交
71 72
    this.observer = null;
    
73 74
    this.element.setAttribute('autocomplete','off');

D
David Heinemeier Hansson 已提交
75
    Element.hide(this.update);
76

D
David Heinemeier Hansson 已提交
77 78 79
    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
  },
80

D
David Heinemeier Hansson 已提交
81
  show: function() {
82
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
83 84 85 86
    if(!this.iefix && 
      (navigator.appVersion.indexOf('MSIE')>0) &&
      (navigator.userAgent.indexOf('Opera')<0) &&
      (Element.getStyle(this.update, 'position')=='absolute')) {
D
David Heinemeier Hansson 已提交
87 88
      new Insertion.After(this.update, 
       '<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>');
D
David Heinemeier Hansson 已提交
91 92
      this.iefix = $(this.update.id+'_iefix');
    }
93 94 95 96 97 98 99 100
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  },
  
  fixIEOverlapping: function() {
    Position.clone(this.update, this.iefix);
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    Element.show(this.iefix);
D
David Heinemeier Hansson 已提交
101
  },
102

D
David Heinemeier Hansson 已提交
103
  hide: function() {
104
    this.stopIndicator();
105
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
D
David Heinemeier Hansson 已提交
106 107
    if(this.iefix) Element.hide(this.iefix);
  },
108

D
David Heinemeier Hansson 已提交
109
  startIndicator: function() {
110
    if(this.options.indicator) Element.show(this.options.indicator);
D
David Heinemeier Hansson 已提交
111
  },
112

D
David Heinemeier Hansson 已提交
113
  stopIndicator: function() {
114
    if(this.options.indicator) Element.hide(this.options.indicator);
D
David Heinemeier Hansson 已提交
115 116 117 118 119 120 121
  },

  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
122
         this.selectEntry();
D
David Heinemeier Hansson 已提交
123 124 125 126
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
127
         Event.stop(event);
D
David Heinemeier Hansson 已提交
128 129 130 131 132
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
133
         this.markPrevious();
D
David Heinemeier Hansson 已提交
134 135 136 137
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
         return;
       case Event.KEY_DOWN:
138
         this.markNext();
D
David Heinemeier Hansson 已提交
139 140 141 142 143 144 145
         this.render();
         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
         return;
      }
     else 
      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
        return;
146

D
David Heinemeier Hansson 已提交
147
    this.changed = true;
148 149
    this.hasFocus = true;

D
David Heinemeier Hansson 已提交
150 151 152 153
    if(this.observer) clearTimeout(this.observer);
      this.observer = 
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },
154

155 156 157 158 159 160
  activate: function() {
    this.changed = false;
    this.hasFocus = true;
    this.getUpdatedChoices();
  },

D
David Heinemeier Hansson 已提交
161 162 163 164 165 166 167 168 169 170 171 172 173
  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex) 
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },
  
  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
174 175
    this.selectEntry();
    this.hide();
D
David Heinemeier Hansson 已提交
176 177 178 179 180
  },
  
  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
181
    this.hasFocus = false;
D
David Heinemeier Hansson 已提交
182 183 184 185
    this.active = false;     
  }, 
  
  render: function() {
186 187
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
D
David Heinemeier Hansson 已提交
188
        this.index==i ? 
189 190
          Element.addClassName(this.getEntry(i),"selected") : 
          Element.removeClassName(this.getEntry(i),"selected");
D
David Heinemeier Hansson 已提交
191
        
192
      if(this.hasFocus) { 
D
David Heinemeier Hansson 已提交
193 194 195
        this.show();
        this.active = true;
      }
T
Thomas Fuchs 已提交
196 197 198 199
    } else {
      this.active = false;
      this.hide();
    }
D
David Heinemeier Hansson 已提交
200 201
  },
  
202
  markPrevious: function() {
D
David Heinemeier Hansson 已提交
203
    if(this.index > 0) this.index--
204
      else this.index = this.entryCount-1;
D
David Heinemeier Hansson 已提交
205 206
  },
  
207 208
  markNext: function() {
    if(this.index < this.entryCount-1) this.index++
D
David Heinemeier Hansson 已提交
209 210 211
      else this.index = 0;
  },
  
212
  getEntry: function(index) {
D
David Heinemeier Hansson 已提交
213 214 215
    return this.update.firstChild.childNodes[index];
  },
  
216 217
  getCurrentEntry: function() {
    return this.getEntry(this.index);
D
David Heinemeier Hansson 已提交
218 219
  },
  
220
  selectEntry: function() {
D
David Heinemeier Hansson 已提交
221
    this.active = false;
222
    this.updateElement(this.getCurrentEntry());
223 224
  },

225 226 227 228 229
  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
230 231 232 233 234 235
    var value = '';
    if (this.options.select) {
      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
236
    
237 238 239 240
    var lastTokenPos = this.findLastToken();
    if (lastTokenPos != -1) {
      var newValue = this.element.value.substr(0, lastTokenPos + 1);
      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
241
      if (whitespace)
242 243
        newValue += whitespace[0];
      this.element.value = newValue + value;
244 245
    } else {
      this.element.value = value;
246 247 248 249 250
    }
    this.element.focus();
    
    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
251
  },
252

253
  updateChoices: function(choices) {
254
    if(!this.changed && this.hasFocus) {
255 256 257 258 259
      this.update.innerHTML = choices;
      Element.cleanWhitespace(this.update);
      Element.cleanWhitespace(this.update.firstChild);

      if(this.update.firstChild && this.update.firstChild.childNodes) {
260
        this.entryCount = 
261
          this.update.firstChild.childNodes.length;
262 263
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
264 265 266 267
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else { 
268
        this.entryCount = 0;
269
      }
270

271
      this.stopIndicator();
272

273 274 275 276 277 278 279 280 281 282 283 284
      this.index = 0;
      this.render();
    }
  },

  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onObserverEvent: function() {
    this.changed = false;   
285
    if(this.getToken().length>=this.options.minChars) {
286 287 288 289 290 291 292 293
      this.startIndicator();
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
  },

294 295 296 297
  getToken: function() {
    var tokenPos = this.findLastToken();
    if (tokenPos != -1)
      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
298 299
    else
      var ret = this.element.value;
300

301 302 303 304
    return /\n/.test(ret) ? '' : ret;
  },

  findLastToken: function() {
305
    var lastTokenPos = -1;
306 307

    for (var i=0; i<this.options.tokens.length; i++) {
308 309 310
      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
      if (thisTokenPos > lastTokenPos)
        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 323 324
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },
325

326
  getUpdatedChoices: function() {
327 328 329
    entry = encodeURIComponent(this.options.paramName) + '=' + 
      encodeURIComponent(this.getToken());

330 331
    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;
332

333 334
    if(this.options.defaultParams) 
      this.options.parameters += '&' + this.options.defaultParams;
335

336 337
    new Ajax.Request(this.url, this.options);
  },
338

339 340 341 342
  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }

343
});
344 345 346 347 348 349 350 351 352 353 354 355 356

// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
357
// - partialSearch - If false, the autocompleter will match entered
358 359 360 361 362
//                    text only at the beginning of strings in the 
//                    autocomplete array. Defaults to true, which will
//                    match text at the beginning of any *word* in the
//                    strings in the autocomplete array. If you want to
//                    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 368
// - partialChars - How many characters to enter before triggering
//                   a partial match (unlike minChars, which defines
369 370 371
//                   how many characters are required to do any match
//                   at all). Defaults to 2.
//
372
// - ignoreCase - Whether to ignore case when autocompleting.
373 374 375 376 377 378 379 380 381 382
//                 Defaults to true.
//
// It's possible to pass in a custom function as the 'selector' 
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.

Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
  initialize: function(element, update, array, options) {
383
    this.baseInitialize(element, update, options);
384 385 386 387 388 389 390 391 392 393
    this.options.array = array;
  },

  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },

  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
394 395 396 397
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
398
      selector: function(instance) {
399 400 401
        var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
402
        var count     = 0;
403

404
        for (var i = 0; i < instance.options.array.length &&  
405 406
          ret.length < instance.options.choices ; i++) { 

407
          var elem = instance.options.array[i];
408
          var foundPos = instance.options.ignoreCase ? 
409 410 411
            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
            elem.indexOf(entry);

412 413
          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) { 
414 415 416
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
                elem.substr(entry.length) + "</li>");
              break;
417 418 419 420 421 422
            } else if (entry.length >= instance.options.partialChars && 
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
423 424 425 426
                break;
              }
            }

427 428 429
            foundPos = instance.options.ignoreCase ? 
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
              elem.indexOf(entry, foundPos + 1);
430 431 432 433 434 435 436 437

          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
        return "<ul>" + ret.join('') + "</ul>";
      }
    }, options || {});
D
David Heinemeier Hansson 已提交
438
  }
439
});
440 441 442

// AJAX in-place editor
//
443
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
444

T
Thomas Fuchs 已提交
445 446 447 448 449 450 451 452 453
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
  setTimeout(function() {
    Field.activate(field);
  }, 1);
}

454 455 456 457 458 459 460 461
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
  initialize: function(element, url, options) {
    this.url = url;
    this.element = $(element);

    this.options = Object.extend({
462
      okButton: true,
463
      okText: "ok",
464
      cancelLink: true,
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
      cancelText: "cancel",
      savingText: "Saving...",
      clickToEditText: "Click to edit",
      okText: "ok",
      rows: 1,
      onComplete: function(transport, element) {
        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
      },
      onFailure: function(transport) {
        alert("Error communicating with the server: " + transport.responseText.stripTags());
      },
      callback: function(form) {
        return Form.serialize(form);
      },
      handleLineBreaks: true,
      loadingText: 'Loading...',
      savingClassName: 'inplaceeditor-saving',
482
      loadingClassName: 'inplaceeditor-loading',
483 484 485
      formClassName: 'inplaceeditor-form',
      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
      highlightendcolor: "#FFFFFF",
486
      externalControl: null,
487
      submitOnBlur: false,
488 489
      ajaxOptions: {},
      evalScripts: false
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522
    }, options || {});

    if(!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + "-inplaceeditor";
      if ($(this.options.formId)) {
        // there's already a form with that name, don't specify an id
        this.options.formId = null;
      }
    }
    
    if (this.options.externalControl) {
      this.options.externalControl = $(this.options.externalControl);
    }
    
    this.originalBackground = Element.getStyle(this.element, 'background-color');
    if (!this.originalBackground) {
      this.originalBackground = "transparent";
    }
    
    this.element.title = this.options.clickToEditText;
    
    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
    Event.observe(this.element, 'click', this.onclickListener);
    Event.observe(this.element, 'mouseover', this.mouseoverListener);
    Event.observe(this.element, 'mouseout', this.mouseoutListener);
    if (this.options.externalControl) {
      Event.observe(this.options.externalControl, 'click', this.onclickListener);
      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
    }
  },
T
Thomas Fuchs 已提交
523
  enterEditMode: function(evt) {
524 525 526 527 528 529 530 531
    if (this.saving) return;
    if (this.editing) return;
    this.editing = true;
    this.onEnterEditMode();
    if (this.options.externalControl) {
      Element.hide(this.options.externalControl);
    }
    Element.hide(this.element);
532
    this.createForm();
533
    this.element.parentNode.insertBefore(this.form, this.element);
T
Thomas Fuchs 已提交
534
    Field.scrollFreeActivate(this.editField);
535
    // stop the event to avoid a page refresh in Safari
T
Thomas Fuchs 已提交
536 537
    if (evt) {
      Event.stop(evt);
538
    }
T
Thomas Fuchs 已提交
539
    return false;
540
  },
541 542 543 544 545
  createForm: function() {
    this.form = document.createElement("form");
    this.form.id = this.options.formId;
    Element.addClassName(this.form, this.options.formClassName)
    this.form.onsubmit = this.onSubmit.bind(this);
546

547
    this.createEditField();
548 549 550

    if (this.options.textarea) {
      var br = document.createElement("br");
551
      this.form.appendChild(br);
552 553
    }

554 555 556 557
    if (this.options.okButton) {
      okButton = document.createElement("input");
      okButton.type = "submit";
      okButton.value = this.options.okText;
558
      okButton.className = 'editor_ok_button';
559 560
      this.form.appendChild(okButton);
    }
561

562 563 564 565 566
    if (this.options.cancelLink) {
      cancelLink = document.createElement("a");
      cancelLink.href = "#";
      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
      cancelLink.onclick = this.onclickCancel.bind(this);
567
      cancelLink.className = 'editor_cancel';      
568 569
      this.form.appendChild(cancelLink);
    }
570 571 572 573 574 575 576 577
  },
  hasHTMLLineBreaks: function(string) {
    if (!this.options.handleLineBreaks) return false;
    return string.match(/<br/i) || string.match(/<p>/i);
  },
  convertHTMLLineBreaks: function(string) {
    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
  },
578 579 580 581 582 583 584
  createEditField: function() {
    var text;
    if(this.options.loadTextURL) {
      text = this.options.loadingText;
    } else {
      text = this.getText();
    }
585 586

    var obj = this;
587 588
    
    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
589 590
      this.options.textarea = false;
      var textField = document.createElement("input");
591
      textField.obj = this;
592 593
      textField.type = "text";
      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 600
      if (this.options.submitOnBlur)
        textField.onblur = this.onSubmit.bind(this);
601 602 603 604
      this.editField = textField;
    } else {
      this.options.textarea = true;
      var textArea = document.createElement("textarea");
605
      textArea.obj = this;
606
      textArea.name = "value";
607
      textArea.value = this.convertHTMLLineBreaks(text);
608 609
      textArea.rows = this.options.rows;
      textArea.cols = this.options.cols || 40;
610
      textArea.className = 'editor_field';      
611 612
      if (this.options.submitOnBlur)
        textArea.onblur = this.onSubmit.bind(this);
613 614
      this.editField = textArea;
    }
615 616
    
    if(this.options.loadTextURL) {
617 618
      this.loadExternalText();
    }
619 620 621 622
    this.form.appendChild(this.editField);
  },
  getText: function() {
    return this.element.innerHTML;
623 624
  },
  loadExternalText: function() {
625 626
    Element.addClassName(this.form, this.options.loadingClassName);
    this.editField.disabled = true;
627 628
    new Ajax.Request(
      this.options.loadTextURL,
629
      Object.extend({
630 631
        asynchronous: true,
        onComplete: this.onLoadedExternalText.bind(this)
632
      }, this.options.ajaxOptions)
633 634 635
    );
  },
  onLoadedExternalText: function(transport) {
636 637 638
    Element.removeClassName(this.form, this.options.loadingClassName);
    this.editField.disabled = false;
    this.editField.value = transport.responseText.stripTags();
639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
  },
  onclickCancel: function() {
    this.onComplete();
    this.leaveEditMode();
    return false;
  },
  onFailure: function(transport) {
    this.options.onFailure(transport);
    if (this.oldInnerHTML) {
      this.element.innerHTML = this.oldInnerHTML;
      this.oldInnerHTML = null;
    }
    return false;
  },
  onSubmit: function() {
654 655 656 657 658 659 660 661 662
    // onLoading resets these so we need to save them away for the Ajax call
    var form = this.form;
    var value = this.editField.value;
    
    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
    // to be displayed indefinitely
    this.onLoading();
    
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682
    if (this.options.evalScripts) {
      new Ajax.Request(
        this.url, Object.extend({
          parameters: this.options.callback(form, value),
          onComplete: this.onComplete.bind(this),
          onFailure: this.onFailure.bind(this),
          asynchronous:true, 
          evalScripts:true
        }, this.options.ajaxOptions));
    } else  {
      new Ajax.Updater(
        { success: this.element,
          // don't update on failure (this could be an option)
          failure: null }, 
        this.url, Object.extend({
          parameters: this.options.callback(form, value),
          onComplete: this.onComplete.bind(this),
          onFailure: this.onFailure.bind(this)
        }, this.options.ajaxOptions));
    }
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
    // stop the event to avoid a page refresh in Safari
    if (arguments.length > 1) {
      Event.stop(arguments[0]);
    }
    return false;
  },
  onLoading: function() {
    this.saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  showSaving: function() {
    this.oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    Element.addClassName(this.element, this.options.savingClassName);
    this.element.style.backgroundColor = this.originalBackground;
    Element.show(this.element);
  },
  removeForm: function() {
    if(this.form) {
704
      if (this.form.parentNode) Element.remove(this.form);
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
      this.form = null;
    }
  },
  enterHover: function() {
    if (this.saving) return;
    this.element.style.backgroundColor = this.options.highlightcolor;
    if (this.effect) {
      this.effect.cancel();
    }
    Element.addClassName(this.element, this.options.hoverClassName)
  },
  leaveHover: function() {
    if (this.options.backgroundColor) {
      this.element.style.backgroundColor = this.oldBackground;
    }
    Element.removeClassName(this.element, this.options.hoverClassName)
    if (this.saving) return;
    this.effect = new Effect.Highlight(this.element, {
      startcolor: this.options.highlightcolor,
      endcolor: this.options.highlightendcolor,
      restorecolor: this.originalBackground
    });
  },
  leaveEditMode: function() {
    Element.removeClassName(this.element, this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this.originalBackground;
    Element.show(this.element);
    if (this.options.externalControl) {
      Element.show(this.options.externalControl);
    }
    this.editing = false;
    this.saving = false;
    this.oldInnerHTML = null;
    this.onLeaveEditMode();
  },
  onComplete: function(transport) {
    this.leaveEditMode();
    this.options.onComplete.bind(this)(transport, this.element);
  },
  onEnterEditMode: function() {},
  onLeaveEditMode: function() {},
  dispose: function() {
    if (this.oldInnerHTML) {
      this.element.innerHTML = this.oldInnerHTML;
    }
    this.leaveEditMode();
    Event.stopObserving(this.element, 'click', this.onclickListener);
    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
    if (this.options.externalControl) {
      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
    }
  }
762 763
};

764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790
Ajax.InPlaceCollectionEditor = Class.create();
Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
  createEditField: function() {
    if (!this.cached_selectTag) {
      var selectTag = document.createElement("select");
      var collection = this.options.collection || [];
      var optionTag;
      collection.each(function(e,i) {
        optionTag = document.createElement("option");
        optionTag.value = (e instanceof Array) ? e[0] : e;
        if(this.options.value==optionTag.value) optionTag.selected = true;
        optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
        selectTag.appendChild(optionTag);
      }.bind(this));
      this.cached_selectTag = selectTag;
    }

    this.editField = this.cached_selectTag;
    if(this.options.loadTextURL) this.loadExternalText();
    this.form.appendChild(this.editField);
    this.options.callback = function(form, value) {
      return "value=" + encodeURIComponent(value);
    }
  }
});

791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814
// Delayed observer, like Form.Element.Observer, 
// but waits for delay after last key input
// Ideal for live-search fields

Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
  initialize: function(element, delay, callback) {
    this.delay     = delay || 0.5;
    this.element   = $(element);
    this.callback  = callback;
    this.timer     = null;
    this.lastValue = $F(this.element); 
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  },
  delayedListener: function(event) {
    if(this.lastValue == $F(this.element)) return;
    if(this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
    this.lastValue = $F(this.element);
  },
  onTimerEvent: function() {
    this.timer = null;
    this.callback(this.element, $F(this.element));
  }
815
};