Creation of new VotingBooth rails application to expose the
[selectricity-live] / public / javascripts / controls.js
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
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; 
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 && (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');
89     }
90     if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
91   },
92   
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);
98   },
99
100   hide: function() {
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);
104   },
105
106   startIndicator: function() {
107     if(this.options.indicator) Element.show(this.options.indicator);
108   },
109
110   stopIndicator: function() {
111     if(this.options.indicator) Element.hide(this.options.indicator);
112   },
113
114   onKeyPress: function(event) {
115     if(this.active)
116       switch(event.keyCode) {
117        case Event.KEY_TAB:
118        case Event.KEY_RETURN:
119          this.selectEntry();
120          Event.stop(event);
121        case Event.KEY_ESC:
122          this.hide();
123          this.active = false;
124          Event.stop(event);
125          return;
126        case Event.KEY_LEFT:
127        case Event.KEY_RIGHT:
128          return;
129        case Event.KEY_UP:
130          this.markPrevious();
131          this.render();
132          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
133          return;
134        case Event.KEY_DOWN:
135          this.markNext();
136          this.render();
137          if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
138          return;
139       }
140      else 
141       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
142         return;
143
144     this.changed = true;
145     this.hasFocus = true;
146
147     if(this.observer) clearTimeout(this.observer);
148       this.observer = 
149         setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
150   },
151
152   onHover: function(event) {
153     var element = Event.findElement(event, 'LI');
154     if(this.index != element.autocompleteIndex) 
155     {
156         this.index = element.autocompleteIndex;
157         this.render();
158     }
159     Event.stop(event);
160   },
161   
162   onClick: function(event) {
163     var element = Event.findElement(event, 'LI');
164     this.index = element.autocompleteIndex;
165     this.selectEntry();
166     this.hide();
167   },
168   
169   onBlur: function(event) {
170     // needed to make click events working
171     setTimeout(this.hide.bind(this), 250);
172     this.hasFocus = false;
173     this.active = false;     
174   }, 
175   
176   render: function() {
177     if(this.entryCount > 0) {
178       for (var i = 0; i < this.entryCount; i++)
179         this.index==i ? 
180           Element.addClassName(this.getEntry(i),"selected") : 
181           Element.removeClassName(this.getEntry(i),"selected");
182         
183       if(this.hasFocus) { 
184         this.show();
185         this.active = true;
186       }
187     } else {
188       this.active = false;
189       this.hide();
190     }
191   },
192   
193   markPrevious: function() {
194     if(this.index > 0) this.index--
195       else this.index = this.entryCount-1;
196   },
197   
198   markNext: function() {
199     if(this.index < this.entryCount-1) this.index++
200       else this.index = 0;
201   },
202   
203   getEntry: function(index) {
204     return this.update.firstChild.childNodes[index];
205   },
206   
207   getCurrentEntry: function() {
208     return this.getEntry(this.index);
209   },
210   
211   selectEntry: function() {
212     this.active = false;
213     this.updateElement(this.getCurrentEntry());
214   },
215
216   updateElement: function(selectedElement) {
217     if (this.options.updateElement) {
218       this.options.updateElement(selectedElement);
219       return;
220     }
221
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+/);
227       if (whitespace)
228         newValue += whitespace[0];
229       this.element.value = newValue + value;
230     } else {
231       this.element.value = value;
232     }
233     this.element.focus();
234     
235     if (this.options.afterUpdateElement)
236       this.options.afterUpdateElement(this.element, selectedElement);
237   },
238
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);
244
245       if(this.update.firstChild && this.update.firstChild.childNodes) {
246         this.entryCount = 
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);
252         }
253       } else { 
254         this.entryCount = 0;
255       }
256
257       this.stopIndicator();
258
259       this.index = 0;
260       this.render();
261     }
262   },
263
264   addObservers: function(element) {
265     Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
266     Event.observe(element, "click", this.onClick.bindAsEventListener(this));
267   },
268
269   onObserverEvent: function() {
270     this.changed = false;   
271     if(this.getToken().length>=this.options.minChars) {
272       this.startIndicator();
273       this.getUpdatedChoices();
274     } else {
275       this.active = false;
276       this.hide();
277     }
278   },
279
280   getToken: function() {
281     var tokenPos = this.findLastToken();
282     if (tokenPos != -1)
283       var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
284     else
285       var ret = this.element.value;
286
287     return /\n/.test(ret) ? '' : ret;
288   },
289
290   findLastToken: function() {
291     var lastTokenPos = -1;
292
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;
297     }
298     return lastTokenPos;
299   }
300 }
301
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;
309     this.url                   = url;
310   },
311
312   getUpdatedChoices: function() {
313     entry = encodeURIComponent(this.options.paramName) + '=' + 
314       encodeURIComponent(this.getToken());
315
316     this.options.parameters = this.options.callback ?
317       this.options.callback(this.element, entry) : entry;
318
319     if(this.options.defaultParams) 
320       this.options.parameters += '&' + this.options.defaultParams;
321
322     new Ajax.Request(this.url, this.options);
323   },
324
325   onComplete: function(request) {
326     this.updateChoices(request.responseText);
327   }
328
329 });
330
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.
334 //
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.
339 //
340 // Extra local autocompletion options:
341 // - choices - How many autocompletion choices to offer
342 //
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).
350 //
351 // - fullSsearch - Search anywhere in autocomplete array strings.
352 //
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.
357 //
358 // - ignoreCase - Whether to ignore case when autocompleting.
359 //                 Defaults to true.
360 //
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
364 // you support them.
365
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;
371   },
372
373   getUpdatedChoices: function() {
374     this.updateChoices(this.options.selector(this));
375   },
376
377   setOptions: function(options) {
378     this.options = Object.extend({
379       choices: 10,
380       partialSearch: true,
381       partialChars: 2,
382       ignoreCase: true,
383       fullSearch: false,
384       selector: function(instance) {
385         var ret       = []; // Beginning matches
386         var partial   = []; // Inside matches
387         var entry     = instance.getToken();
388         var count     = 0;
389
390         for (var i = 0; i < instance.options.array.length &&  
391           ret.length < instance.options.choices ; i++) { 
392
393           var elem = instance.options.array[i];
394           var foundPos = instance.options.ignoreCase ? 
395             elem.toLowerCase().indexOf(entry.toLowerCase()) : 
396             elem.indexOf(entry);
397
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>");
402               break;
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>");
409                 break;
410               }
411             }
412
413             foundPos = instance.options.ignoreCase ? 
414               elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
415               elem.indexOf(entry, foundPos + 1);
416
417           }
418         }
419         if (partial.length)
420           ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
421         return "<ul>" + ret.join('') + "</ul>";
422       }
423     }, options || {});
424   }
425 });
426
427 // AJAX in-place editor
428 //
429 // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
430
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);
437   }, 1);
438 }
439
440 Ajax.InPlaceEditor = Class.create();
441 Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
442 Ajax.InPlaceEditor.prototype = {
443   initialize: function(element, url, options) {
444     this.url = url;
445     this.element = $(element);
446
447     this.options = Object.extend({
448       okText: "ok",
449       cancelText: "cancel",
450       savingText: "Saving...",
451       clickToEditText: "Click to edit",
452       okText: "ok",
453       rows: 1,
454       onComplete: function(transport, element) {
455         new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
456       },
457       onFailure: function(transport) {
458         alert("Error communicating with the server: " + transport.responseText.stripTags());
459       },
460       callback: function(form) {
461         return Form.serialize(form);
462       },
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,
471       ajaxOptions: {}
472     }, options || {});
473
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;
479       }
480     }
481     
482     if (this.options.externalControl) {
483       this.options.externalControl = $(this.options.externalControl);
484     }
485     
486     this.originalBackground = Element.getStyle(this.element, 'background-color');
487     if (!this.originalBackground) {
488       this.originalBackground = "transparent";
489     }
490     
491     this.element.title = this.options.clickToEditText;
492     
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);
503     }
504   },
505   enterEditMode: function(evt) {
506     if (this.saving) return;
507     if (this.editing) return;
508     this.editing = true;
509     this.onEnterEditMode();
510     if (this.options.externalControl) {
511       Element.hide(this.options.externalControl);
512     }
513     Element.hide(this.element);
514     this.createForm();
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
518     if (evt) {
519       Event.stop(evt);
520     }
521     return false;
522   },
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);
528
529     this.createEditField();
530
531     if (this.options.textarea) {
532       var br = document.createElement("br");
533       this.form.appendChild(br);
534     }
535
536     okButton = document.createElement("input");
537     okButton.type = "submit";
538     okButton.value = this.options.okText;
539     this.form.appendChild(okButton);
540
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);
546   },
547   hasHTMLLineBreaks: function(string) {
548     if (!this.options.handleLineBreaks) return false;
549     return string.match(/<br/i) || string.match(/<p>/i);
550   },
551   convertHTMLLineBreaks: function(string) {
552     return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
553   },
554   createEditField: function() {
555     var text;
556     if(this.options.loadTextURL) {
557       text = this.options.loadingText;
558     } else {
559       text = this.getText();
560     }
561     
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;
572     } else {
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;
580     }
581     
582     if(this.options.loadTextURL) {
583       this.loadExternalText();
584     }
585     this.form.appendChild(this.editField);
586   },
587   getText: function() {
588     return this.element.innerHTML;
589   },
590   loadExternalText: function() {
591     Element.addClassName(this.form, this.options.loadingClassName);
592     this.editField.disabled = true;
593     new Ajax.Request(
594       this.options.loadTextURL,
595       Object.extend({
596         asynchronous: true,
597         onComplete: this.onLoadedExternalText.bind(this)
598       }, this.options.ajaxOptions)
599     );
600   },
601   onLoadedExternalText: function(transport) {
602     Element.removeClassName(this.form, this.options.loadingClassName);
603     this.editField.disabled = false;
604     this.editField.value = transport.responseText.stripTags();
605   },
606   onclickCancel: function() {
607     this.onComplete();
608     this.leaveEditMode();
609     return false;
610   },
611   onFailure: function(transport) {
612     this.options.onFailure(transport);
613     if (this.oldInnerHTML) {
614       this.element.innerHTML = this.oldInnerHTML;
615       this.oldInnerHTML = null;
616     }
617     return false;
618   },
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;
623     
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
627     this.onLoading();
628     
629     new Ajax.Updater(
630       { 
631         success: this.element,
632          // don't update on failure (this could be an option)
633         failure: null
634       },
635       this.url,
636       Object.extend({
637         parameters: this.options.callback(form, value),
638         onComplete: this.onComplete.bind(this),
639         onFailure: this.onFailure.bind(this)
640       }, this.options.ajaxOptions)
641     );
642     // stop the event to avoid a page refresh in Safari
643     if (arguments.length > 1) {
644       Event.stop(arguments[0]);
645     }
646     return false;
647   },
648   onLoading: function() {
649     this.saving = true;
650     this.removeForm();
651     this.leaveHover();
652     this.showSaving();
653   },
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);
660   },
661   removeForm: function() {
662     if(this.form) {
663       if (this.form.parentNode) Element.remove(this.form);
664       this.form = null;
665     }
666   },
667   enterHover: function() {
668     if (this.saving) return;
669     this.element.style.backgroundColor = this.options.highlightcolor;
670     if (this.effect) {
671       this.effect.cancel();
672     }
673     Element.addClassName(this.element, this.options.hoverClassName)
674   },
675   leaveHover: function() {
676     if (this.options.backgroundColor) {
677       this.element.style.backgroundColor = this.oldBackground;
678     }
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
685     });
686   },
687   leaveEditMode: function() {
688     Element.removeClassName(this.element, this.options.savingClassName);
689     this.removeForm();
690     this.leaveHover();
691     this.element.style.backgroundColor = this.originalBackground;
692     Element.show(this.element);
693     if (this.options.externalControl) {
694       Element.show(this.options.externalControl);
695     }
696     this.editing = false;
697     this.saving = false;
698     this.oldInnerHTML = null;
699     this.onLeaveEditMode();
700   },
701   onComplete: function(transport) {
702     this.leaveEditMode();
703     this.options.onComplete.bind(this)(transport, this.element);
704   },
705   onEnterEditMode: function() {},
706   onLeaveEditMode: function() {},
707   dispose: function() {
708     if (this.oldInnerHTML) {
709       this.element.innerHTML = this.oldInnerHTML;
710     }
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);
719     }
720   }
721 };

Benjamin Mako Hill || Want to submit a patch?