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)
9 // See scriptaculous.js for full license.
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.
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.
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.
36 var Autocompleter = {}
37 Autocompleter.Base = function() {};
38 Autocompleter.Base.prototype = {
39 baseInitialize: function(element, update, options) {
40 this.element = $(element);
41 this.update = $(update);
42 this.hasFocus = false;
49 this.setOptions(options);
51 this.options = options || {};
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});
63 Effect.Appear(update,{duration:0.15});
65 this.options.onHide = this.options.onHide ||
66 function(element, update){ new Effect.Fade(update,{duration:0.15}) };
68 if (typeof(this.options.tokens) == 'string')
69 this.options.tokens = new Array(this.options.tokens);
73 this.element.setAttribute('autocomplete','off');
75 Element.hide(this.update);
77 Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
78 Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
82 if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
83 if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
84 new Insertion.After(this.update,
85 '<iframe id="' + this.update.id + '_iefix" '+
86 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
87 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
88 this.iefix = $(this.update.id+'_iefix');
90 if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
93 fixIEOverlapping: function() {
94 Position.clone(this.update, this.iefix);
95 this.iefix.style.zIndex = 1;
96 this.update.style.zIndex = 2;
97 Element.show(this.iefix);
101 this.stopIndicator();
102 if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
103 if(this.iefix) Element.hide(this.iefix);
106 startIndicator: function() {
107 if(this.options.indicator) Element.show(this.options.indicator);
110 stopIndicator: function() {
111 if(this.options.indicator) Element.hide(this.options.indicator);
114 onKeyPress: function(event) {
116 switch(event.keyCode) {
118 case Event.KEY_RETURN:
127 case Event.KEY_RIGHT:
132 if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
137 if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
141 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
145 this.hasFocus = true;
147 if(this.observer) clearTimeout(this.observer);
149 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
152 onHover: function(event) {
153 var element = Event.findElement(event, 'LI');
154 if(this.index != element.autocompleteIndex)
156 this.index = element.autocompleteIndex;
162 onClick: function(event) {
163 var element = Event.findElement(event, 'LI');
164 this.index = element.autocompleteIndex;
169 onBlur: function(event) {
170 // needed to make click events working
171 setTimeout(this.hide.bind(this), 250);
172 this.hasFocus = false;
177 if(this.entryCount > 0) {
178 for (var i = 0; i < this.entryCount; i++)
180 Element.addClassName(this.getEntry(i),"selected") :
181 Element.removeClassName(this.getEntry(i),"selected");
193 markPrevious: function() {
194 if(this.index > 0) this.index--
195 else this.index = this.entryCount-1;
198 markNext: function() {
199 if(this.index < this.entryCount-1) this.index++
203 getEntry: function(index) {
204 return this.update.firstChild.childNodes[index];
207 getCurrentEntry: function() {
208 return this.getEntry(this.index);
211 selectEntry: function() {
213 this.updateElement(this.getCurrentEntry());
216 updateElement: function(selectedElement) {
217 if (this.options.updateElement) {
218 this.options.updateElement(selectedElement);
222 var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
223 var lastTokenPos = this.findLastToken();
224 if (lastTokenPos != -1) {
225 var newValue = this.element.value.substr(0, lastTokenPos + 1);
226 var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
228 newValue += whitespace[0];
229 this.element.value = newValue + value;
231 this.element.value = value;
233 this.element.focus();
235 if (this.options.afterUpdateElement)
236 this.options.afterUpdateElement(this.element, selectedElement);
239 updateChoices: function(choices) {
240 if(!this.changed && this.hasFocus) {
241 this.update.innerHTML = choices;
242 Element.cleanWhitespace(this.update);
243 Element.cleanWhitespace(this.update.firstChild);
245 if(this.update.firstChild && this.update.firstChild.childNodes) {
247 this.update.firstChild.childNodes.length;
248 for (var i = 0; i < this.entryCount; i++) {
249 var entry = this.getEntry(i);
250 entry.autocompleteIndex = i;
251 this.addObservers(entry);
257 this.stopIndicator();
264 addObservers: function(element) {
265 Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
266 Event.observe(element, "click", this.onClick.bindAsEventListener(this));
269 onObserverEvent: function() {
270 this.changed = false;
271 if(this.getToken().length>=this.options.minChars) {
272 this.startIndicator();
273 this.getUpdatedChoices();
280 getToken: function() {
281 var tokenPos = this.findLastToken();
283 var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
285 var ret = this.element.value;
287 return /\n/.test(ret) ? '' : ret;
290 findLastToken: function() {
291 var lastTokenPos = -1;
293 for (var i=0; i<this.options.tokens.length; i++) {
294 var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
295 if (thisTokenPos > lastTokenPos)
296 lastTokenPos = thisTokenPos;
302 Ajax.Autocompleter = Class.create();
303 Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
304 initialize: function(element, update, url, options) {
305 this.baseInitialize(element, update, options);
306 this.options.asynchronous = true;
307 this.options.onComplete = this.onComplete.bind(this);
308 this.options.defaultParams = this.options.parameters || null;
312 getUpdatedChoices: function() {
313 entry = encodeURIComponent(this.options.paramName) + '=' +
314 encodeURIComponent(this.getToken());
316 this.options.parameters = this.options.callback ?
317 this.options.callback(this.element, entry) : entry;
319 if(this.options.defaultParams)
320 this.options.parameters += '&' + this.options.defaultParams;
322 new Ajax.Request(this.url, this.options);
325 onComplete: function(request) {
326 this.updateChoices(request.responseText);
331 // The local array autocompleter. Used when you'd prefer to
332 // inject an array of autocompletion options into the page, rather
333 // than sending out Ajax queries, which can be quite slow sometimes.
335 // The constructor takes four parameters. The first two are, as usual,
336 // the id of the monitored textbox, and id of the autocompletion menu.
337 // The third is the array you want to autocomplete from, and the fourth
338 // is the options block.
340 // Extra local autocompletion options:
341 // - choices - How many autocompletion choices to offer
343 // - partialSearch - If false, the autocompleter will match entered
344 // text only at the beginning of strings in the
345 // autocomplete array. Defaults to true, which will
346 // match text at the beginning of any *word* in the
347 // strings in the autocomplete array. If you want to
348 // search anywhere in the string, additionally set
349 // the option fullSearch to true (default: off).
351 // - fullSsearch - Search anywhere in autocomplete array strings.
353 // - partialChars - How many characters to enter before triggering
354 // a partial match (unlike minChars, which defines
355 // how many characters are required to do any match
356 // at all). Defaults to 2.
358 // - ignoreCase - Whether to ignore case when autocompleting.
361 // It's possible to pass in a custom function as the 'selector'
362 // option, if you prefer to write your own autocompletion logic.
363 // In that case, the other options above will not apply unless
366 Autocompleter.Local = Class.create();
367 Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
368 initialize: function(element, update, array, options) {
369 this.baseInitialize(element, update, options);
370 this.options.array = array;
373 getUpdatedChoices: function() {
374 this.updateChoices(this.options.selector(this));
377 setOptions: function(options) {
378 this.options = Object.extend({
384 selector: function(instance) {
385 var ret = []; // Beginning matches
386 var partial = []; // Inside matches
387 var entry = instance.getToken();
390 for (var i = 0; i < instance.options.array.length &&
391 ret.length < instance.options.choices ; i++) {
393 var elem = instance.options.array[i];
394 var foundPos = instance.options.ignoreCase ?
395 elem.toLowerCase().indexOf(entry.toLowerCase()) :
398 while (foundPos != -1) {
399 if (foundPos == 0 && elem.length != entry.length) {
400 ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
401 elem.substr(entry.length) + "</li>");
403 } else if (entry.length >= instance.options.partialChars &&
404 instance.options.partialSearch && foundPos != -1) {
405 if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
406 partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
407 elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
408 foundPos + entry.length) + "</li>");
413 foundPos = instance.options.ignoreCase ?
414 elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
415 elem.indexOf(entry, foundPos + 1);
420 ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
421 return "<ul>" + ret.join('') + "</ul>";
427 // AJAX in-place editor
429 // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
431 // Use this if you notice weird scrolling problems on some browsers,
432 // the DOM might be a bit confused when this gets called so do this
433 // waits 1 ms (with setTimeout) until it does the activation
434 Field.scrollFreeActivate = function(field) {
435 setTimeout(function() {
436 Field.activate(field);
440 Ajax.InPlaceEditor = Class.create();
441 Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
442 Ajax.InPlaceEditor.prototype = {
443 initialize: function(element, url, options) {
445 this.element = $(element);
447 this.options = Object.extend({
449 cancelText: "cancel",
450 savingText: "Saving...",
451 clickToEditText: "Click to edit",
454 onComplete: function(transport, element) {
455 new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
457 onFailure: function(transport) {
458 alert("Error communicating with the server: " + transport.responseText.stripTags());
460 callback: function(form) {
461 return Form.serialize(form);
463 handleLineBreaks: true,
464 loadingText: 'Loading...',
465 savingClassName: 'inplaceeditor-saving',
466 loadingClassName: 'inplaceeditor-loading',
467 formClassName: 'inplaceeditor-form',
468 highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
469 highlightendcolor: "#FFFFFF",
470 externalControl: null,
474 if(!this.options.formId && this.element.id) {
475 this.options.formId = this.element.id + "-inplaceeditor";
476 if ($(this.options.formId)) {
477 // there's already a form with that name, don't specify an id
478 this.options.formId = null;
482 if (this.options.externalControl) {
483 this.options.externalControl = $(this.options.externalControl);
486 this.originalBackground = Element.getStyle(this.element, 'background-color');
487 if (!this.originalBackground) {
488 this.originalBackground = "transparent";
491 this.element.title = this.options.clickToEditText;
493 this.onclickListener = this.enterEditMode.bindAsEventListener(this);
494 this.mouseoverListener = this.enterHover.bindAsEventListener(this);
495 this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
496 Event.observe(this.element, 'click', this.onclickListener);
497 Event.observe(this.element, 'mouseover', this.mouseoverListener);
498 Event.observe(this.element, 'mouseout', this.mouseoutListener);
499 if (this.options.externalControl) {
500 Event.observe(this.options.externalControl, 'click', this.onclickListener);
501 Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
502 Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
505 enterEditMode: function(evt) {
506 if (this.saving) return;
507 if (this.editing) return;
509 this.onEnterEditMode();
510 if (this.options.externalControl) {
511 Element.hide(this.options.externalControl);
513 Element.hide(this.element);
515 this.element.parentNode.insertBefore(this.form, this.element);
516 Field.scrollFreeActivate(this.editField);
517 // stop the event to avoid a page refresh in Safari
523 createForm: function() {
524 this.form = document.createElement("form");
525 this.form.id = this.options.formId;
526 Element.addClassName(this.form, this.options.formClassName)
527 this.form.onsubmit = this.onSubmit.bind(this);
529 this.createEditField();
531 if (this.options.textarea) {
532 var br = document.createElement("br");
533 this.form.appendChild(br);
536 okButton = document.createElement("input");
537 okButton.type = "submit";
538 okButton.value = this.options.okText;
539 this.form.appendChild(okButton);
541 cancelLink = document.createElement("a");
542 cancelLink.href = "#";
543 cancelLink.appendChild(document.createTextNode(this.options.cancelText));
544 cancelLink.onclick = this.onclickCancel.bind(this);
545 this.form.appendChild(cancelLink);
547 hasHTMLLineBreaks: function(string) {
548 if (!this.options.handleLineBreaks) return false;
549 return string.match(/<br/i) || string.match(/<p>/i);
551 convertHTMLLineBreaks: function(string) {
552 return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
554 createEditField: function() {
556 if(this.options.loadTextURL) {
557 text = this.options.loadingText;
559 text = this.getText();
562 if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
563 this.options.textarea = false;
564 var textField = document.createElement("input");
565 textField.type = "text";
566 textField.name = "value";
567 textField.value = text;
568 textField.style.backgroundColor = this.options.highlightcolor;
569 var size = this.options.size || this.options.cols || 0;
570 if (size != 0) textField.size = size;
571 this.editField = textField;
573 this.options.textarea = true;
574 var textArea = document.createElement("textarea");
575 textArea.name = "value";
576 textArea.value = this.convertHTMLLineBreaks(text);
577 textArea.rows = this.options.rows;
578 textArea.cols = this.options.cols || 40;
579 this.editField = textArea;
582 if(this.options.loadTextURL) {
583 this.loadExternalText();
585 this.form.appendChild(this.editField);
587 getText: function() {
588 return this.element.innerHTML;
590 loadExternalText: function() {
591 Element.addClassName(this.form, this.options.loadingClassName);
592 this.editField.disabled = true;
594 this.options.loadTextURL,
597 onComplete: this.onLoadedExternalText.bind(this)
598 }, this.options.ajaxOptions)
601 onLoadedExternalText: function(transport) {
602 Element.removeClassName(this.form, this.options.loadingClassName);
603 this.editField.disabled = false;
604 this.editField.value = transport.responseText.stripTags();
606 onclickCancel: function() {
608 this.leaveEditMode();
611 onFailure: function(transport) {
612 this.options.onFailure(transport);
613 if (this.oldInnerHTML) {
614 this.element.innerHTML = this.oldInnerHTML;
615 this.oldInnerHTML = null;
619 onSubmit: function() {
620 // onLoading resets these so we need to save them away for the Ajax call
621 var form = this.form;
622 var value = this.editField.value;
624 // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
625 // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
626 // to be displayed indefinitely
631 success: this.element,
632 // don't update on failure (this could be an option)
637 parameters: this.options.callback(form, value),
638 onComplete: this.onComplete.bind(this),
639 onFailure: this.onFailure.bind(this)
640 }, this.options.ajaxOptions)
642 // stop the event to avoid a page refresh in Safari
643 if (arguments.length > 1) {
644 Event.stop(arguments[0]);
648 onLoading: function() {
654 showSaving: function() {
655 this.oldInnerHTML = this.element.innerHTML;
656 this.element.innerHTML = this.options.savingText;
657 Element.addClassName(this.element, this.options.savingClassName);
658 this.element.style.backgroundColor = this.originalBackground;
659 Element.show(this.element);
661 removeForm: function() {
663 if (this.form.parentNode) Element.remove(this.form);
667 enterHover: function() {
668 if (this.saving) return;
669 this.element.style.backgroundColor = this.options.highlightcolor;
671 this.effect.cancel();
673 Element.addClassName(this.element, this.options.hoverClassName)
675 leaveHover: function() {
676 if (this.options.backgroundColor) {
677 this.element.style.backgroundColor = this.oldBackground;
679 Element.removeClassName(this.element, this.options.hoverClassName)
680 if (this.saving) return;
681 this.effect = new Effect.Highlight(this.element, {
682 startcolor: this.options.highlightcolor,
683 endcolor: this.options.highlightendcolor,
684 restorecolor: this.originalBackground
687 leaveEditMode: function() {
688 Element.removeClassName(this.element, this.options.savingClassName);
691 this.element.style.backgroundColor = this.originalBackground;
692 Element.show(this.element);
693 if (this.options.externalControl) {
694 Element.show(this.options.externalControl);
696 this.editing = false;
698 this.oldInnerHTML = null;
699 this.onLeaveEditMode();
701 onComplete: function(transport) {
702 this.leaveEditMode();
703 this.options.onComplete.bind(this)(transport, this.element);
705 onEnterEditMode: function() {},
706 onLeaveEditMode: function() {},
707 dispose: function() {
708 if (this.oldInnerHTML) {
709 this.element.innerHTML = this.oldInnerHTML;
711 this.leaveEditMode();
712 Event.stopObserving(this.element, 'click', this.onclickListener);
713 Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
714 Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
715 if (this.options.externalControl) {
716 Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
717 Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
718 Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);