home *** CD-ROM | disk | FTP | other *** search
/ Cricao de Sites - 650 Layouts Prontos / WebMasters.iso / Blogs / wordpress2.6.exe / wordpress2.6 / wp-includes / js / scriptaculous / controls.js < prev    next >
Encoding:
Text File  |  2008-03-03  |  34.1 KB  |  966 lines

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