Changeset 1679

Show
Ignore:
Timestamp:
03/25/08 20:45:38 (2 months ago)
Author:
neuro
Message:

Fixes a bug breaking the spellchecker in FCKEditor.
Adds autocompletion in the tags (ticket 1217)

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/app/controllers/admin/content_controller.rb

    r1665 r1679  
    22module Admin; end 
    33class Admin::ContentController < Admin::BaseController 
     4   
     5  def auto_complete_for_article_keywords 
     6    @items = Tag.find_with_char params[:article][:keywords].strip 
     7    render :inline => "<%= auto_complete_result @items, 'name' %>" 
     8  end 
     9   
    410  def index 
    511    list 
  • trunk/app/models/tag.rb

    r1677 r1679  
    5454    'tag' 
    5555  end 
    56  
     56    
     57  # Return all tags with the char or string  
     58  # send by parameter 
     59  def self.find_with_char(char) 
     60    find :all, :conditions => ['name LIKE ? ', "%#{char}%"], :order => 'name ASC' 
     61  end 
     62   
    5763  def published_articles 
    5864    articles.find_already_published 
     
    8490    permalink 
    8591  end 
     92   
    8693end 
  • trunk/app/views/admin/content/_form.html.erb

    r1677 r1679  
    11<%= error_messages_for 'article' %> 
    22<!--[form:articles]--> 
     3<div id="article_keywords_auto_complete" class="auto_complete"></div> 
    34<fieldset class="set admin" style="display: block"> 
    45  <legend><%= _("Write post") %></legend> 
     
    3233       <li class="paginate l"> 
    3334          <label class="block" for="article_keywords"> 
    34           <%= link_to_function _("Tags") + " (+/-)",update_page { |page| page.visual_effect(:toggle_blind, "tags", :duration => 0.2) } %> 
     35          <%= link_to_function _("Tags") + " (+/-)", update_page { |page| page.visual_effect(:toggle_blind, "tags", :duration => 0.2); page.toggle(:tags) } %> 
    3536          </label> 
    3637          <p id="tags" <%= "style='display: none;'" if @article.keywords.blank? %>> 
    37             <%= text_field 'article', 'keywords', :style => 'width: 90%;' %> 
     38            <%= text_field 'article', 'keywords', {:autocomplete => 'off', :style => 'width: 90%;'} %> 
     39            <%= auto_complete_field 'article_keywords', { :url => { :action => "auto_complete_for_article_keywords"}, :tokens => ','}%> 
    3840          </p> 
    3941      </li> 
     
    9294</fieldset> 
    9395 
     96 
    9497<!--[eoform:articles]--> 
  • trunk/public/javascripts/controls.js

    r1307 r1679  
    1 // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 
    2 //           (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan) 
    3 //           (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com) 
     1// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 
     2//           (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) 
     3//           (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) 
    44// Contributors: 
    55//  Richard Livsey 
     
    3838  throw("controls.js requires including script.aculo.us' effects.js library"); 
    3939 
    40 var Autocompleter = {} 
    41 Autocompleter.Base = function() {}; 
    42 Autocompleter.Base.prototype = { 
     40var Autocompleter = { } 
     41Autocompleter.Base = Class.create({ 
    4342  baseInitialize: function(element, update, options) { 
    44     this.element     = $(element);  
     43    element          = $(element) 
     44    this.element     = element;  
    4545    this.update      = $(update);   
    4646    this.hasFocus    = false;  
     
    4949    this.index       = 0;      
    5050    this.entryCount  = 0; 
     51    this.oldElementValue = this.element.value; 
    5152 
    5253    if(this.setOptions) 
    5354      this.setOptions(options); 
    5455    else 
    55       this.options = options || {}; 
     56      this.options = options || { }; 
    5657 
    5758    this.options.paramName    = this.options.paramName || this.element.name; 
     
    7576    if(typeof(this.options.tokens) == 'string')  
    7677      this.options.tokens = new Array(this.options.tokens); 
     78    // Force carriage returns as token delimiters anyway 
     79    if (!this.options.tokens.include('\n')) 
     80      this.options.tokens.push('\n'); 
    7781 
    7882    this.observer = null; 
     
    8286    Element.hide(this.update); 
    8387 
    84     Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); 
    85     Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); 
     88    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); 
     89    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); 
    8690  }, 
    8791 
     
    8993    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); 
    9094    if(!this.iefix &&  
    91       (navigator.appVersion.indexOf('MSIE')>0) && 
    92       (navigator.userAgent.indexOf('Opera')<0) && 
     95      (Prototype.Browser.IE) && 
    9396      (Element.getStyle(this.update, 'position')=='absolute')) { 
    9497      new Insertion.After(this.update,  
     
    140143         this.markPrevious(); 
    141144         this.render(); 
    142          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); 
     145         Event.stop(event); 
    143146         return; 
    144147       case Event.KEY_DOWN: 
    145148         this.markNext(); 
    146149         this.render(); 
    147          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); 
     150         Event.stop(event); 
    148151         return; 
    149152      } 
    150153     else  
    151154       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||  
    152          (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; 
     155         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; 
    153156 
    154157    this.changed = true; 
     
    196199          Element.addClassName(this.getEntry(i),"selected") :  
    197200          Element.removeClassName(this.getEntry(i),"selected"); 
    198          
    199201      if(this.hasFocus) {  
    200202        this.show(); 
     
    239241    var value = ''; 
    240242    if (this.options.select) { 
    241       var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; 
     243      var nodes = $(selectedElement).select('.' + this.options.select) || []; 
    242244      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); 
    243245    } else 
    244246      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); 
    245247     
    246     var lastTokenPos = this.findLastToken(); 
    247     if (lastTokenPos != -1) { 
    248       var newValue = this.element.value.substr(0, lastTokenPos + 1); 
    249       var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); 
     248    var bounds = this.getTokenBounds(); 
     249    if (bounds[0] != -1) { 
     250      var newValue = this.element.value.substr(0, bounds[0]); 
     251      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); 
    250252      if (whitespace) 
    251253        newValue += whitespace[0]; 
    252       this.element.value = newValue + value
     254      this.element.value = newValue + value + this.element.value.substr(bounds[1])
    253255    } else { 
    254256      this.element.value = value; 
    255257    } 
     258    this.oldElementValue = this.element.value; 
    256259    this.element.focus(); 
    257260     
     
    297300  onObserverEvent: function() { 
    298301    this.changed = false;    
     302    this.tokenBounds = null; 
    299303    if(this.getToken().length>=this.options.minChars) { 
    300       this.startIndicator(); 
    301304      this.getUpdatedChoices(); 
    302305    } else { 
     
    304307      this.hide(); 
    305308    } 
     309    this.oldElementValue = this.element.value; 
    306310  }, 
    307311 
    308312  getToken: function() { 
    309     var tokenPos = this.findLastToken(); 
    310     if (tokenPos != -1) 
    311       var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); 
    312     else 
    313       var ret = this.element.value; 
    314  
    315     return /\n/.test(ret) ? '' : ret; 
    316   }, 
    317  
    318   findLastToken: function() { 
    319     var lastTokenPos = -1; 
    320  
    321     for (var i=0; i<this.options.tokens.length; i++) { 
    322       var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]); 
    323       if (thisTokenPos > lastTokenPos) 
    324         lastTokenPos = thisTokenPos; 
    325     } 
    326     return lastTokenPos; 
     313    var bounds = this.getTokenBounds(); 
     314    return this.element.value.substring(bounds[0], bounds[1]).strip(); 
     315  }, 
     316 
     317  getTokenBounds: function() { 
     318    if (null != this.tokenBounds) return this.tokenBounds; 
     319    var value = this.element.value; 
     320    if (value.strip().empty()) return [-1, 0]; 
     321    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); 
     322    var offset = (diff == this.oldElementValue.length ? 1 : 0); 
     323    var prevTokenPos = -1, nextTokenPos = value.length; 
     324    var tp; 
     325    for (var index = 0, l = this.options.tokens.length; index < l; ++index) { 
     326      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); 
     327      if (tp > prevTokenPos) prevTokenPos = tp; 
     328      tp = value.indexOf(this.options.tokens[index], diff + offset); 
     329      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; 
     330    } 
     331    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); 
    327332  } 
    328 
    329  
    330 Ajax.Autocompleter = Class.create(); 
    331 Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { 
     333}); 
     334 
     335Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { 
     336  var boundary = Math.min(newS.length, oldS.length); 
     337  for (var index = 0; index < boundary; ++index) 
     338    if (newS[index] != oldS[index]) 
     339      return index; 
     340  return boundary; 
     341}; 
     342 
     343Ajax.Autocompleter = Class.create(Autocompleter.Base, { 
    332344  initialize: function(element, update, url, options) { 
    333345    this.baseInitialize(element, update, options); 
     
    339351 
    340352  getUpdatedChoices: function() { 
    341     entry = encodeURIComponent(this.options.paramName) + '=' +  
     353    this.startIndicator(); 
     354     
     355    var entry = encodeURIComponent(this.options.paramName) + '=' +  
    342356      encodeURIComponent(this.getToken()); 
    343357 
     
    347361    if(this.options.defaultParams)  
    348362      this.options.parameters += '&' + this.options.defaultParams; 
    349  
     363     
    350364    new Ajax.Request(this.url, this.options); 
    351365  }, 
     
    354368    this.updateChoices(request.responseText); 
    355369  } 
    356  
    357370}); 
    358371 
     
    392405// you support them. 
    393406 
    394 Autocompleter.Local = Class.create(); 
    395 Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { 
     407Autocompleter.Local = Class.create(Autocompleter.Base, { 
    396408  initialize: function(element, update, array, options) { 
    397409    this.baseInitialize(element, update, options); 
     
    449461        return "<ul>" + ret.join('') + "</ul>"; 
    450462      } 
    451     }, options || {}); 
     463    }, options || { }); 
    452464  } 
    453465}); 
    454466 
    455 // AJAX in-place editor 
    456 // 
    457 // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor 
     467// AJAX in-place editor and collection editor 
     468// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007). 
    458469 
    459470// Use this if you notice weird scrolling problems on some browsers, 
     
    466477} 
    467478 
    468 Ajax.InPlaceEditor = Class.create(); 
    469 Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; 
    470 Ajax.InPlaceEditor.prototype = { 
     479Ajax.InPlaceEditor = Class.create({ 
    471480  initialize: function(element, url, options) { 
    472481    this.url = url; 
    473     this.element = $(element); 
    474  
    475     this.options = Object.extend({ 
    476       paramName: "value", 
    477       okButton: true, 
    478       okText: "ok", 
    479       cancelLink: true, 
    480       cancelText: "cancel", 
    481       savingText: "Saving...", 
    482       clickToEditText: "Click to edit", 
    483       okText: "ok", 
    484       rows: 1, 
    485       onComplete: function(transport, element) { 
    486         new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); 
    487       }, 
    488       onFailure: function(transport) { 
    489         alert("Error communicating with the server: " + transport.responseText.stripTags()); 
    490       }, 
    491       callback: function(form) { 
    492         return Form.serialize(form); 
    493       }, 
    494       handleLineBreaks: true, 
    495       loadingText: 'Loading...', 
    496       savingClassName: 'inplaceeditor-saving', 
    497       loadingClassName: 'inplaceeditor-loading', 
    498       formClassName: 'inplaceeditor-form', 
    499       highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, 
    500       highlightendcolor: "#FFFFFF", 
    501       externalControl: null, 
    502       submitOnBlur: false, 
    503       ajaxOptions: {}, 
    504       evalScripts: false 
    505     }, options || {}); 
    506  
    507     if(!this.options.formId && this.element.id) { 
    508       this.options.formId = this.element.id + "-inplaceeditor"; 
    509       if ($(this.options.formId)) { 
    510         // there's already a form with that name, don't specify an id 
    511         this.options.formId = null; 
    512       } 
    513     } 
    514      
    515     if (this.options.externalControl) { 
     482    this.element = element = $(element); 
     483    this.prepareOptions(); 
     484    this._controls = { }; 
     485    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! 
     486    Object.extend(this.options, options || { }); 
     487    if (!this.options.formId && this.element.id) { 
     488      this.options.formId = this.element.id + '-inplaceeditor'; 
     489      if ($(this.options.formId)) 
     490        this.options.formId = ''; 
     491    } 
     492    if (this.options.externalControl) 
    516493      this.options.externalControl = $(this.options.externalControl); 
    517     } 
    518      
    519     this.originalBackground = Element.getStyle(this.element, 'background-color'); 
    520     if (!this.originalBackground) { 
    521       this.originalBackground = "transparent"; 
    522     } 
    523      
     494    if (!this.options.externalControl) 
     495      this.options.externalControlOnly = false; 
     496    this._originalBackground = this.element.getStyle('background-color') || 'transparent'; 
    524497    this.element.title = this.options.clickToEditText; 
    525      
    526     this.onclickListener = this.enterEditMode.bindAsEventListener(this); 
    527     this.mouseoverListener = this.enterHover.bindAsEventListener(this); 
    528     this.mouseoutListener = this.leaveHover.bindAsEventListener(this); 
    529     Event.observe(this.element, 'click', this.onclickListener); 
    530     Event.observe(this.element, 'mouseover', this.mouseoverListener); 
    531     Event.observe(this.element, 'mouseout', this.mouseoutListener); 
    532     if (this.options.externalControl) { 
    533       Event.observe(this.options.externalControl, 'click', this.onclickListener); 
    534       Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); 
    535       Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); 
    536     } 
    537   }, 
    538   enterEditMode: function(evt) { 
    539     if (this.saving) return; 
    540     if (this.editing) return; 
    541     this.editing = true; 
    542     this.onEnterEditMode(); 
    543     if (this.options.externalControl) { 
    544       Element.hide(this.options.externalControl); 
    545     } 
    546     Element.hide(this.element); 
     498    this._boundCancelHandler = this.handleFormCancellation.bind(this); 
     499    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); 
     500    this._boundFailureHandler = this.handleAJAXFailure.bind(this); 
     501    this._boundSubmitHandler = this.handleFormSubmission.bind(this); 
     502    this._boundWrapperHandler = this.wrapUp.bind(this); 
     503    this.registerListeners(); 
     504  }, 
     505  checkForEscapeOrReturn: function(e) { 
     506    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; 
     507    if (Event.KEY_ESC == e.keyCode) 
     508      this.handleFormCancellation(e); 
     509    else if (Event.KEY_RETURN == e.keyCode) 
     510      this.handleFormSubmission(e); 
     511  }, 
     512  createControl: function(mode, handler, extraClasses) { 
     513    var control = this.options[mode + 'Control']; 
     514    var text = this.options[mode + 'Text']; 
     515    if ('button' == control) { 
     516      var btn = document.createElement('input'); 
     517      btn.type = 'submit'; 
     518      btn.value = text; 
     519      btn.className = 'editor_' + mode + '_button'; 
     520      if ('cancel' == mode) 
     521        btn.onclick = this._boundCancelHandler; 
     522      this._form.appendChild(btn); 
     523      this._controls[mode] = btn; 
     524    } else if ('link' == control) { 
     525      var link = document.createElement('a'); 
     526      link.href = '#'; 
     527      link.appendChild(document.createTextNode(text)); 
     528      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; 
     529      link.className = 'editor_' + mode + '_link'; 
     530      if (extraClasses) 
     531        link.className += ' ' + extraClasses; 
     532      this._form.appendChild(link); 
     533      this._controls[mode] = link; 
     534    } 
     535  }, 
     536  createEditField: function() { 
     537    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); 
     538    var fld; 
     539    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { 
     540      fld = document.createElement('input'); 
     541      fld.type = 'text'; 
     542      var size = this.options.size || this.options.cols || 0; 
     543      if (0 < size) fld.size = size; 
     544    } else { 
     545      fld = document.createElement('textarea'); 
     546      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); 
     547      fld.cols = this.options.cols || 40; 
     548    } 
     549    fld.name = this.options.paramName; 
     550    fld.value = text; // No HTML breaks conversion anymore 
     551    fld.className = 'editor_field'; 
     552    if (this.options.submitOnBlur) 
     553      fld.onblur = this._boundSubmitHandler; 
     554    this._controls.editor = fld; 
     555    if (this.options.loadTextURL) 
     556      this.loadExternalText(); 
     557    this._form.appendChild(this._controls.editor); 
     558  }, 
     559  createForm: function() { 
     560    var ipe = this; 
     561    function addText(mode, condition) { 
     562      var text = ipe.options['text' + mode + 'Controls']; 
     563      if (!text || condition === false) return; 
     564      ipe._form.appendChild(document.createTextNode(text)); 
     565    }; 
     566    this._form = $(document.createElement('form')); 
     567    this._form.id = this.options.formId; 
     568    this._form.addClassName(this.options.formClassName); 
     569    this._form.onsubmit = this._boundSubmitHandler; 
     570    this.createEditField(); 
     571    if ('textarea' == this._controls.editor.tagName.toLowerCase()) 
     572      this._form.appendChild(document.createElement('br')); 
     573    if (this.options.onFormCustomization) 
     574      this.options.onFormCustomization(this, this._form); 
     575    addText('Before', this.options.okControl || this.options.cancelControl); 
     576    this.createControl('ok', this._boundSubmitHandler); 
     577    addText('Between', this.options.okControl && this.options.cancelControl); 
     578    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); 
     579    addText('After', this.options.okControl || this.options.cancelControl); 
     580  }, 
     581  destroy: function() { 
     582    if (this._oldInnerHTML) 
     583      this.element.innerHTML = this._oldInnerHTML; 
     584    this.leaveEditMode(); 
     585    this.unregisterListeners(); 
     586  }, 
     587  enterEditMode: function(e) { 
     588    if (this._saving || this._editing) return; 
     589    this._editing = true; 
     590    this.triggerCallback('onEnterEditMode'); 
     591    if (this.options.externalControl) 
     592      this.options.externalControl.hide(); 
     593    this.element.hide(); 
    547594    this.createForm(); 
    548     this.element.parentNode.insertBefore(this.form, this.element); 
    549     if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField); 
    550     // stop the event to avoid a page refresh in Safari 
    551     if (evt) { 
    552       Event.stop(evt); 
    553     } 
    554     return false; 
    555   }, 
    556   createForm: function() { 
    557     this.form = document.createElement("form"); 
    558     this.form.id = this.options.formId; 
    559     Element.addClassName(this.form, this.options.formClassName) 
    560     this.form.onsubmit = this.onSubmit.bind(this); 
    561  
    562     this.createEditField(); 
    563  
    564     if (this.options.textarea) { 
    565       var br = document.createElement("br"); 
    566       this.form.appendChild(br); 
    567     } 
    568  
    569     if (this.options.okButton) { 
    570       okButton = document.createElement("input"); 
    571       okButton.type = "submit"; 
    572       okButton.value = this.options.okText; 
    573       okButton.className = 'editor_ok_button'; 
    574       this.form.appendChild(okButton); 
    575     } 
    576  
    577     if (this.options.cancelLink) { 
    578       cancelLink = document.createElement("a"); 
    579       cancelLink.href = "#"; 
    580       cancelLink.appendChild(document.createTextNode(this.options.cancelText)); 
    581       cancelLink.onclick = this.onclickCancel.bind(this); 
    582       cancelLink.className = 'editor_cancel';       
    583       this.form.appendChild(cancelLink); 
    584     } 
    585   }, 
    586   hasHTMLLineBreaks: function(string) { 
    587     if (!this.options.handleLineBreaks) return false; 
    588     return string.match(/<br/i) || string.match(/<p>/i); 
    589   }, 
    590   convertHTMLLineBreaks: function(string) { 
    591     return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, ""); 
    592   }, 
    593   createEditField: function() { 
    594     var text; 
    595     if(this.options.loadTextURL) { 
    596       text = this.options.loadingText; 
    597     } else { 
    598       text = this.getText(); 
    599     } 
    600  
    601     var obj = this; 
    602      
    603     if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { 
    604       this.options.textarea = false; 
    605       var textField = document.createElement("input"); 
    606       textField.obj = this; 
    607       textField.type = "text"; 
    608       textField.name = this.options.paramName; 
    609       textField.value = text; 
    610       textField.style.backgroundColor = this.options.highlightcolor; 
    611       textField.className = 'editor_field'; 
    612       var size = this.options.size || this.options.cols || 0; 
    613       if (size != 0) textField.size = size; 
    614       if (this.options.submitOnBlur) 
    615         textField.onblur = this.onSubmit.bind(this); 
    616       this.editField = textField; 
    617     } else { 
    618       this.options.textarea = true; 
    619       var textArea = document.createElement("textarea"); 
    620       textArea.obj = this; 
    621       textArea.name = this.options.paramName; 
    622       textArea.value = this.convertHTMLLineBreaks(text); 
    623       textArea.rows = this.options.rows; 
    624       textArea.cols = this.options.cols || 40; 
    625       textArea.className = 'editor_field';       
    626       if (this.options.submitOnBlur) 
    627         textArea.onblur = this.onSubmit.bind(this); 
    628       this.editField = textArea; 
    629     } 
    630      
    631     if(this.options.loadTextURL) { 
    632       this.loadExternalText(); 
    633     } 
    634     this.form.appendChild(this.editField); 
     595    this.element.parentNode.insertBefore(this._form, this.element); 
     596    if (!this.options.loadTextURL) 
     597      this.postProcessEditField(); 
     598    if (e) Event.stop(e); 
     599  }, 
     600  enterHover: function(e) { 
     601    if (this.options.hoverClassName) 
     602      this.element.addClassName(this.options.hoverClassName); 
     603    if (this._saving) return; 
     604    this.triggerCallback('onEnterHover'); 
    635605  }, 
    636606  getText: function() { 
    637607    return this.element.innerHTML; 
    638608  }, 
     609  handleAJAXFailure: function(transport) { 
     610    this.triggerCallback('onFailure', transport); 
     611    if (this._oldInnerHTML) { 
     612      this.element.innerHTML = this._oldInnerHTML; 
     613      this._oldInnerHTML = null; 
     614    } 
     615  }, 
     616  handleFormCancellation: function(e) { 
     617    this.wrapUp(); 
     618    if (e) Event.stop(e); 
     619  }, 
     620  handleFormSubmission: function(e) { 
     621    var form = this._form; 
     622    var value = $F(this._controls.editor); 
     623    this.prepareSubmission(); 
     624    var params = this.options.callback(form, value) || ''; 
     625    if (Object.isString(params)) 
     626      params = params.toQueryParams(); 
     627    params.editorId = this.element.id; 
     628    if (this.options.htmlResponse) { 
     629      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); 
     630      Object.extend(options, { 
     631        parameters: params, 
     632        onComplete: this._boundWrapperHandler, 
     633        onFailure: this._boundFailureHandler 
     634      }); 
     635      new Ajax.Updater({ success: this.element }, this.url, options); 
     636    } else { 
     637      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 
     638      Object.extend(options, { 
     639        parameters: params, 
     640        onComplete: this._boundWrapperHandler, 
     641        onFailure: this._boundFailureHandler 
     642      }); 
     643      new Ajax.Request(this.url, options); 
     644    } 
     645    if (e) Event.stop(e); 
     646  }, 
     647  leaveEditMode: function() { 
     648    this.element.removeClassName(this.options.savingClassName); 
     649    this.removeForm(); 
     650    this.leaveHover(); 
     651    this.element.style.backgroundColor = this._originalBackground; 
     652    this.element.show(); 
     653    if (this.options.externalControl) 
     654      this.options.externalControl.show(); 
     655    this._saving = false; 
     656    this._editing = false; 
     657    this._oldInnerHTML = null; 
     658    this.triggerCallback('onLeaveEditMode'); 
     659  }, 
     660  leaveHover: function(e) { 
     661    if (this.options.hoverClassName) 
     662      this.element.removeClassName(this.options.hoverClassName); 
     663    if (this._saving) return; 
     664    this.triggerCallback('onLeaveHover'); 
     665  }, 
    639666  loadExternalText: function() { 
    640     Element.addClassName(this.form, this.options.loadingClassName); 
    641     this.editField.disabled = true; 
    642     new Ajax.Request( 
    643       this.options.loadTextURL, 
    644       Object.extend({ 
    645         asynchronous: true, 
    646         onComplete: this.onLoadedExternalText.bind(this) 
    647       }, this.options.ajaxOptions) 
    648     ); 
    649   }, 
    650   onLoadedExternalText: function(transport) { 
    651     Element.removeClassName(this.form, this.options.loadingClassName); 
    652     this.editField.disabled = false; 
    653     this.editField.value = transport.responseText.stripTags(); 
    654     Field.scrollFreeActivate(this.editField); 
    655   }, 
    656   onclickCancel: function() { 
    657     this.onComplete(); 
    658     this.leaveEditMode(); 
    659     return false; 
    660   }, 
    661   onFailure: function(transport) { 
    662     this.options.onFailure(transport); 
    663     if (this.oldInnerHTML) { 
    664       this.element.innerHTML = this.oldInnerHTML; 
    665       this.oldInnerHTML = null; 
    666     } 
    667     return false; 
    668   }, 
    669   onSubmit: function() { 
    670     // onLoading resets these so we need to save them away for the Ajax call 
    671     var form = this.form; 
    672     var value = this.editField.value; 
    673      
    674     // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... 
    675     // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... 
    676     // to be displayed indefinitely 
    677     this.onLoading(); 
    678      
    679     if (this.options.evalScripts) { 
    680       new Ajax.Request( 
    681         this.url, Object.extend({ 
    682           parameters: this.options.callback(form, value), 
    683           onComplete: this.onComplete.bind(this), 
    684           onFailure: this.onFailure.bind(this), 
    685           asynchronous:true,  
    686           evalScripts:true 
    687         }, this.options.ajaxOptions)); 
    688     } else  { 
    689       new Ajax.Updater( 
    690         { success: this.element, 
    691           // don't update on failure (this could be an option) 
    692           failure: null },  
    693         this.url, Object.extend({ 
    694           parameters: this.options.callback(form, value), 
    695           onComplete: this.onComplete.bind(this), 
    696           onFailure: this.onFailure.bind(this) 
    697         }, this.options.ajaxOptions)); 
    698     } 
    699     // stop the event to avoid a page refresh in Safari 
    700     if (arguments.length > 1) { 
    701       Event.stop(arguments[0]); 
    702     } 
    703     return false; 
    704   }, 
    705   onLoading: function() { 
    706     this.saving = true; 
     667    this._form.addClassName(this.options.loadingClassName); 
     668    this._controls.editor.disabled = true; 
     669    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 
     670    Object.extend(options, { 
     671      parameters: 'editorId=' + encodeURIComponent(this.element.id), 
     672      onComplete: Prototype.emptyFunction, 
     673      onSuccess: function(transport) { 
     674        this._form.removeClassName(this.options.loadingClassName); 
     675        var text = transport.responseText; 
     676        if (this.options.stripLoadedTextTags) 
     677          text = text.stripTags(); 
     678        this._controls.editor.value = text; 
     679        this._controls.editor.disabled = false; 
     680        this.postProcessEditField(); 
     681      }.bind(this), 
     682      onFailure: this._boundFailureHandler 
     683    }); 
     684    new Ajax.Request(this.options.loadTextURL, options); 
     685  }, 
     686  postProcessEditField: function() { 
     687    var fpc = this.options.fieldPostCreation; 
     688    if (fpc) 
     689      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); 
     690  }, 
     691  prepareOptions: function() { 
     692    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); 
     693    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); 
     694    [this._extraDefaultOptions].flatten().compact().each(function(defs) { 
     695      Object.extend(this.options, defs); 
     696    }.bind(this)); 
     697  }, 
     698  prepareSubmission: function() { 
     699    this._saving = true; 
    707700    this.removeForm(); 
    708701    this.leaveHover(); 
    709702    this.showSaving(); 
    710703  }, 
     704  registerListeners: function() { 
     705    this._listeners = { }; 
     706    var listener; 
     707    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { 
     708      listener = this[pair.value].bind(this); 
     709      this._listeners[pair.key] = listener; 
     710      if (!this.options.externalControlOnly) 
     711        this.element.observe(pair.key, listener); 
     712      if (this.options.externalControl) 
     713        this.options.externalControl.observe(pair.key, listener); 
     714    }.bind(this)); 
     715  }, 
     716  removeForm: function() { 
     717    if (!this._form) return; 
     718    this._form.remove(); 
     719    this._form = null; 
     720    this._controls = { }; 
     721  }, 
    711722  showSaving: function() { 
    712     this.oldInnerHTML = this.element.innerHTML; 
     723    this._oldInnerHTML = this.element.innerHTML; 
    713724    this.element.innerHTML = this.options.savingText; 
    714     Element.addClassName(this.element, this.options.savingClassName); 
    715     this.element.style.backgroundColor = this.originalBackground; 
    716     Element.show(this.element); 
    717   }, 
    718   removeForm: function() { 
    719     if(this.form) { 
    720       if (this.form.parentNode) Element.remove(this.form); 
    721       this.form = null; 
    722     } 
    723   }, 
    724   enterHover: function() { 
    725     if (this.saving) return; 
    726     this.element.style.backgroundColor = this.options.highlightcolor; 
    727     if (this.effect) { 
    728       this.effect.cancel(); 
    729     } 
    730     Element.addClassName(this.element, this.options.hoverClassName) 
    731   }, 
    732   leaveHover: function() { 
    733     if (this.options.backgroundColor) { 
    734       this.element.style.backgroundColor = this.oldBackground; 
    735     } 
    736     Element.removeClassName(this.element, this.options.hoverClassName) 
    737     if (this.saving) return; 
    738     this.effect = new Effect.Highlight(this.element, { 
    739       startcolor: this.options.highlightcolor, 
    740       endcolor: this.options.highlightendcolor, 
    741       restorecolor: this.originalBackground 
    742     }); 
    743   }, 
    744   leaveEditMode: function() { 
    745     Element.removeClassName(this.element, this.options.savingClassName); 
    746     this.removeForm(); 
    747     this.leaveHover(); 
    748     this.element.style.backgroundColor = this.originalBackground; 
    749     Element.show(this.element); 
    750     if (this.options.externalControl) { 
    751       Element.show(this.options.externalControl); 
    752     } 
    753     this.editing = false; 
    754     this.saving = false; 
    755     this.oldInnerHTML = null; 
    756     this.onLeaveEditMode(); 
    757   }, 
    758   onComplete: function(transport) { 
     725    this.element.addClassName(this.options.savingClassName); 
     726    this.element.style.backgroundColor = this._originalBackground; 
     727    this.element.show(); 
     728  }, 
     729  triggerCallback: function(cbName, arg) { 
     730    if ('function' == typeof this.options[cbName]) { 
     731      this.options[cbName](this, arg); 
     732    } 
     733  }, 
     734  unregisterListeners: function() { 
     735    $H(this._listeners).each(function(pair) { 
     736      if (!this.options.externalControlOnly) 
     737        this.element.stopObserving(pair.key, pair.value); 
     738      if (this.options.externalControl) 
     739        this.options.externalControl.stopObserving(pair.key, pair.value); 
     740    }.bind(this)); 
     741  }, 
     742  wrapUp: function(transport) { 
    759743    this.leaveEditMode(); 
    760     this.options.onComplete.bind(this)(transport, this.element); 
    761   }, 
    762   onEnterEditMode: function() {}, 
    763   onLeaveEditMode: function() {}, 
    764   dispose: function() { 
    765     if (this.oldInnerHTML) { 
    766       this.element.innerHTML = this.oldInnerHTML; 
    767     } 
    768     this.leaveEditMode(); 
    769     Event.stopObserving(this.element, 'click', this.onclickListener); 
    770     Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); 
    771     Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); 
    772     if (this.options.externalControl) { 
    773       Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); 
    774       Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); 
    775       Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); 
    776     } 
    777   } 
    778 }; 
    779  
    780 Ajax.InPlaceCollectionEditor = Class.create(); 
    781 Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); 
    782 Object.extend(Ajax.InPlaceCollectionEditor.prototype, { 
    783   createEditField: function() { 
    784     if (!this.cached_selectTag) { 
    785       var selectTag = document.createElement("select"); 
    786       var collection = this.options.collection || []; 
    787       var optionTag; 
    788       collection.each(function(e,i) { 
    789         optionTag = document.createElement("option"); 
    790         optionTag.value = (e instanceof Array) ? e[0] : e; 
    791         if((typeof this.options.value == 'undefined') &&  
    792           ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true; 
    793         if(this.options.value==optionTag.value) optionTag.selected = true; 
    794         optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); 
    795         selectTag.appendChild(optionTag); 
    796       }.bind(this)); 
    797       this.cached_selectTag = selectTag; 
    798     } 
    799  
    800     this.editField = this.cached_selectTag; 
    801     if(this.options.loadTextURL) this.loadExternalText(); 
    802     this.form.appendChild(this.editField); 
    803     this.options.callback = function(form, value) { 
    804       return "value=" + encodeURIComponent(value); 
    805     } 
     744    // Can't use triggerCallback due to backward compatibility: requires 
     745    // binding + direct element 
     746    this._boundComplete(transport, this.element); 
    806747  } 
    807748}); 
     749 
     750Object.extend(Ajax.InPlaceEditor.prototype, { 
     751  dispose: Ajax.InPlaceEditor.prototype.destroy 
     752}); 
     753 
     754Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { 
     755  initialize: function($super, element, url, options) { 
     756    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; 
     757    $super(element, url, options); 
     758  }, 
     759 
     760  createEditField: function() { 
     761    var list = document.createElement('select'); 
     762    list.name = this.options.paramName; 
     763    list.size = 1; 
     764    this._controls.editor = list; 
     765    this._collection = this.options.collection || []; 
     766    if (this.options.loadCollectionURL) 
     767      this.loadCollection(); 
     768    else 
     769      this.checkForExternalText(); 
     770    this._form.appendChild(this._controls.editor); 
     771  }, 
     772 
     773  loadCollection: function() { 
     774    this._form.addClassName(this.options.loadingClassName); 
     775    this.showLoadingText(this.options.loadingCollectionText); 
     776    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 
     777    Object.extend(options, { 
     778      parameters: 'editorId=' + encodeURIComponent(this.element.id), 
     779      onComplete: Prototype.emptyFunction, 
     780      onSuccess: function(transport) { 
     781        var js = transport.responseText.strip(); 
     782        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check 
     783          throw 'Server returned an invalid collection representation.'; 
     784        this._collection = eval(js); 
     785        this.checkForExternalText(); 
     786      }.bind(this), 
     787      onFailure: this.onFailure 
     788    }); 
     789    new Ajax.Request(this.options.loadCollectionURL, options); 
     790  }, 
     791 
     792  showLoadingText: function(text) { 
     793    this._controls.editor.disabled = true; 
     794    var tempOption = this._controls.editor.firstChild; 
     795    if (!tempOption) { 
     796      tempOption = document.createElement('option'); 
     797      tempOption.value = ''; 
     798      this._controls.editor.appendChild(tempOption); 
     799      tempOption.selected = true; 
     800    } 
     801    tempOption.update((text || '').stripScripts().stripTags()); 
     802  }, 
     803 
     804  checkForExternalText: function() { 
     805    this._text = this.getText(); 
     806    if (this.options.loadTextURL) 
     807      this.loadExternalText(); 
     808    else 
     809      this.buildOptionList(); 
     810  }, 
     811 
     812  loadExternalText: function() { 
     813    this.showLoadingText(this.options.loadingText); 
     814    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 
     815    Object.extend(options, { 
     816      parameters: 'editorId=' + encodeURIComponent(this.element.id), 
     817      onComplete: Prototype.emptyFunction, 
     818      onSuccess: function(transport) { 
     819        this._text = transport.responseText.strip(); 
     820        this.buildOptionList(); 
     821      }.bind(this), 
     822      onFailure: this.onFailure 
     823    }); 
     824    new Ajax.Request(this.options.loadTextURL, options); 
     825  }, 
     826 
     827  buildOptionList: function() { 
     828    this._form.removeClassName(this.options.loadingClassName); 
     829    this._collection = this._collection.map(function(entry) { 
     830      return 2 === entry.length ? entry : [entry, entry].flatten(); 
     831    }); 
     832    var marker = ('value' in this.options) ? this.options.value : this._text; 
     833    var textFound = this._collection.any(function(entry) { 
     834      return entry[0] == marker; 
     835    }.bind(this)); 
     836    this._controls.editor.update(''); 
     837    var option; 
     838    this._collection.each(function(entry, index) { 
     839      option = document.createElement('option'); 
     840      option.value = entry[0]; 
     841      option.selected = textFound ? entry[0] == marker : 0 == index; 
     842      option.appendChild(document.createTextNode(entry[1])); 
     843      this._controls.editor.appendChild(option); 
     844    }.bind(this)); 
     845    this._controls.editor.disabled = false; 
     846    Field.scrollFreeActivate(this._controls.editor); 
     847  } 
     848}); 
     849 
     850//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** 
     851//**** This only  exists for a while,  in order to  let **** 
     852//**** users adapt to  the new API.  Read up on the new **** 
     853//**** API and convert your code to it ASAP!            **** 
     854 
     855Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { 
     856  if (!options) return; 
     857  function fallback(name, expr) { 
     858    if (name in options || expr === undefined) return; 
     859    options[name] = expr; 
     860  }; 
     861  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : 
     862    options.cancelLink == options.cancelButton == false ? false : undefined))); 
     863  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : 
     864    options.okLink == options.okButton == false ? false : undefined))); 
     865  fallback('highlightColor', options.highlightcolor); 
     866  fallback('highlightEndColor', options.highlightendcolor); 
     867}; 
     868 
     869Object.extend(Ajax.InPlaceEditor, { 
     870  DefaultOptions: { 
     871    ajaxOptions: { }, 
     872    autoRows: 3,                                // Use when multi-line w/ rows == 1 
     873    cancelControl: 'link',                      // 'link'|'button'|false 
     874    cancelText: 'cancel', 
     875    clickToEditText: 'Click to edit', 
     876    externalControl: null,                      // id|elt 
     877    externalControlOnly: false, 
     878    fieldPostCreation: 'activate',              // 'activate'|'focus'|false 
     879    formClassName: 'inplaceeditor-form', 
     880    formId: null,                               // id|elt 
     881    highlightColor: '#ffff99', 
     882    highlightEndColor: '#ffffff', 
     883    hoverClassName: '', 
     884    htmlResponse: true, 
     885&nb