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