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