dom/tests/mochitest/ajax/scriptaculous/src/controls.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/tests/mochitest/ajax/scriptaculous/src/controls.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,875 @@
     1.4 +// script.aculo.us controls.js v1.7.1_beta2, Tue May 15 15:15:45 EDT 2007
     1.5 +
     1.6 +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
     1.7 +//           (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
     1.8 +//           (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
     1.9 +// Contributors:
    1.10 +//  Richard Livsey
    1.11 +//  Rahul Bhargava
    1.12 +//  Rob Wills
    1.13 +// 
    1.14 +// script.aculo.us is freely distributable under the terms of an MIT-style license.
    1.15 +// For details, see the script.aculo.us web site: http://script.aculo.us/
    1.16 +
    1.17 +// Autocompleter.Base handles all the autocompletion functionality 
    1.18 +// that's independent of the data source for autocompletion. This
    1.19 +// includes drawing the autocompletion menu, observing keyboard
    1.20 +// and mouse events, and similar.
    1.21 +//
    1.22 +// Specific autocompleters need to provide, at the very least, 
    1.23 +// a getUpdatedChoices function that will be invoked every time
    1.24 +// the text inside the monitored textbox changes. This method 
    1.25 +// should get the text for which to provide autocompletion by
    1.26 +// invoking this.getToken(), NOT by directly accessing
    1.27 +// this.element.value. This is to allow incremental tokenized
    1.28 +// autocompletion. Specific auto-completion logic (AJAX, etc)
    1.29 +// belongs in getUpdatedChoices.
    1.30 +//
    1.31 +// Tokenized incremental autocompletion is enabled automatically
    1.32 +// when an autocompleter is instantiated with the 'tokens' option
    1.33 +// in the options parameter, e.g.:
    1.34 +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
    1.35 +// will incrementally autocomplete with a comma as the token.
    1.36 +// Additionally, ',' in the above example can be replaced with
    1.37 +// a token array, e.g. { tokens: [',', '\n'] } which
    1.38 +// enables autocompletion on multiple tokens. This is most 
    1.39 +// useful when one of the tokens is \n (a newline), as it 
    1.40 +// allows smart autocompletion after linebreaks.
    1.41 +
    1.42 +if(typeof Effect == 'undefined')
    1.43 +  throw("controls.js requires including script.aculo.us' effects.js library");
    1.44 +
    1.45 +var Autocompleter = {}
    1.46 +Autocompleter.Base = function() {};
    1.47 +Autocompleter.Base.prototype = {
    1.48 +  baseInitialize: function(element, update, options) {
    1.49 +    element          = $(element)
    1.50 +    this.element     = element; 
    1.51 +    this.update      = $(update);  
    1.52 +    this.hasFocus    = false; 
    1.53 +    this.changed     = false; 
    1.54 +    this.active      = false; 
    1.55 +    this.index       = 0;     
    1.56 +    this.entryCount  = 0;
    1.57 +
    1.58 +    if(this.setOptions)
    1.59 +      this.setOptions(options);
    1.60 +    else
    1.61 +      this.options = options || {};
    1.62 +
    1.63 +    this.options.paramName    = this.options.paramName || this.element.name;
    1.64 +    this.options.tokens       = this.options.tokens || [];
    1.65 +    this.options.frequency    = this.options.frequency || 0.4;
    1.66 +    this.options.minChars     = this.options.minChars || 1;
    1.67 +    this.options.onShow       = this.options.onShow || 
    1.68 +      function(element, update){ 
    1.69 +        if(!update.style.position || update.style.position=='absolute') {
    1.70 +          update.style.position = 'absolute';
    1.71 +          Position.clone(element, update, {
    1.72 +            setHeight: false, 
    1.73 +            offsetTop: element.offsetHeight
    1.74 +          });
    1.75 +        }
    1.76 +        Effect.Appear(update,{duration:0.15});
    1.77 +      };
    1.78 +    this.options.onHide = this.options.onHide || 
    1.79 +      function(element, update){ new Effect.Fade(update,{duration:0.15}) };
    1.80 +
    1.81 +    if(typeof(this.options.tokens) == 'string') 
    1.82 +      this.options.tokens = new Array(this.options.tokens);
    1.83 +
    1.84 +    this.observer = null;
    1.85 +    
    1.86 +    this.element.setAttribute('autocomplete','off');
    1.87 +
    1.88 +    Element.hide(this.update);
    1.89 +
    1.90 +    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
    1.91 +    Event.observe(this.element, 'keypress', this.onKeyPress.bindAsEventListener(this));
    1.92 +
    1.93 +    // Turn autocomplete back on when the user leaves the page, so that the
    1.94 +    // field's value will be remembered on Mozilla-based browsers.
    1.95 +    Event.observe(window, 'beforeunload', function(){ 
    1.96 +      element.setAttribute('autocomplete', 'on'); 
    1.97 +    });
    1.98 +  },
    1.99 +
   1.100 +  show: function() {
   1.101 +    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
   1.102 +    if(!this.iefix && 
   1.103 +      (Prototype.Browser.IE) &&
   1.104 +      (Element.getStyle(this.update, 'position')=='absolute')) {
   1.105 +      new Insertion.After(this.update, 
   1.106 +       '<iframe id="' + this.update.id + '_iefix" '+
   1.107 +       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
   1.108 +       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
   1.109 +      this.iefix = $(this.update.id+'_iefix');
   1.110 +    }
   1.111 +    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
   1.112 +  },
   1.113 +  
   1.114 +  fixIEOverlapping: function() {
   1.115 +    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
   1.116 +    this.iefix.style.zIndex = 1;
   1.117 +    this.update.style.zIndex = 2;
   1.118 +    Element.show(this.iefix);
   1.119 +  },
   1.120 +
   1.121 +  hide: function() {
   1.122 +    this.stopIndicator();
   1.123 +    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
   1.124 +    if(this.iefix) Element.hide(this.iefix);
   1.125 +  },
   1.126 +
   1.127 +  startIndicator: function() {
   1.128 +    if(this.options.indicator) Element.show(this.options.indicator);
   1.129 +  },
   1.130 +
   1.131 +  stopIndicator: function() {
   1.132 +    if(this.options.indicator) Element.hide(this.options.indicator);
   1.133 +  },
   1.134 +
   1.135 +  onKeyPress: function(event) {
   1.136 +    if(this.active)
   1.137 +      switch(event.keyCode) {
   1.138 +       case Event.KEY_TAB:
   1.139 +       case Event.KEY_RETURN:
   1.140 +         this.selectEntry();
   1.141 +         Event.stop(event);
   1.142 +       case Event.KEY_ESC:
   1.143 +         this.hide();
   1.144 +         this.active = false;
   1.145 +         Event.stop(event);
   1.146 +         return;
   1.147 +       case Event.KEY_LEFT:
   1.148 +       case Event.KEY_RIGHT:
   1.149 +         return;
   1.150 +       case Event.KEY_UP:
   1.151 +         this.markPrevious();
   1.152 +         this.render();
   1.153 +         if(Prototype.Browser.WebKit) Event.stop(event);
   1.154 +         return;
   1.155 +       case Event.KEY_DOWN:
   1.156 +         this.markNext();
   1.157 +         this.render();
   1.158 +         if(Prototype.Browser.WebKit) Event.stop(event);
   1.159 +         return;
   1.160 +      }
   1.161 +     else 
   1.162 +       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
   1.163 +         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
   1.164 +
   1.165 +    this.changed = true;
   1.166 +    this.hasFocus = true;
   1.167 +
   1.168 +    if(this.observer) clearTimeout(this.observer);
   1.169 +      this.observer = 
   1.170 +        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
   1.171 +  },
   1.172 +
   1.173 +  activate: function() {
   1.174 +    this.changed = false;
   1.175 +    this.hasFocus = true;
   1.176 +    this.getUpdatedChoices();
   1.177 +  },
   1.178 +
   1.179 +  onHover: function(event) {
   1.180 +    var element = Event.findElement(event, 'LI');
   1.181 +    if(this.index != element.autocompleteIndex) 
   1.182 +    {
   1.183 +        this.index = element.autocompleteIndex;
   1.184 +        this.render();
   1.185 +    }
   1.186 +    Event.stop(event);
   1.187 +  },
   1.188 +  
   1.189 +  onClick: function(event) {
   1.190 +    var element = Event.findElement(event, 'LI');
   1.191 +    this.index = element.autocompleteIndex;
   1.192 +    this.selectEntry();
   1.193 +    this.hide();
   1.194 +  },
   1.195 +  
   1.196 +  onBlur: function(event) {
   1.197 +    // needed to make click events working
   1.198 +    setTimeout(this.hide.bind(this), 250);
   1.199 +    this.hasFocus = false;
   1.200 +    this.active = false;     
   1.201 +  }, 
   1.202 +  
   1.203 +  render: function() {
   1.204 +    if(this.entryCount > 0) {
   1.205 +      for (var i = 0; i < this.entryCount; i++)
   1.206 +        this.index==i ? 
   1.207 +          Element.addClassName(this.getEntry(i),"selected") : 
   1.208 +          Element.removeClassName(this.getEntry(i),"selected");
   1.209 +      if(this.hasFocus) { 
   1.210 +        this.show();
   1.211 +        this.active = true;
   1.212 +      }
   1.213 +    } else {
   1.214 +      this.active = false;
   1.215 +      this.hide();
   1.216 +    }
   1.217 +  },
   1.218 +  
   1.219 +  markPrevious: function() {
   1.220 +    if(this.index > 0) this.index--
   1.221 +      else this.index = this.entryCount-1;
   1.222 +    this.getEntry(this.index).scrollIntoView(true);
   1.223 +  },
   1.224 +  
   1.225 +  markNext: function() {
   1.226 +    if(this.index < this.entryCount-1) this.index++
   1.227 +      else this.index = 0;
   1.228 +    this.getEntry(this.index).scrollIntoView(false);
   1.229 +  },
   1.230 +  
   1.231 +  getEntry: function(index) {
   1.232 +    return this.update.firstChild.childNodes[index];
   1.233 +  },
   1.234 +  
   1.235 +  getCurrentEntry: function() {
   1.236 +    return this.getEntry(this.index);
   1.237 +  },
   1.238 +  
   1.239 +  selectEntry: function() {
   1.240 +    this.active = false;
   1.241 +    this.updateElement(this.getCurrentEntry());
   1.242 +  },
   1.243 +
   1.244 +  updateElement: function(selectedElement) {
   1.245 +    if (this.options.updateElement) {
   1.246 +      this.options.updateElement(selectedElement);
   1.247 +      return;
   1.248 +    }
   1.249 +    var value = '';
   1.250 +    if (this.options.select) {
   1.251 +      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
   1.252 +      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
   1.253 +    } else
   1.254 +      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
   1.255 +    
   1.256 +    var lastTokenPos = this.findLastToken();
   1.257 +    if (lastTokenPos != -1) {
   1.258 +      var newValue = this.element.value.substr(0, lastTokenPos + 1);
   1.259 +      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
   1.260 +      if (whitespace)
   1.261 +        newValue += whitespace[0];
   1.262 +      this.element.value = newValue + value;
   1.263 +    } else {
   1.264 +      this.element.value = value;
   1.265 +    }
   1.266 +    this.element.focus();
   1.267 +    
   1.268 +    if (this.options.afterUpdateElement)
   1.269 +      this.options.afterUpdateElement(this.element, selectedElement);
   1.270 +  },
   1.271 +
   1.272 +  updateChoices: function(choices) {
   1.273 +    if(!this.changed && this.hasFocus) {
   1.274 +      this.update.innerHTML = choices;
   1.275 +      Element.cleanWhitespace(this.update);
   1.276 +      Element.cleanWhitespace(this.update.down());
   1.277 +
   1.278 +      if(this.update.firstChild && this.update.down().childNodes) {
   1.279 +        this.entryCount = 
   1.280 +          this.update.down().childNodes.length;
   1.281 +        for (var i = 0; i < this.entryCount; i++) {
   1.282 +          var entry = this.getEntry(i);
   1.283 +          entry.autocompleteIndex = i;
   1.284 +          this.addObservers(entry);
   1.285 +        }
   1.286 +      } else { 
   1.287 +        this.entryCount = 0;
   1.288 +      }
   1.289 +
   1.290 +      this.stopIndicator();
   1.291 +      this.index = 0;
   1.292 +      
   1.293 +      if(this.entryCount==1 && this.options.autoSelect) {
   1.294 +        this.selectEntry();
   1.295 +        this.hide();
   1.296 +      } else {
   1.297 +        this.render();
   1.298 +      }
   1.299 +    }
   1.300 +  },
   1.301 +
   1.302 +  addObservers: function(element) {
   1.303 +    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
   1.304 +    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
   1.305 +  },
   1.306 +
   1.307 +  onObserverEvent: function() {
   1.308 +    this.changed = false;   
   1.309 +    if(this.getToken().length>=this.options.minChars) {
   1.310 +      this.getUpdatedChoices();
   1.311 +    } else {
   1.312 +      this.active = false;
   1.313 +      this.hide();
   1.314 +    }
   1.315 +  },
   1.316 +
   1.317 +  getToken: function() {
   1.318 +    var tokenPos = this.findLastToken();
   1.319 +    if (tokenPos != -1)
   1.320 +      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
   1.321 +    else
   1.322 +      var ret = this.element.value;
   1.323 +
   1.324 +    return /\n/.test(ret) ? '' : ret;
   1.325 +  },
   1.326 +
   1.327 +  findLastToken: function() {
   1.328 +    var lastTokenPos = -1;
   1.329 +
   1.330 +    for (var i=0; i<this.options.tokens.length; i++) {
   1.331 +      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
   1.332 +      if (thisTokenPos > lastTokenPos)
   1.333 +        lastTokenPos = thisTokenPos;
   1.334 +    }
   1.335 +    return lastTokenPos;
   1.336 +  }
   1.337 +}
   1.338 +
   1.339 +Ajax.Autocompleter = Class.create();
   1.340 +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
   1.341 +  initialize: function(element, update, url, options) {
   1.342 +    this.baseInitialize(element, update, options);
   1.343 +    this.options.asynchronous  = true;
   1.344 +    this.options.onComplete    = this.onComplete.bind(this);
   1.345 +    this.options.defaultParams = this.options.parameters || null;
   1.346 +    this.url                   = url;
   1.347 +  },
   1.348 +
   1.349 +  getUpdatedChoices: function() {
   1.350 +    this.startIndicator();
   1.351 +    
   1.352 +    var entry = encodeURIComponent(this.options.paramName) + '=' + 
   1.353 +      encodeURIComponent(this.getToken());
   1.354 +
   1.355 +    this.options.parameters = this.options.callback ?
   1.356 +      this.options.callback(this.element, entry) : entry;
   1.357 +
   1.358 +    if(this.options.defaultParams) 
   1.359 +      this.options.parameters += '&' + this.options.defaultParams;
   1.360 +    
   1.361 +    new Ajax.Request(this.url, this.options);
   1.362 +  },
   1.363 +
   1.364 +  onComplete: function(request) {
   1.365 +    this.updateChoices(request.responseText);
   1.366 +  }
   1.367 +
   1.368 +});
   1.369 +
   1.370 +// The local array autocompleter. Used when you'd prefer to
   1.371 +// inject an array of autocompletion options into the page, rather
   1.372 +// than sending out Ajax queries, which can be quite slow sometimes.
   1.373 +//
   1.374 +// The constructor takes four parameters. The first two are, as usual,
   1.375 +// the id of the monitored textbox, and id of the autocompletion menu.
   1.376 +// The third is the array you want to autocomplete from, and the fourth
   1.377 +// is the options block.
   1.378 +//
   1.379 +// Extra local autocompletion options:
   1.380 +// - choices - How many autocompletion choices to offer
   1.381 +//
   1.382 +// - partialSearch - If false, the autocompleter will match entered
   1.383 +//                    text only at the beginning of strings in the 
   1.384 +//                    autocomplete array. Defaults to true, which will
   1.385 +//                    match text at the beginning of any *word* in the
   1.386 +//                    strings in the autocomplete array. If you want to
   1.387 +//                    search anywhere in the string, additionally set
   1.388 +//                    the option fullSearch to true (default: off).
   1.389 +//
   1.390 +// - fullSsearch - Search anywhere in autocomplete array strings.
   1.391 +//
   1.392 +// - partialChars - How many characters to enter before triggering
   1.393 +//                   a partial match (unlike minChars, which defines
   1.394 +//                   how many characters are required to do any match
   1.395 +//                   at all). Defaults to 2.
   1.396 +//
   1.397 +// - ignoreCase - Whether to ignore case when autocompleting.
   1.398 +//                 Defaults to true.
   1.399 +//
   1.400 +// It's possible to pass in a custom function as the 'selector' 
   1.401 +// option, if you prefer to write your own autocompletion logic.
   1.402 +// In that case, the other options above will not apply unless
   1.403 +// you support them.
   1.404 +
   1.405 +Autocompleter.Local = Class.create();
   1.406 +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
   1.407 +  initialize: function(element, update, array, options) {
   1.408 +    this.baseInitialize(element, update, options);
   1.409 +    this.options.array = array;
   1.410 +  },
   1.411 +
   1.412 +  getUpdatedChoices: function() {
   1.413 +    this.updateChoices(this.options.selector(this));
   1.414 +  },
   1.415 +
   1.416 +  setOptions: function(options) {
   1.417 +    this.options = Object.extend({
   1.418 +      choices: 10,
   1.419 +      partialSearch: true,
   1.420 +      partialChars: 2,
   1.421 +      ignoreCase: true,
   1.422 +      fullSearch: false,
   1.423 +      selector: function(instance) {
   1.424 +        var ret       = []; // Beginning matches
   1.425 +        var partial   = []; // Inside matches
   1.426 +        var entry     = instance.getToken();
   1.427 +        var count     = 0;
   1.428 +
   1.429 +        for (var i = 0; i < instance.options.array.length &&  
   1.430 +          ret.length < instance.options.choices ; i++) { 
   1.431 +
   1.432 +          var elem = instance.options.array[i];
   1.433 +          var foundPos = instance.options.ignoreCase ? 
   1.434 +            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
   1.435 +            elem.indexOf(entry);
   1.436 +
   1.437 +          while (foundPos != -1) {
   1.438 +            if (foundPos == 0 && elem.length != entry.length) { 
   1.439 +              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
   1.440 +                elem.substr(entry.length) + "</li>");
   1.441 +              break;
   1.442 +            } else if (entry.length >= instance.options.partialChars && 
   1.443 +              instance.options.partialSearch && foundPos != -1) {
   1.444 +              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
   1.445 +                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
   1.446 +                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
   1.447 +                  foundPos + entry.length) + "</li>");
   1.448 +                break;
   1.449 +              }
   1.450 +            }
   1.451 +
   1.452 +            foundPos = instance.options.ignoreCase ? 
   1.453 +              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
   1.454 +              elem.indexOf(entry, foundPos + 1);
   1.455 +
   1.456 +          }
   1.457 +        }
   1.458 +        if (partial.length)
   1.459 +          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
   1.460 +        return "<ul>" + ret.join('') + "</ul>";
   1.461 +      }
   1.462 +    }, options || {});
   1.463 +  }
   1.464 +});
   1.465 +
   1.466 +// AJAX in-place editor
   1.467 +//
   1.468 +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
   1.469 +
   1.470 +// Use this if you notice weird scrolling problems on some browsers,
   1.471 +// the DOM might be a bit confused when this gets called so do this
   1.472 +// waits 1 ms (with setTimeout) until it does the activation
   1.473 +Field.scrollFreeActivate = function(field) {
   1.474 +  setTimeout(function() {
   1.475 +    Field.activate(field);
   1.476 +  }, 1);
   1.477 +}
   1.478 +
   1.479 +Ajax.InPlaceEditor = Class.create();
   1.480 +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
   1.481 +Ajax.InPlaceEditor.prototype = {
   1.482 +  initialize: function(element, url, options) {
   1.483 +    this.url = url;
   1.484 +    this.element = $(element);
   1.485 +
   1.486 +    this.options = Object.extend({
   1.487 +      paramName: "value",
   1.488 +      okButton: true,
   1.489 +      okLink: false,
   1.490 +      okText: "ok",
   1.491 +      cancelButton: false,
   1.492 +      cancelLink: true,
   1.493 +      cancelText: "cancel",
   1.494 +      textBeforeControls: '',
   1.495 +      textBetweenControls: '',
   1.496 +      textAfterControls: '',
   1.497 +      savingText: "Saving...",
   1.498 +      clickToEditText: "Click to edit",
   1.499 +      okText: "ok",
   1.500 +      rows: 1,
   1.501 +      onComplete: function(transport, element) {
   1.502 +        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
   1.503 +      },
   1.504 +      onFailure: function(transport) {
   1.505 +        alert("Error communicating with the server: " + transport.responseText.stripTags());
   1.506 +      },
   1.507 +      callback: function(form) {
   1.508 +        return Form.serialize(form);
   1.509 +      },
   1.510 +      handleLineBreaks: true,
   1.511 +      loadingText: 'Loading...',
   1.512 +      savingClassName: 'inplaceeditor-saving',
   1.513 +      loadingClassName: 'inplaceeditor-loading',
   1.514 +      formClassName: 'inplaceeditor-form',
   1.515 +      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
   1.516 +      highlightendcolor: "#FFFFFF",
   1.517 +      externalControl: null,
   1.518 +      submitOnBlur: false,
   1.519 +      ajaxOptions: {},
   1.520 +      evalScripts: false
   1.521 +    }, options || {});
   1.522 +
   1.523 +    if(!this.options.formId && this.element.id) {
   1.524 +      this.options.formId = this.element.id + "-inplaceeditor";
   1.525 +      if ($(this.options.formId)) {
   1.526 +        // there's already a form with that name, don't specify an id
   1.527 +        this.options.formId = null;
   1.528 +      }
   1.529 +    }
   1.530 +    
   1.531 +    if (this.options.externalControl) {
   1.532 +      this.options.externalControl = $(this.options.externalControl);
   1.533 +    }
   1.534 +    
   1.535 +    this.originalBackground = Element.getStyle(this.element, 'background-color');
   1.536 +    if (!this.originalBackground) {
   1.537 +      this.originalBackground = "transparent";
   1.538 +    }
   1.539 +    
   1.540 +    this.element.title = this.options.clickToEditText;
   1.541 +    
   1.542 +    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
   1.543 +    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
   1.544 +    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
   1.545 +    Event.observe(this.element, 'click', this.onclickListener);
   1.546 +    Event.observe(this.element, 'mouseover', this.mouseoverListener);
   1.547 +    Event.observe(this.element, 'mouseout', this.mouseoutListener);
   1.548 +    if (this.options.externalControl) {
   1.549 +      Event.observe(this.options.externalControl, 'click', this.onclickListener);
   1.550 +      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
   1.551 +      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
   1.552 +    }
   1.553 +  },
   1.554 +  enterEditMode: function(evt) {
   1.555 +    if (this.saving) return;
   1.556 +    if (this.editing) return;
   1.557 +    this.editing = true;
   1.558 +    this.onEnterEditMode();
   1.559 +    if (this.options.externalControl) {
   1.560 +      Element.hide(this.options.externalControl);
   1.561 +    }
   1.562 +    Element.hide(this.element);
   1.563 +    this.createForm();
   1.564 +    this.element.parentNode.insertBefore(this.form, this.element);
   1.565 +    if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
   1.566 +    // stop the event to avoid a page refresh in Safari
   1.567 +    if (evt) {
   1.568 +      Event.stop(evt);
   1.569 +    }
   1.570 +    return false;
   1.571 +  },
   1.572 +  createForm: function() {
   1.573 +    this.form = document.createElement("form");
   1.574 +    this.form.id = this.options.formId;
   1.575 +    Element.addClassName(this.form, this.options.formClassName)
   1.576 +    this.form.onsubmit = this.onSubmit.bind(this);
   1.577 +
   1.578 +    this.createEditField();
   1.579 +
   1.580 +    if (this.options.textarea) {
   1.581 +      var br = document.createElement("br");
   1.582 +      this.form.appendChild(br);
   1.583 +    }
   1.584 +    
   1.585 +    if (this.options.textBeforeControls)
   1.586 +      this.form.appendChild(document.createTextNode(this.options.textBeforeControls));
   1.587 +
   1.588 +    if (this.options.okButton) {
   1.589 +      var okButton = document.createElement("input");
   1.590 +      okButton.type = "submit";
   1.591 +      okButton.value = this.options.okText;
   1.592 +      okButton.className = 'editor_ok_button';
   1.593 +      this.form.appendChild(okButton);
   1.594 +    }
   1.595 +    
   1.596 +    if (this.options.okLink) {
   1.597 +      var okLink = document.createElement("a");
   1.598 +      okLink.href = "#";
   1.599 +      okLink.appendChild(document.createTextNode(this.options.okText));
   1.600 +      okLink.onclick = this.onSubmit.bind(this);
   1.601 +      okLink.className = 'editor_ok_link';
   1.602 +      this.form.appendChild(okLink);
   1.603 +    }
   1.604 +    
   1.605 +    if (this.options.textBetweenControls && 
   1.606 +      (this.options.okLink || this.options.okButton) && 
   1.607 +      (this.options.cancelLink || this.options.cancelButton))
   1.608 +      this.form.appendChild(document.createTextNode(this.options.textBetweenControls));
   1.609 +      
   1.610 +    if (this.options.cancelButton) {
   1.611 +      var cancelButton = document.createElement("input");
   1.612 +      cancelButton.type = "submit";
   1.613 +      cancelButton.value = this.options.cancelText;
   1.614 +      cancelButton.onclick = this.onclickCancel.bind(this);
   1.615 +      cancelButton.className = 'editor_cancel_button';
   1.616 +      this.form.appendChild(cancelButton);
   1.617 +    }
   1.618 +
   1.619 +    if (this.options.cancelLink) {
   1.620 +      var cancelLink = document.createElement("a");
   1.621 +      cancelLink.href = "#";
   1.622 +      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
   1.623 +      cancelLink.onclick = this.onclickCancel.bind(this);
   1.624 +      cancelLink.className = 'editor_cancel editor_cancel_link';      
   1.625 +      this.form.appendChild(cancelLink);
   1.626 +    }
   1.627 +    
   1.628 +    if (this.options.textAfterControls)
   1.629 +      this.form.appendChild(document.createTextNode(this.options.textAfterControls));
   1.630 +  },
   1.631 +  hasHTMLLineBreaks: function(string) {
   1.632 +    if (!this.options.handleLineBreaks) return false;
   1.633 +    return string.match(/<br/i) || string.match(/<p>/i);
   1.634 +  },
   1.635 +  convertHTMLLineBreaks: function(string) {
   1.636 +    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
   1.637 +  },
   1.638 +  createEditField: function() {
   1.639 +    var text;
   1.640 +    if(this.options.loadTextURL) {
   1.641 +      text = this.options.loadingText;
   1.642 +    } else {
   1.643 +      text = this.getText();
   1.644 +    }
   1.645 +
   1.646 +    var obj = this;
   1.647 +    
   1.648 +    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
   1.649 +      this.options.textarea = false;
   1.650 +      var textField = document.createElement("input");
   1.651 +      textField.obj = this;
   1.652 +      textField.type = "text";
   1.653 +      textField.name = this.options.paramName;
   1.654 +      textField.value = text;
   1.655 +      textField.style.backgroundColor = this.options.highlightcolor;
   1.656 +      textField.className = 'editor_field';
   1.657 +      var size = this.options.size || this.options.cols || 0;
   1.658 +      if (size != 0) textField.size = size;
   1.659 +      if (this.options.submitOnBlur)
   1.660 +        textField.onblur = this.onSubmit.bind(this);
   1.661 +      this.editField = textField;
   1.662 +    } else {
   1.663 +      this.options.textarea = true;
   1.664 +      var textArea = document.createElement("textarea");
   1.665 +      textArea.obj = this;
   1.666 +      textArea.name = this.options.paramName;
   1.667 +      textArea.value = this.convertHTMLLineBreaks(text);
   1.668 +      textArea.rows = this.options.rows;
   1.669 +      textArea.cols = this.options.cols || 40;
   1.670 +      textArea.className = 'editor_field';      
   1.671 +      if (this.options.submitOnBlur)
   1.672 +        textArea.onblur = this.onSubmit.bind(this);
   1.673 +      this.editField = textArea;
   1.674 +    }
   1.675 +    
   1.676 +    if(this.options.loadTextURL) {
   1.677 +      this.loadExternalText();
   1.678 +    }
   1.679 +    this.form.appendChild(this.editField);
   1.680 +  },
   1.681 +  getText: function() {
   1.682 +    return this.element.innerHTML;
   1.683 +  },
   1.684 +  loadExternalText: function() {
   1.685 +    Element.addClassName(this.form, this.options.loadingClassName);
   1.686 +    this.editField.disabled = true;
   1.687 +    new Ajax.Request(
   1.688 +      this.options.loadTextURL,
   1.689 +      Object.extend({
   1.690 +        asynchronous: true,
   1.691 +        onComplete: this.onLoadedExternalText.bind(this)
   1.692 +      }, this.options.ajaxOptions)
   1.693 +    );
   1.694 +  },
   1.695 +  onLoadedExternalText: function(transport) {
   1.696 +    Element.removeClassName(this.form, this.options.loadingClassName);
   1.697 +    this.editField.disabled = false;
   1.698 +    this.editField.value = transport.responseText.stripTags();
   1.699 +    Field.scrollFreeActivate(this.editField);
   1.700 +  },
   1.701 +  onclickCancel: function() {
   1.702 +    this.onComplete();
   1.703 +    this.leaveEditMode();
   1.704 +    return false;
   1.705 +  },
   1.706 +  onFailure: function(transport) {
   1.707 +    this.options.onFailure(transport);
   1.708 +    if (this.oldInnerHTML) {
   1.709 +      this.element.innerHTML = this.oldInnerHTML;
   1.710 +      this.oldInnerHTML = null;
   1.711 +    }
   1.712 +    return false;
   1.713 +  },
   1.714 +  onSubmit: function() {
   1.715 +    // onLoading resets these so we need to save them away for the Ajax call
   1.716 +    var form = this.form;
   1.717 +    var value = this.editField.value;
   1.718 +    
   1.719 +    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
   1.720 +    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
   1.721 +    // to be displayed indefinitely
   1.722 +    this.onLoading();
   1.723 +    
   1.724 +    if (this.options.evalScripts) {
   1.725 +      new Ajax.Request(
   1.726 +        this.url, Object.extend({
   1.727 +          parameters: this.options.callback(form, value),
   1.728 +          onComplete: this.onComplete.bind(this),
   1.729 +          onFailure: this.onFailure.bind(this),
   1.730 +          asynchronous:true, 
   1.731 +          evalScripts:true
   1.732 +        }, this.options.ajaxOptions));
   1.733 +    } else  {
   1.734 +      new Ajax.Updater(
   1.735 +        { success: this.element,
   1.736 +          // don't update on failure (this could be an option)
   1.737 +          failure: null }, 
   1.738 +        this.url, Object.extend({
   1.739 +          parameters: this.options.callback(form, value),
   1.740 +          onComplete: this.onComplete.bind(this),
   1.741 +          onFailure: this.onFailure.bind(this)
   1.742 +        }, this.options.ajaxOptions));
   1.743 +    }
   1.744 +    // stop the event to avoid a page refresh in Safari
   1.745 +    if (arguments.length > 1) {
   1.746 +      Event.stop(arguments[0]);
   1.747 +    }
   1.748 +    return false;
   1.749 +  },
   1.750 +  onLoading: function() {
   1.751 +    this.saving = true;
   1.752 +    this.removeForm();
   1.753 +    this.leaveHover();
   1.754 +    this.showSaving();
   1.755 +  },
   1.756 +  showSaving: function() {
   1.757 +    this.oldInnerHTML = this.element.innerHTML;
   1.758 +    this.element.innerHTML = this.options.savingText;
   1.759 +    Element.addClassName(this.element, this.options.savingClassName);
   1.760 +    this.element.style.backgroundColor = this.originalBackground;
   1.761 +    Element.show(this.element);
   1.762 +  },
   1.763 +  removeForm: function() {
   1.764 +    if(this.form) {
   1.765 +      if (this.form.parentNode) Element.remove(this.form);
   1.766 +      this.form = null;
   1.767 +    }
   1.768 +  },
   1.769 +  enterHover: function() {
   1.770 +    if (this.saving) return;
   1.771 +    this.element.style.backgroundColor = this.options.highlightcolor;
   1.772 +    if (this.effect) {
   1.773 +      this.effect.cancel();
   1.774 +    }
   1.775 +    Element.addClassName(this.element, this.options.hoverClassName)
   1.776 +  },
   1.777 +  leaveHover: function() {
   1.778 +    if (this.options.backgroundColor) {
   1.779 +      this.element.style.backgroundColor = this.oldBackground;
   1.780 +    }
   1.781 +    Element.removeClassName(this.element, this.options.hoverClassName)
   1.782 +    if (this.saving) return;
   1.783 +    this.effect = new Effect.Highlight(this.element, {
   1.784 +      startcolor: this.options.highlightcolor,
   1.785 +      endcolor: this.options.highlightendcolor,
   1.786 +      restorecolor: this.originalBackground
   1.787 +    });
   1.788 +  },
   1.789 +  leaveEditMode: function() {
   1.790 +    Element.removeClassName(this.element, this.options.savingClassName);
   1.791 +    this.removeForm();
   1.792 +    this.leaveHover();
   1.793 +    this.element.style.backgroundColor = this.originalBackground;
   1.794 +    Element.show(this.element);
   1.795 +    if (this.options.externalControl) {
   1.796 +      Element.show(this.options.externalControl);
   1.797 +    }
   1.798 +    this.editing = false;
   1.799 +    this.saving = false;
   1.800 +    this.oldInnerHTML = null;
   1.801 +    this.onLeaveEditMode();
   1.802 +  },
   1.803 +  onComplete: function(transport) {
   1.804 +    this.leaveEditMode();
   1.805 +    this.options.onComplete.bind(this)(transport, this.element);
   1.806 +  },
   1.807 +  onEnterEditMode: function() {},
   1.808 +  onLeaveEditMode: function() {},
   1.809 +  dispose: function() {
   1.810 +    if (this.oldInnerHTML) {
   1.811 +      this.element.innerHTML = this.oldInnerHTML;
   1.812 +    }
   1.813 +    this.leaveEditMode();
   1.814 +    Event.stopObserving(this.element, 'click', this.onclickListener);
   1.815 +    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
   1.816 +    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
   1.817 +    if (this.options.externalControl) {
   1.818 +      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
   1.819 +      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
   1.820 +      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
   1.821 +    }
   1.822 +  }
   1.823 +};
   1.824 +
   1.825 +Ajax.InPlaceCollectionEditor = Class.create();
   1.826 +Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
   1.827 +Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
   1.828 +  createEditField: function() {
   1.829 +    if (!this.cached_selectTag) {
   1.830 +      var selectTag = document.createElement("select");
   1.831 +      var collection = this.options.collection || [];
   1.832 +      var optionTag;
   1.833 +      collection.each(function(e,i) {
   1.834 +        optionTag = document.createElement("option");
   1.835 +        optionTag.value = (e instanceof Array) ? e[0] : e;
   1.836 +        if((typeof this.options.value == 'undefined') && 
   1.837 +          ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
   1.838 +        if(this.options.value==optionTag.value) optionTag.selected = true;
   1.839 +        optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
   1.840 +        selectTag.appendChild(optionTag);
   1.841 +      }.bind(this));
   1.842 +      this.cached_selectTag = selectTag;
   1.843 +    }
   1.844 +
   1.845 +    this.editField = this.cached_selectTag;
   1.846 +    if(this.options.loadTextURL) this.loadExternalText();
   1.847 +    this.form.appendChild(this.editField);
   1.848 +    this.options.callback = function(form, value) {
   1.849 +      return "value=" + encodeURIComponent(value);
   1.850 +    }
   1.851 +  }
   1.852 +});
   1.853 +
   1.854 +// Delayed observer, like Form.Element.Observer, 
   1.855 +// but waits for delay after last key input
   1.856 +// Ideal for live-search fields
   1.857 +
   1.858 +Form.Element.DelayedObserver = Class.create();
   1.859 +Form.Element.DelayedObserver.prototype = {
   1.860 +  initialize: function(element, delay, callback) {
   1.861 +    this.delay     = delay || 0.5;
   1.862 +    this.element   = $(element);
   1.863 +    this.callback  = callback;
   1.864 +    this.timer     = null;
   1.865 +    this.lastValue = $F(this.element); 
   1.866 +    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
   1.867 +  },
   1.868 +  delayedListener: function(event) {
   1.869 +    if(this.lastValue == $F(this.element)) return;
   1.870 +    if(this.timer) clearTimeout(this.timer);
   1.871 +    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
   1.872 +    this.lastValue = $F(this.element);
   1.873 +  },
   1.874 +  onTimerEvent: function() {
   1.875 +    this.timer = null;
   1.876 +    this.callback(this.element, $F(this.element));
   1.877 +  }
   1.878 +};

mercurial