michael@0: // script.aculo.us controls.js v1.7.1_beta2, Tue May 15 15:15:45 EDT 2007 michael@0: michael@0: // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) michael@0: // (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) michael@0: // (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) michael@0: // Contributors: michael@0: // Richard Livsey michael@0: // Rahul Bhargava michael@0: // Rob Wills michael@0: // michael@0: // script.aculo.us is freely distributable under the terms of an MIT-style license. michael@0: // For details, see the script.aculo.us web site: http://script.aculo.us/ michael@0: michael@0: // Autocompleter.Base handles all the autocompletion functionality michael@0: // that's independent of the data source for autocompletion. This michael@0: // includes drawing the autocompletion menu, observing keyboard michael@0: // and mouse events, and similar. michael@0: // michael@0: // Specific autocompleters need to provide, at the very least, michael@0: // a getUpdatedChoices function that will be invoked every time michael@0: // the text inside the monitored textbox changes. This method michael@0: // should get the text for which to provide autocompletion by michael@0: // invoking this.getToken(), NOT by directly accessing michael@0: // this.element.value. This is to allow incremental tokenized michael@0: // autocompletion. Specific auto-completion logic (AJAX, etc) michael@0: // belongs in getUpdatedChoices. michael@0: // michael@0: // Tokenized incremental autocompletion is enabled automatically michael@0: // when an autocompleter is instantiated with the 'tokens' option michael@0: // in the options parameter, e.g.: michael@0: // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); michael@0: // will incrementally autocomplete with a comma as the token. michael@0: // Additionally, ',' in the above example can be replaced with michael@0: // a token array, e.g. { tokens: [',', '\n'] } which michael@0: // enables autocompletion on multiple tokens. This is most michael@0: // useful when one of the tokens is \n (a newline), as it michael@0: // allows smart autocompletion after linebreaks. michael@0: michael@0: if(typeof Effect == 'undefined') michael@0: throw("controls.js requires including script.aculo.us' effects.js library"); michael@0: michael@0: var Autocompleter = {} michael@0: Autocompleter.Base = function() {}; michael@0: Autocompleter.Base.prototype = { michael@0: baseInitialize: function(element, update, options) { michael@0: element = $(element) michael@0: this.element = element; michael@0: this.update = $(update); michael@0: this.hasFocus = false; michael@0: this.changed = false; michael@0: this.active = false; michael@0: this.index = 0; michael@0: this.entryCount = 0; michael@0: michael@0: if(this.setOptions) michael@0: this.setOptions(options); michael@0: else michael@0: this.options = options || {}; michael@0: michael@0: this.options.paramName = this.options.paramName || this.element.name; michael@0: this.options.tokens = this.options.tokens || []; michael@0: this.options.frequency = this.options.frequency || 0.4; michael@0: this.options.minChars = this.options.minChars || 1; michael@0: this.options.onShow = this.options.onShow || michael@0: function(element, update){ michael@0: if(!update.style.position || update.style.position=='absolute') { michael@0: update.style.position = 'absolute'; michael@0: Position.clone(element, update, { michael@0: setHeight: false, michael@0: offsetTop: element.offsetHeight michael@0: }); michael@0: } michael@0: Effect.Appear(update,{duration:0.15}); michael@0: }; michael@0: this.options.onHide = this.options.onHide || michael@0: function(element, update){ new Effect.Fade(update,{duration:0.15}) }; michael@0: michael@0: if(typeof(this.options.tokens) == 'string') michael@0: this.options.tokens = new Array(this.options.tokens); michael@0: michael@0: this.observer = null; michael@0: michael@0: this.element.setAttribute('autocomplete','off'); michael@0: michael@0: Element.hide(this.update); michael@0: michael@0: Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); michael@0: Event.observe(this.element, 'keypress', this.onKeyPress.bindAsEventListener(this)); michael@0: michael@0: // Turn autocomplete back on when the user leaves the page, so that the michael@0: // field's value will be remembered on Mozilla-based browsers. michael@0: Event.observe(window, 'beforeunload', function(){ michael@0: element.setAttribute('autocomplete', 'on'); michael@0: }); michael@0: }, michael@0: michael@0: show: function() { michael@0: if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); michael@0: if(!this.iefix && michael@0: (Prototype.Browser.IE) && michael@0: (Element.getStyle(this.update, 'position')=='absolute')) { michael@0: new Insertion.After(this.update, michael@0: ''); michael@0: this.iefix = $(this.update.id+'_iefix'); michael@0: } michael@0: if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); michael@0: }, michael@0: michael@0: fixIEOverlapping: function() { michael@0: Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); michael@0: this.iefix.style.zIndex = 1; michael@0: this.update.style.zIndex = 2; michael@0: Element.show(this.iefix); michael@0: }, michael@0: michael@0: hide: function() { michael@0: this.stopIndicator(); michael@0: if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); michael@0: if(this.iefix) Element.hide(this.iefix); michael@0: }, michael@0: michael@0: startIndicator: function() { michael@0: if(this.options.indicator) Element.show(this.options.indicator); michael@0: }, michael@0: michael@0: stopIndicator: function() { michael@0: if(this.options.indicator) Element.hide(this.options.indicator); michael@0: }, michael@0: michael@0: onKeyPress: function(event) { michael@0: if(this.active) michael@0: switch(event.keyCode) { michael@0: case Event.KEY_TAB: michael@0: case Event.KEY_RETURN: michael@0: this.selectEntry(); michael@0: Event.stop(event); michael@0: case Event.KEY_ESC: michael@0: this.hide(); michael@0: this.active = false; michael@0: Event.stop(event); michael@0: return; michael@0: case Event.KEY_LEFT: michael@0: case Event.KEY_RIGHT: michael@0: return; michael@0: case Event.KEY_UP: michael@0: this.markPrevious(); michael@0: this.render(); michael@0: if(Prototype.Browser.WebKit) Event.stop(event); michael@0: return; michael@0: case Event.KEY_DOWN: michael@0: this.markNext(); michael@0: this.render(); michael@0: if(Prototype.Browser.WebKit) Event.stop(event); michael@0: return; michael@0: } michael@0: else michael@0: if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || michael@0: (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; michael@0: michael@0: this.changed = true; michael@0: this.hasFocus = true; michael@0: michael@0: if(this.observer) clearTimeout(this.observer); michael@0: this.observer = michael@0: setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); michael@0: }, michael@0: michael@0: activate: function() { michael@0: this.changed = false; michael@0: this.hasFocus = true; michael@0: this.getUpdatedChoices(); michael@0: }, michael@0: michael@0: onHover: function(event) { michael@0: var element = Event.findElement(event, 'LI'); michael@0: if(this.index != element.autocompleteIndex) michael@0: { michael@0: this.index = element.autocompleteIndex; michael@0: this.render(); michael@0: } michael@0: Event.stop(event); michael@0: }, michael@0: michael@0: onClick: function(event) { michael@0: var element = Event.findElement(event, 'LI'); michael@0: this.index = element.autocompleteIndex; michael@0: this.selectEntry(); michael@0: this.hide(); michael@0: }, michael@0: michael@0: onBlur: function(event) { michael@0: // needed to make click events working michael@0: setTimeout(this.hide.bind(this), 250); michael@0: this.hasFocus = false; michael@0: this.active = false; michael@0: }, michael@0: michael@0: render: function() { michael@0: if(this.entryCount > 0) { michael@0: for (var i = 0; i < this.entryCount; i++) michael@0: this.index==i ? michael@0: Element.addClassName(this.getEntry(i),"selected") : michael@0: Element.removeClassName(this.getEntry(i),"selected"); michael@0: if(this.hasFocus) { michael@0: this.show(); michael@0: this.active = true; michael@0: } michael@0: } else { michael@0: this.active = false; michael@0: this.hide(); michael@0: } michael@0: }, michael@0: michael@0: markPrevious: function() { michael@0: if(this.index > 0) this.index-- michael@0: else this.index = this.entryCount-1; michael@0: this.getEntry(this.index).scrollIntoView(true); michael@0: }, michael@0: michael@0: markNext: function() { michael@0: if(this.index < this.entryCount-1) this.index++ michael@0: else this.index = 0; michael@0: this.getEntry(this.index).scrollIntoView(false); michael@0: }, michael@0: michael@0: getEntry: function(index) { michael@0: return this.update.firstChild.childNodes[index]; michael@0: }, michael@0: michael@0: getCurrentEntry: function() { michael@0: return this.getEntry(this.index); michael@0: }, michael@0: michael@0: selectEntry: function() { michael@0: this.active = false; michael@0: this.updateElement(this.getCurrentEntry()); michael@0: }, michael@0: michael@0: updateElement: function(selectedElement) { michael@0: if (this.options.updateElement) { michael@0: this.options.updateElement(selectedElement); michael@0: return; michael@0: } michael@0: var value = ''; michael@0: if (this.options.select) { michael@0: var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; michael@0: if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); michael@0: } else michael@0: value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); michael@0: michael@0: var lastTokenPos = this.findLastToken(); michael@0: if (lastTokenPos != -1) { michael@0: var newValue = this.element.value.substr(0, lastTokenPos + 1); michael@0: var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); michael@0: if (whitespace) michael@0: newValue += whitespace[0]; michael@0: this.element.value = newValue + value; michael@0: } else { michael@0: this.element.value = value; michael@0: } michael@0: this.element.focus(); michael@0: michael@0: if (this.options.afterUpdateElement) michael@0: this.options.afterUpdateElement(this.element, selectedElement); michael@0: }, michael@0: michael@0: updateChoices: function(choices) { michael@0: if(!this.changed && this.hasFocus) { michael@0: this.update.innerHTML = choices; michael@0: Element.cleanWhitespace(this.update); michael@0: Element.cleanWhitespace(this.update.down()); michael@0: michael@0: if(this.update.firstChild && this.update.down().childNodes) { michael@0: this.entryCount = michael@0: this.update.down().childNodes.length; michael@0: for (var i = 0; i < this.entryCount; i++) { michael@0: var entry = this.getEntry(i); michael@0: entry.autocompleteIndex = i; michael@0: this.addObservers(entry); michael@0: } michael@0: } else { michael@0: this.entryCount = 0; michael@0: } michael@0: michael@0: this.stopIndicator(); michael@0: this.index = 0; michael@0: michael@0: if(this.entryCount==1 && this.options.autoSelect) { michael@0: this.selectEntry(); michael@0: this.hide(); michael@0: } else { michael@0: this.render(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: addObservers: function(element) { michael@0: Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); michael@0: Event.observe(element, "click", this.onClick.bindAsEventListener(this)); michael@0: }, michael@0: michael@0: onObserverEvent: function() { michael@0: this.changed = false; michael@0: if(this.getToken().length>=this.options.minChars) { michael@0: this.getUpdatedChoices(); michael@0: } else { michael@0: this.active = false; michael@0: this.hide(); michael@0: } michael@0: }, michael@0: michael@0: getToken: function() { michael@0: var tokenPos = this.findLastToken(); michael@0: if (tokenPos != -1) michael@0: var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); michael@0: else michael@0: var ret = this.element.value; michael@0: michael@0: return /\n/.test(ret) ? '' : ret; michael@0: }, michael@0: michael@0: findLastToken: function() { michael@0: var lastTokenPos = -1; michael@0: michael@0: for (var i=0; i lastTokenPos) michael@0: lastTokenPos = thisTokenPos; michael@0: } michael@0: return lastTokenPos; michael@0: } michael@0: } michael@0: michael@0: Ajax.Autocompleter = Class.create(); michael@0: Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { michael@0: initialize: function(element, update, url, options) { michael@0: this.baseInitialize(element, update, options); michael@0: this.options.asynchronous = true; michael@0: this.options.onComplete = this.onComplete.bind(this); michael@0: this.options.defaultParams = this.options.parameters || null; michael@0: this.url = url; michael@0: }, michael@0: michael@0: getUpdatedChoices: function() { michael@0: this.startIndicator(); michael@0: michael@0: var entry = encodeURIComponent(this.options.paramName) + '=' + michael@0: encodeURIComponent(this.getToken()); michael@0: michael@0: this.options.parameters = this.options.callback ? michael@0: this.options.callback(this.element, entry) : entry; michael@0: michael@0: if(this.options.defaultParams) michael@0: this.options.parameters += '&' + this.options.defaultParams; michael@0: michael@0: new Ajax.Request(this.url, this.options); michael@0: }, michael@0: michael@0: onComplete: function(request) { michael@0: this.updateChoices(request.responseText); michael@0: } michael@0: michael@0: }); michael@0: michael@0: // The local array autocompleter. Used when you'd prefer to michael@0: // inject an array of autocompletion options into the page, rather michael@0: // than sending out Ajax queries, which can be quite slow sometimes. michael@0: // michael@0: // The constructor takes four parameters. The first two are, as usual, michael@0: // the id of the monitored textbox, and id of the autocompletion menu. michael@0: // The third is the array you want to autocomplete from, and the fourth michael@0: // is the options block. michael@0: // michael@0: // Extra local autocompletion options: michael@0: // - choices - How many autocompletion choices to offer michael@0: // michael@0: // - partialSearch - If false, the autocompleter will match entered michael@0: // text only at the beginning of strings in the michael@0: // autocomplete array. Defaults to true, which will michael@0: // match text at the beginning of any *word* in the michael@0: // strings in the autocomplete array. If you want to michael@0: // search anywhere in the string, additionally set michael@0: // the option fullSearch to true (default: off). michael@0: // michael@0: // - fullSsearch - Search anywhere in autocomplete array strings. michael@0: // michael@0: // - partialChars - How many characters to enter before triggering michael@0: // a partial match (unlike minChars, which defines michael@0: // how many characters are required to do any match michael@0: // at all). Defaults to 2. michael@0: // michael@0: // - ignoreCase - Whether to ignore case when autocompleting. michael@0: // Defaults to true. michael@0: // michael@0: // It's possible to pass in a custom function as the 'selector' michael@0: // option, if you prefer to write your own autocompletion logic. michael@0: // In that case, the other options above will not apply unless michael@0: // you support them. michael@0: michael@0: Autocompleter.Local = Class.create(); michael@0: Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { michael@0: initialize: function(element, update, array, options) { michael@0: this.baseInitialize(element, update, options); michael@0: this.options.array = array; michael@0: }, michael@0: michael@0: getUpdatedChoices: function() { michael@0: this.updateChoices(this.options.selector(this)); michael@0: }, michael@0: michael@0: setOptions: function(options) { michael@0: this.options = Object.extend({ michael@0: choices: 10, michael@0: partialSearch: true, michael@0: partialChars: 2, michael@0: ignoreCase: true, michael@0: fullSearch: false, michael@0: selector: function(instance) { michael@0: var ret = []; // Beginning matches michael@0: var partial = []; // Inside matches michael@0: var entry = instance.getToken(); michael@0: var count = 0; michael@0: michael@0: for (var i = 0; i < instance.options.array.length && michael@0: ret.length < instance.options.choices ; i++) { michael@0: michael@0: var elem = instance.options.array[i]; michael@0: var foundPos = instance.options.ignoreCase ? michael@0: elem.toLowerCase().indexOf(entry.toLowerCase()) : michael@0: elem.indexOf(entry); michael@0: michael@0: while (foundPos != -1) { michael@0: if (foundPos == 0 && elem.length != entry.length) { michael@0: ret.push("
  • " + elem.substr(0, entry.length) + "" + michael@0: elem.substr(entry.length) + "
  • "); michael@0: break; michael@0: } else if (entry.length >= instance.options.partialChars && michael@0: instance.options.partialSearch && foundPos != -1) { michael@0: if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { michael@0: partial.push("
  • " + elem.substr(0, foundPos) + "" + michael@0: elem.substr(foundPos, entry.length) + "" + elem.substr( michael@0: foundPos + entry.length) + "
  • "); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: foundPos = instance.options.ignoreCase ? michael@0: elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : michael@0: elem.indexOf(entry, foundPos + 1); michael@0: michael@0: } michael@0: } michael@0: if (partial.length) michael@0: ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) michael@0: return ""; michael@0: } michael@0: }, options || {}); michael@0: } michael@0: }); michael@0: michael@0: // AJAX in-place editor michael@0: // michael@0: // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor michael@0: michael@0: // Use this if you notice weird scrolling problems on some browsers, michael@0: // the DOM might be a bit confused when this gets called so do this michael@0: // waits 1 ms (with setTimeout) until it does the activation michael@0: Field.scrollFreeActivate = function(field) { michael@0: setTimeout(function() { michael@0: Field.activate(field); michael@0: }, 1); michael@0: } michael@0: michael@0: Ajax.InPlaceEditor = Class.create(); michael@0: Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; michael@0: Ajax.InPlaceEditor.prototype = { michael@0: initialize: function(element, url, options) { michael@0: this.url = url; michael@0: this.element = $(element); michael@0: michael@0: this.options = Object.extend({ michael@0: paramName: "value", michael@0: okButton: true, michael@0: okLink: false, michael@0: okText: "ok", michael@0: cancelButton: false, michael@0: cancelLink: true, michael@0: cancelText: "cancel", michael@0: textBeforeControls: '', michael@0: textBetweenControls: '', michael@0: textAfterControls: '', michael@0: savingText: "Saving...", michael@0: clickToEditText: "Click to edit", michael@0: okText: "ok", michael@0: rows: 1, michael@0: onComplete: function(transport, element) { michael@0: new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); michael@0: }, michael@0: onFailure: function(transport) { michael@0: alert("Error communicating with the server: " + transport.responseText.stripTags()); michael@0: }, michael@0: callback: function(form) { michael@0: return Form.serialize(form); michael@0: }, michael@0: handleLineBreaks: true, michael@0: loadingText: 'Loading...', michael@0: savingClassName: 'inplaceeditor-saving', michael@0: loadingClassName: 'inplaceeditor-loading', michael@0: formClassName: 'inplaceeditor-form', michael@0: highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, michael@0: highlightendcolor: "#FFFFFF", michael@0: externalControl: null, michael@0: submitOnBlur: false, michael@0: ajaxOptions: {}, michael@0: evalScripts: false michael@0: }, options || {}); michael@0: michael@0: if(!this.options.formId && this.element.id) { michael@0: this.options.formId = this.element.id + "-inplaceeditor"; michael@0: if ($(this.options.formId)) { michael@0: // there's already a form with that name, don't specify an id michael@0: this.options.formId = null; michael@0: } michael@0: } michael@0: michael@0: if (this.options.externalControl) { michael@0: this.options.externalControl = $(this.options.externalControl); michael@0: } michael@0: michael@0: this.originalBackground = Element.getStyle(this.element, 'background-color'); michael@0: if (!this.originalBackground) { michael@0: this.originalBackground = "transparent"; michael@0: } michael@0: michael@0: this.element.title = this.options.clickToEditText; michael@0: michael@0: this.onclickListener = this.enterEditMode.bindAsEventListener(this); michael@0: this.mouseoverListener = this.enterHover.bindAsEventListener(this); michael@0: this.mouseoutListener = this.leaveHover.bindAsEventListener(this); michael@0: Event.observe(this.element, 'click', this.onclickListener); michael@0: Event.observe(this.element, 'mouseover', this.mouseoverListener); michael@0: Event.observe(this.element, 'mouseout', this.mouseoutListener); michael@0: if (this.options.externalControl) { michael@0: Event.observe(this.options.externalControl, 'click', this.onclickListener); michael@0: Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); michael@0: Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); michael@0: } michael@0: }, michael@0: enterEditMode: function(evt) { michael@0: if (this.saving) return; michael@0: if (this.editing) return; michael@0: this.editing = true; michael@0: this.onEnterEditMode(); michael@0: if (this.options.externalControl) { michael@0: Element.hide(this.options.externalControl); michael@0: } michael@0: Element.hide(this.element); michael@0: this.createForm(); michael@0: this.element.parentNode.insertBefore(this.form, this.element); michael@0: if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField); michael@0: // stop the event to avoid a page refresh in Safari michael@0: if (evt) { michael@0: Event.stop(evt); michael@0: } michael@0: return false; michael@0: }, michael@0: createForm: function() { michael@0: this.form = document.createElement("form"); michael@0: this.form.id = this.options.formId; michael@0: Element.addClassName(this.form, this.options.formClassName) michael@0: this.form.onsubmit = this.onSubmit.bind(this); michael@0: michael@0: this.createEditField(); michael@0: michael@0: if (this.options.textarea) { michael@0: var br = document.createElement("br"); michael@0: this.form.appendChild(br); michael@0: } michael@0: michael@0: if (this.options.textBeforeControls) michael@0: this.form.appendChild(document.createTextNode(this.options.textBeforeControls)); michael@0: michael@0: if (this.options.okButton) { michael@0: var okButton = document.createElement("input"); michael@0: okButton.type = "submit"; michael@0: okButton.value = this.options.okText; michael@0: okButton.className = 'editor_ok_button'; michael@0: this.form.appendChild(okButton); michael@0: } michael@0: michael@0: if (this.options.okLink) { michael@0: var okLink = document.createElement("a"); michael@0: okLink.href = "#"; michael@0: okLink.appendChild(document.createTextNode(this.options.okText)); michael@0: okLink.onclick = this.onSubmit.bind(this); michael@0: okLink.className = 'editor_ok_link'; michael@0: this.form.appendChild(okLink); michael@0: } michael@0: michael@0: if (this.options.textBetweenControls && michael@0: (this.options.okLink || this.options.okButton) && michael@0: (this.options.cancelLink || this.options.cancelButton)) michael@0: this.form.appendChild(document.createTextNode(this.options.textBetweenControls)); michael@0: michael@0: if (this.options.cancelButton) { michael@0: var cancelButton = document.createElement("input"); michael@0: cancelButton.type = "submit"; michael@0: cancelButton.value = this.options.cancelText; michael@0: cancelButton.onclick = this.onclickCancel.bind(this); michael@0: cancelButton.className = 'editor_cancel_button'; michael@0: this.form.appendChild(cancelButton); michael@0: } michael@0: michael@0: if (this.options.cancelLink) { michael@0: var cancelLink = document.createElement("a"); michael@0: cancelLink.href = "#"; michael@0: cancelLink.appendChild(document.createTextNode(this.options.cancelText)); michael@0: cancelLink.onclick = this.onclickCancel.bind(this); michael@0: cancelLink.className = 'editor_cancel editor_cancel_link'; michael@0: this.form.appendChild(cancelLink); michael@0: } michael@0: michael@0: if (this.options.textAfterControls) michael@0: this.form.appendChild(document.createTextNode(this.options.textAfterControls)); michael@0: }, michael@0: hasHTMLLineBreaks: function(string) { michael@0: if (!this.options.handleLineBreaks) return false; michael@0: return string.match(/
    /i); michael@0: }, michael@0: convertHTMLLineBreaks: function(string) { michael@0: return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); michael@0: }, michael@0: createEditField: function() { michael@0: var text; michael@0: if(this.options.loadTextURL) { michael@0: text = this.options.loadingText; michael@0: } else { michael@0: text = this.getText(); michael@0: } michael@0: michael@0: var obj = this; michael@0: michael@0: if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { michael@0: this.options.textarea = false; michael@0: var textField = document.createElement("input"); michael@0: textField.obj = this; michael@0: textField.type = "text"; michael@0: textField.name = this.options.paramName; michael@0: textField.value = text; michael@0: textField.style.backgroundColor = this.options.highlightcolor; michael@0: textField.className = 'editor_field'; michael@0: var size = this.options.size || this.options.cols || 0; michael@0: if (size != 0) textField.size = size; michael@0: if (this.options.submitOnBlur) michael@0: textField.onblur = this.onSubmit.bind(this); michael@0: this.editField = textField; michael@0: } else { michael@0: this.options.textarea = true; michael@0: var textArea = document.createElement("textarea"); michael@0: textArea.obj = this; michael@0: textArea.name = this.options.paramName; michael@0: textArea.value = this.convertHTMLLineBreaks(text); michael@0: textArea.rows = this.options.rows; michael@0: textArea.cols = this.options.cols || 40; michael@0: textArea.className = 'editor_field'; michael@0: if (this.options.submitOnBlur) michael@0: textArea.onblur = this.onSubmit.bind(this); michael@0: this.editField = textArea; michael@0: } michael@0: michael@0: if(this.options.loadTextURL) { michael@0: this.loadExternalText(); michael@0: } michael@0: this.form.appendChild(this.editField); michael@0: }, michael@0: getText: function() { michael@0: return this.element.innerHTML; michael@0: }, michael@0: loadExternalText: function() { michael@0: Element.addClassName(this.form, this.options.loadingClassName); michael@0: this.editField.disabled = true; michael@0: new Ajax.Request( michael@0: this.options.loadTextURL, michael@0: Object.extend({ michael@0: asynchronous: true, michael@0: onComplete: this.onLoadedExternalText.bind(this) michael@0: }, this.options.ajaxOptions) michael@0: ); michael@0: }, michael@0: onLoadedExternalText: function(transport) { michael@0: Element.removeClassName(this.form, this.options.loadingClassName); michael@0: this.editField.disabled = false; michael@0: this.editField.value = transport.responseText.stripTags(); michael@0: Field.scrollFreeActivate(this.editField); michael@0: }, michael@0: onclickCancel: function() { michael@0: this.onComplete(); michael@0: this.leaveEditMode(); michael@0: return false; michael@0: }, michael@0: onFailure: function(transport) { michael@0: this.options.onFailure(transport); michael@0: if (this.oldInnerHTML) { michael@0: this.element.innerHTML = this.oldInnerHTML; michael@0: this.oldInnerHTML = null; michael@0: } michael@0: return false; michael@0: }, michael@0: onSubmit: function() { michael@0: // onLoading resets these so we need to save them away for the Ajax call michael@0: var form = this.form; michael@0: var value = this.editField.value; michael@0: michael@0: // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... michael@0: // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... michael@0: // to be displayed indefinitely michael@0: this.onLoading(); michael@0: michael@0: if (this.options.evalScripts) { michael@0: new Ajax.Request( michael@0: this.url, Object.extend({ michael@0: parameters: this.options.callback(form, value), michael@0: onComplete: this.onComplete.bind(this), michael@0: onFailure: this.onFailure.bind(this), michael@0: asynchronous:true, michael@0: evalScripts:true michael@0: }, this.options.ajaxOptions)); michael@0: } else { michael@0: new Ajax.Updater( michael@0: { success: this.element, michael@0: // don't update on failure (this could be an option) michael@0: failure: null }, michael@0: this.url, Object.extend({ michael@0: parameters: this.options.callback(form, value), michael@0: onComplete: this.onComplete.bind(this), michael@0: onFailure: this.onFailure.bind(this) michael@0: }, this.options.ajaxOptions)); michael@0: } michael@0: // stop the event to avoid a page refresh in Safari michael@0: if (arguments.length > 1) { michael@0: Event.stop(arguments[0]); michael@0: } michael@0: return false; michael@0: }, michael@0: onLoading: function() { michael@0: this.saving = true; michael@0: this.removeForm(); michael@0: this.leaveHover(); michael@0: this.showSaving(); michael@0: }, michael@0: showSaving: function() { michael@0: this.oldInnerHTML = this.element.innerHTML; michael@0: this.element.innerHTML = this.options.savingText; michael@0: Element.addClassName(this.element, this.options.savingClassName); michael@0: this.element.style.backgroundColor = this.originalBackground; michael@0: Element.show(this.element); michael@0: }, michael@0: removeForm: function() { michael@0: if(this.form) { michael@0: if (this.form.parentNode) Element.remove(this.form); michael@0: this.form = null; michael@0: } michael@0: }, michael@0: enterHover: function() { michael@0: if (this.saving) return; michael@0: this.element.style.backgroundColor = this.options.highlightcolor; michael@0: if (this.effect) { michael@0: this.effect.cancel(); michael@0: } michael@0: Element.addClassName(this.element, this.options.hoverClassName) michael@0: }, michael@0: leaveHover: function() { michael@0: if (this.options.backgroundColor) { michael@0: this.element.style.backgroundColor = this.oldBackground; michael@0: } michael@0: Element.removeClassName(this.element, this.options.hoverClassName) michael@0: if (this.saving) return; michael@0: this.effect = new Effect.Highlight(this.element, { michael@0: startcolor: this.options.highlightcolor, michael@0: endcolor: this.options.highlightendcolor, michael@0: restorecolor: this.originalBackground michael@0: }); michael@0: }, michael@0: leaveEditMode: function() { michael@0: Element.removeClassName(this.element, this.options.savingClassName); michael@0: this.removeForm(); michael@0: this.leaveHover(); michael@0: this.element.style.backgroundColor = this.originalBackground; michael@0: Element.show(this.element); michael@0: if (this.options.externalControl) { michael@0: Element.show(this.options.externalControl); michael@0: } michael@0: this.editing = false; michael@0: this.saving = false; michael@0: this.oldInnerHTML = null; michael@0: this.onLeaveEditMode(); michael@0: }, michael@0: onComplete: function(transport) { michael@0: this.leaveEditMode(); michael@0: this.options.onComplete.bind(this)(transport, this.element); michael@0: }, michael@0: onEnterEditMode: function() {}, michael@0: onLeaveEditMode: function() {}, michael@0: dispose: function() { michael@0: if (this.oldInnerHTML) { michael@0: this.element.innerHTML = this.oldInnerHTML; michael@0: } michael@0: this.leaveEditMode(); michael@0: Event.stopObserving(this.element, 'click', this.onclickListener); michael@0: Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); michael@0: Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); michael@0: if (this.options.externalControl) { michael@0: Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); michael@0: Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); michael@0: Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: Ajax.InPlaceCollectionEditor = Class.create(); michael@0: Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); michael@0: Object.extend(Ajax.InPlaceCollectionEditor.prototype, { michael@0: createEditField: function() { michael@0: if (!this.cached_selectTag) { michael@0: var selectTag = document.createElement("select"); michael@0: var collection = this.options.collection || []; michael@0: var optionTag; michael@0: collection.each(function(e,i) { michael@0: optionTag = document.createElement("option"); michael@0: optionTag.value = (e instanceof Array) ? e[0] : e; michael@0: if((typeof this.options.value == 'undefined') && michael@0: ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true; michael@0: if(this.options.value==optionTag.value) optionTag.selected = true; michael@0: optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); michael@0: selectTag.appendChild(optionTag); michael@0: }.bind(this)); michael@0: this.cached_selectTag = selectTag; michael@0: } michael@0: michael@0: this.editField = this.cached_selectTag; michael@0: if(this.options.loadTextURL) this.loadExternalText(); michael@0: this.form.appendChild(this.editField); michael@0: this.options.callback = function(form, value) { michael@0: return "value=" + encodeURIComponent(value); michael@0: } michael@0: } michael@0: }); michael@0: michael@0: // Delayed observer, like Form.Element.Observer, michael@0: // but waits for delay after last key input michael@0: // Ideal for live-search fields michael@0: michael@0: Form.Element.DelayedObserver = Class.create(); michael@0: Form.Element.DelayedObserver.prototype = { michael@0: initialize: function(element, delay, callback) { michael@0: this.delay = delay || 0.5; michael@0: this.element = $(element); michael@0: this.callback = callback; michael@0: this.timer = null; michael@0: this.lastValue = $F(this.element); michael@0: Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); michael@0: }, michael@0: delayedListener: function(event) { michael@0: if(this.lastValue == $F(this.element)) return; michael@0: if(this.timer) clearTimeout(this.timer); michael@0: this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); michael@0: this.lastValue = $F(this.element); michael@0: }, michael@0: onTimerEvent: function() { michael@0: this.timer = null; michael@0: this.callback(this.element, $F(this.element)); michael@0: } michael@0: };