controls.js
上传用户:netsea168
上传日期:2022-07-22
资源大小:4652k
文件大小:34k
源码类别:

Ajax

开发平台:

Others

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