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