Merge pull request #1967 from solgenomics/topic/trait_props
[sgn.git] / js / MochiKit / Controls.js
blobb0bc5bfea5fa45f8298b8de690847cd7d80ee026
1 /***
2 Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
3           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
4           (c) 2005 Jon Tirsen (http://www.tirsen.com)
5 Contributors:
6     Richard Livsey
7     Rahul Bhargava
8     Rob Wills
9     Mochi-ized By Thomas Herve (_firstname_@nimail.org)
11 See scriptaculous.js for full license.
13 Autocompleter.Base handles all the autocompletion functionality
14 that's independent of the data source for autocompletion. This
15 includes drawing the autocompletion menu, observing keyboard
16 and mouse events, and similar.
18 Specific autocompleters need to provide, at the very least,
19 a getUpdatedChoices function that will be invoked every time
20 the text inside the monitored textbox changes. This method
21 should get the text for which to provide autocompletion by
22 invoking this.getToken(), NOT by directly accessing
23 this.element.value. This is to allow incremental tokenized
24 autocompletion. Specific auto-completion logic (AJAX, etc)
25 belongs in getUpdatedChoices.
27 Tokenized incremental autocompletion is enabled automatically
28 when an autocompleter is instantiated with the 'tokens' option
29 in the options parameter, e.g.:
30 new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
31 will incrementally autocomplete with a comma as the token.
32 Additionally, ',' in the above example can be replaced with
33 a token array, e.g. { tokens: [',', '\n'] } which
34 enables autocompletion on multiple tokens. This is most
35 useful when one of the tokens is \n (a newline), as it
36 allows smart autocompletion after linebreaks.
38 ***/
40 MochiKit.Base.update(MochiKit.Base, {
41     ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
43 /** @id MochiKit.Base.stripScripts */
44     stripScripts: function (str) {
45         return str.replace(new RegExp(MochiKit.Base.ScriptFragment, 'img'), '');
46     },
48 /** @id MochiKit.Base.stripTags */
49     stripTags: function(str) {
50         return str.replace(/<\/?[^>]+>/gi, '');
51     },
53 /** @id MochiKit.Base.extractScripts */
54     extractScripts: function (str) {
55         var matchAll = new RegExp(MochiKit.Base.ScriptFragment, 'img');
56         var matchOne = new RegExp(MochiKit.Base.ScriptFragment, 'im');
57         return MochiKit.Base.map(function (scriptTag) {
58             return (scriptTag.match(matchOne) || ['', ''])[1];
59         }, str.match(matchAll) || []);
60     },
62 /** @id MochiKit.Base.evalScripts */
63     evalScripts: function (str) {
64         return MochiKit.Base.map(function (scr) {
65             eval(scr);
66         }, MochiKit.Base.extractScripts(str));
67     }
68 });
70 MochiKit.Form = {
72 /** @id MochiKit.Form.serialize */
73     serialize: function (form) {
74         var elements = MochiKit.Form.getElements(form);
75         var queryComponents = [];
77         for (var i = 0; i < elements.length; i++) {
78             var queryComponent = MochiKit.Form.serializeElement(elements[i]);
79             if (queryComponent) {
80                 queryComponents.push(queryComponent);
81             }
82         }
84         return queryComponents.join('&');
85     },
87 /** @id MochiKit.Form.getElements */
88     getElements: function (form) {
89         form = MochiKit.DOM.getElement(form);
90         var elements = [];
92         for (tagName in MochiKit.Form.Serializers) {
93             var tagElements = form.getElementsByTagName(tagName);
94             for (var j = 0; j < tagElements.length; j++) {
95                 elements.push(tagElements[j]);
96             }
97         }
98         return elements;
99     },
101 /** @id MochiKit.Form.serializeElement */
102     serializeElement: function (element) {
103         element = MochiKit.DOM.getElement(element);
104         var method = element.tagName.toLowerCase();
105         var parameter = MochiKit.Form.Serializers[method](element);
107         if (parameter) {
108             var key = encodeURIComponent(parameter[0]);
109             if (key.length === 0) {
110                 return;
111             }
113             if (!(parameter[1] instanceof Array)) {
114                 parameter[1] = [parameter[1]];
115             }
117             return parameter[1].map(function (value) {
118                 return key + '=' + encodeURIComponent(value);
119             }).join('&');
120         }
121     }
124 MochiKit.Form.Serializers = {
126 /** @id MochiKit.Form.Serializers.input */
127     input: function (element) {
128         switch (element.type.toLowerCase()) {
129             case 'submit':
130             case 'hidden':
131             case 'password':
132             case 'text':
133                 return MochiKit.Form.Serializers.textarea(element);
134             case 'checkbox':
135             case 'radio':
136                 return MochiKit.Form.Serializers.inputSelector(element);
137         }
138         return false;
139     },
141 /** @id MochiKit.Form.Serializers.inputSelector */
142     inputSelector: function (element) {
143         if (element.checked) {
144             return [element.name, element.value];
145         }
146     },
148 /** @id MochiKit.Form.Serializers.textarea */
149     textarea: function (element) {
150         return [element.name, element.value];
151     },
153 /** @id MochiKit.Form.Serializers.select */
154     select: function (element) {
155         return MochiKit.Form.Serializers[element.type == 'select-one' ?
156         'selectOne' : 'selectMany'](element);
157     },
159 /** @id MochiKit.Form.Serializers.selectOne */
160     selectOne: function (element) {
161         var value = '', opt, index = element.selectedIndex;
162         if (index >= 0) {
163             opt = element.options[index];
164             value = opt.value;
165             if (!value && !('value' in opt)) {
166                 value = opt.text;
167             }
168         }
169         return [element.name, value];
170     },
172 /** @id MochiKit.Form.Serializers.selectMany */
173     selectMany: function (element) {
174         var value = [];
175         for (var i = 0; i < element.length; i++) {
176             var opt = element.options[i];
177             if (opt.selected) {
178                 var optValue = opt.value;
179                 if (!optValue && !('value' in opt)) {
180                     optValue = opt.text;
181                 }
182                 value.push(optValue);
183             }
184         }
185         return [element.name, value];
186     }
189 /** @id Ajax */
190 var Ajax = {
191     activeRequestCount: 0
194 Ajax.Responders = {
195     responders: [],
197 /** @id Ajax.Responders.register */
198     register: function (responderToAdd) {
199         if (MochiKit.Base.find(this.responders, responderToAdd) == -1) {
200             this.responders.push(responderToAdd);
201         }
202     },
204 /** @id Ajax.Responders.unregister */
205     unregister: function (responderToRemove) {
206         this.responders = this.responders.without(responderToRemove);
207     },
209 /** @id Ajax.Responders.dispatch */
210     dispatch: function (callback, request, transport, json) {
211         MochiKit.Iter.forEach(this.responders, function (responder) {
212             if (responder[callback] &&
213                 typeof(responder[callback]) == 'function') {
214                 try {
215                     responder[callback].apply(responder, [request, transport, json]);
216                 } catch (e) {}
217             }
218         });
219     }
222 Ajax.Responders.register({
224 /** @id Ajax.Responders.onCreate */
225     onCreate: function () {
226         Ajax.activeRequestCount++;
227     },
229 /** @id Ajax.Responders.onComplete */
230     onComplete: function () {
231         Ajax.activeRequestCount--;
232     }
235 /** @id Ajax.Base */
236 Ajax.Base = function () {};
238 Ajax.Base.prototype = {
240 /** @id Ajax.Base.prototype.setOptions */
241     setOptions: function (options) {
242         this.options = {
243             method: 'post',
244             asynchronous: true,
245             parameters:   ''
246         }
247         MochiKit.Base.update(this.options, options || {});
248     },
250 /** @id Ajax.Base.prototype.responseIsSuccess */
251     responseIsSuccess: function () {
252         return this.transport.status == undefined
253             || this.transport.status === 0
254             || (this.transport.status >= 200 && this.transport.status < 300);
255     },
257 /** @id Ajax.Base.prototype.responseIsFailure */
258     responseIsFailure: function () {
259         return !this.responseIsSuccess();
260     }
263 /** @id Ajax.Request */
264 Ajax.Request = function (url, options) {
265     this.__init__(url, options);
268 /** @id Ajax.Events */
269 Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded',
270                        'Interactive', 'Complete'];
272 MochiKit.Base.update(Ajax.Request.prototype, Ajax.Base.prototype);
274 MochiKit.Base.update(Ajax.Request.prototype, {
275     __init__: function (url, options) {
276         this.transport = MochiKit.Async.getXMLHttpRequest();
277         this.setOptions(options);
278         this.request(url);
279     },
281 /** @id Ajax.Request.prototype.request */
282     request: function (url) {
283         var parameters = this.options.parameters || '';
284         if (parameters.length > 0){
285             parameters += '&_=';
286         }
288         try {
289             this.url = url;
290             if (this.options.method == 'get' && parameters.length > 0) {
291                 this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
292             }
293             Ajax.Responders.dispatch('onCreate', this, this.transport);
295             this.transport.open(this.options.method, this.url,
296                                 this.options.asynchronous);
298             if (this.options.asynchronous) {
299                 this.transport.onreadystatechange = MochiKit.Base.bind(this.onStateChange, this);
300                 setTimeout(MochiKit.Base.bind(function () {
301                     this.respondToReadyState(1);
302                 }, this), 10);
303             }
305             this.setRequestHeaders();
307             var body = this.options.postBody ? this.options.postBody : parameters;
308             this.transport.send(this.options.method == 'post' ? body : null);
310         } catch (e) {
311             this.dispatchException(e);
312         }
313     },
315 /** @id Ajax.Request.prototype.setRequestHeaders */
316     setRequestHeaders: function () {
317         var requestHeaders = ['X-Requested-With', 'XMLHttpRequest'];
319         if (this.options.method == 'post') {
320             requestHeaders.push('Content-type',
321                                 'application/x-www-form-urlencoded');
323             /* Force 'Connection: close' for Mozilla browsers to work around
324              * a bug where XMLHttpRequest sends an incorrect Content-length
325              * header. See Mozilla Bugzilla #246651.
326              */
327             if (this.transport.overrideMimeType) {
328                 requestHeaders.push('Connection', 'close');
329             }
330         }
332         if (this.options.requestHeaders) {
333             requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
334         }
336         for (var i = 0; i < requestHeaders.length; i += 2) {
337             this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
338         }
339     },
341 /** @id Ajax.Request.prototype.onStateChange */
342     onStateChange: function () {
343         var readyState = this.transport.readyState;
344         if (readyState != 1) {
345             this.respondToReadyState(this.transport.readyState);
346         }
347     },
349 /** @id Ajax.Request.prototype.header */
350     header: function (name) {
351         try {
352           return this.transport.getResponseHeader(name);
353         } catch (e) {}
354     },
356 /** @id Ajax.Request.prototype.evalJSON */
357     evalJSON: function () {
358         try {
359           return eval(this.header('X-JSON'));
360         } catch (e) {}
361     },
363 /** @id Ajax.Request.prototype.evalResponse */
364     evalResponse: function () {
365         try {
366           return eval(this.transport.responseText);
367         } catch (e) {
368           this.dispatchException(e);
369         }
370     },
372 /** @id Ajax.Request.prototype.respondToReadyState */
373     respondToReadyState: function (readyState) {
374         var event = Ajax.Request.Events[readyState];
375         var transport = this.transport, json = this.evalJSON();
377         if (event == 'Complete') {
378             try {
379                 (this.options['on' + this.transport.status]
380                 || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
381                 || MochiKit.Base.noop)(transport, json);
382             } catch (e) {
383                 this.dispatchException(e);
384             }
386             if ((this.header('Content-type') || '').match(/^text\/javascript/i)) {
387                 this.evalResponse();
388             }
389         }
391         try {
392             (this.options['on' + event] || MochiKit.Base.noop)(transport, json);
393             Ajax.Responders.dispatch('on' + event, this, transport, json);
394         } catch (e) {
395             this.dispatchException(e);
396         }
398         /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
399         if (event == 'Complete') {
400             this.transport.onreadystatechange = MochiKit.Base.noop;
401         }
402     },
404 /** @id Ajax.Request.prototype.dispatchException */
405     dispatchException: function (exception) {
406         (this.options.onException || MochiKit.Base.noop)(this, exception);
407         Ajax.Responders.dispatch('onException', this, exception);
408     }
411 /** @id Ajax.Updater */
412 Ajax.Updater = function (container, url, options) {
413     this.__init__(container, url, options);
416 MochiKit.Base.update(Ajax.Updater.prototype, Ajax.Request.prototype);
418 MochiKit.Base.update(Ajax.Updater.prototype, {
419     __init__: function (container, url, options) {
420         this.containers = {
421             success: container.success ? MochiKit.DOM.getElement(container.success) : MochiKit.DOM.getElement(container),
422             failure: container.failure ? MochiKit.DOM.getElement(container.failure) :
423                 (container.success ? null : MochiKit.DOM.getElement(container))
424         }
425         this.transport = MochiKit.Async.getXMLHttpRequest();
426         this.setOptions(options);
428         var onComplete = this.options.onComplete || MochiKit.Base.noop;
429         this.options.onComplete = MochiKit.Base.bind(function (transport, object) {
430             this.updateContent();
431             onComplete(transport, object);
432         }, this);
434         this.request(url);
435     },
437 /** @id Ajax.Updater.prototype.updateContent */
438     updateContent: function () {
439         var receiver = this.responseIsSuccess() ?
440             this.containers.success : this.containers.failure;
441         var response = this.transport.responseText;
443         if (!this.options.evalScripts) {
444             response = MochiKit.Base.stripScripts(response);
445         }
447         if (receiver) {
448             if (this.options.insertion) {
449                 new this.options.insertion(receiver, response);
450             } else {
451                 MochiKit.DOM.getElement(receiver).innerHTML =
452                     MochiKit.Base.stripScripts(response);
453                 setTimeout(function () {
454                     MochiKit.Base.evalScripts(response);
455                 }, 10);
456             }
457         }
459         if (this.responseIsSuccess()) {
460             if (this.onComplete) {
461                 setTimeout(MochiKit.Base.bind(this.onComplete, this), 10);
462             }
463         }
464     }
467 /** @id Field */
468 var Field = {
470 /** @id clear */
471     clear: function () {
472         for (var i = 0; i < arguments.length; i++) {
473             MochiKit.DOM.getElement(arguments[i]).value = '';
474         }
475     },
477 /** @id focus */
478     focus: function (element) {
479         MochiKit.DOM.getElement(element).focus();
480     },
482 /** @id present */
483     present: function () {
484         for (var i = 0; i < arguments.length; i++) {
485             if (MochiKit.DOM.getElement(arguments[i]).value == '') {
486                 return false;
487             }
488         }
489         return true;
490     },
492 /** @id select */
493     select: function (element) {
494         MochiKit.DOM.getElement(element).select();
495     },
497 /** @id activate */
498     activate: function (element) {
499         element = MochiKit.DOM.getElement(element);
500         element.focus();
501         if (element.select) {
502             element.select();
503         }
504     },
506 /** @id scrollFreeActivate */
507     scrollFreeActivate: function (field) {
508         setTimeout(function () {
509             Field.activate(field);
510         }, 1);
511     }
515 /** @id Autocompleter */
516 var Autocompleter = {};
518 /** @id Autocompleter.Base */
519 Autocompleter.Base = function () {};
521 Autocompleter.Base.prototype = {
523 /** @id Autocompleter.Base.prototype.baseInitialize */
524     baseInitialize: function (element, update, options) {
525         this.element = MochiKit.DOM.getElement(element);
526         this.update = MochiKit.DOM.getElement(update);
527         this.hasFocus = false;
528         this.changed = false;
529         this.active = false;
530         this.index = 0;
531         this.entryCount = 0;
533         if (this.setOptions) {
534             this.setOptions(options);
535         }
536         else {
537             this.options = options || {};
538         }
540         this.options.paramName = this.options.paramName || this.element.name;
541         this.options.tokens = this.options.tokens || [];
542         this.options.frequency = this.options.frequency || 0.4;
543         this.options.minChars = this.options.minChars || 1;
544         this.options.onShow = this.options.onShow || function (element, update) {
545                 if (!update.style.position || update.style.position == 'absolute') {
546                     update.style.position = 'absolute';
547                     MochiKit.Position.clone(element, update, {
548                         setHeight: false,
549                         offsetTop: element.offsetHeight
550                     });
551                 }
552                 MochiKit.Visual.appear(update, {duration:0.15});
553             };
554         this.options.onHide = this.options.onHide || function (element, update) {
555                 MochiKit.Visual.fade(update, {duration: 0.15});
556             };
558         if (typeof(this.options.tokens) == 'string') {
559             this.options.tokens = new Array(this.options.tokens);
560         }
562         this.observer = null;
564         this.element.setAttribute('autocomplete', 'off');
566         MochiKit.Style.hideElement(this.update);
568         MochiKit.Signal.connect(this.element, 'onblur', this, this.onBlur);
569         MochiKit.Signal.connect(this.element, 'onkeypress', this, this.onKeyPress, this);
570     },
572 /** @id Autocompleter.Base.prototype.show */
573     show: function () {
574         if (MochiKit.Style.getStyle(this.update, 'display') == 'none') {
575             this.options.onShow(this.element, this.update);
576         }
577         if (!this.iefix && /MSIE/.test(navigator.userAgent &&
578             (MochiKit.Style.getStyle(this.update, 'position') == 'absolute'))) {
579             new Insertion.After(this.update,
580              '<iframe id="' + this.update.id + '_iefix" '+
581              'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
582              'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
583             this.iefix = MochiKit.DOM.getElement(this.update.id + '_iefix');
584         }
585         if (this.iefix) {
586             setTimeout(MochiKit.Base.bind(this.fixIEOverlapping, this), 50);
587         }
588     },
590 /** @id Autocompleter.Base.prototype.fixIEOverlapping */
591     fixIEOverlapping: function () {
592         MochiKit.Position.clone(this.update, this.iefix);
593         this.iefix.style.zIndex = 1;
594         this.update.style.zIndex = 2;
595         MochiKit.Style.showElement(this.iefix);
596     },
598 /** @id Autocompleter.Base.prototype.hide */
599     hide: function () {
600         this.stopIndicator();
601         if (MochiKit.Style.getStyle(this.update, 'display') != 'none') {
602             this.options.onHide(this.element, this.update);
603         }
604         if (this.iefix) {
605             MochiKit.Style.hideElement(this.iefix);
606         }
607     },
609 /** @id Autocompleter.Base.prototype.startIndicator */
610     startIndicator: function () {
611         if (this.options.indicator) {
612             MochiKit.Style.showElement(this.options.indicator);
613         }
614     },
616 /** @id Autocompleter.Base.prototype.stopIndicator */
617     stopIndicator: function () {
618         if (this.options.indicator) {
619             MochiKit.Style.hideElement(this.options.indicator);
620         }
621     },
623 /** @id Autocompleter.Base.prototype.onKeyPress */
624     onKeyPress: function (event) {
625         if (this.active) {
626             if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") {
627                  this.selectEntry();
628                  MochiKit.Event.stop(event);
629             } else if (event.key().string == "KEY_ESCAPE") {
630                  this.hide();
631                  this.active = false;
632                  MochiKit.Event.stop(event);
633                  return;
634             } else if (event.key().string == "KEY_LEFT" || event.key().string == "KEY_RIGHT") {
635                  return;
636             } else if (event.key().string == "KEY_UP") {
637                  this.markPrevious();
638                  this.render();
639                  if (/AppleWebKit'/.test(navigator.appVersion)) {
640                      event.stop();
641                  }
642                  return;
643             } else if (event.key().string == "KEY_DOWN") {
644                  this.markNext();
645                  this.render();
646                  if (/AppleWebKit'/.test(navigator.appVersion)) {
647                      event.stop();
648                  }
649                  return;
650             }
651         } else {
652             if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") {
653                 return;
654             }
655         }
657         this.changed = true;
658         this.hasFocus = true;
660         if (this.observer) {
661             clearTimeout(this.observer);
662         }
663         this.observer = setTimeout(MochiKit.Base.bind(this.onObserverEvent, this),
664                                    this.options.frequency*1000);
665     },
667 /** @id Autocompleter.Base.prototype.findElement */
668     findElement: function (event, tagName) {
669         var element = event.target;
670         while (element.parentNode && (!element.tagName ||
671               (element.tagName.toUpperCase() != tagName.toUpperCase()))) {
672             element = element.parentNode;
673         }
674         return element;
675     },
677 /** @id Autocompleter.Base.prototype.hover */
678     onHover: function (event) {
679         var element = this.findElement(event, 'LI');
680         if (this.index != element.autocompleteIndex) {
681             this.index = element.autocompleteIndex;
682             this.render();
683         }
684         event.stop();
685     },
687 /** @id Autocompleter.Base.prototype.onClick */
688     onClick: function (event) {
689         var element = this.findElement(event, 'LI');
690         this.index = element.autocompleteIndex;
691         this.selectEntry();
692         this.hide();
693     },
695 /** @id Autocompleter.Base.prototype.onBlur */
696     onBlur: function (event) {
697         // needed to make click events working
698         setTimeout(MochiKit.Base.bind(this.hide, this), 250);
699         this.hasFocus = false;
700         this.active = false;
701     },
703 /** @id Autocompleter.Base.prototype.render */
704     render: function () {
705         if (this.entryCount > 0) {
706             for (var i = 0; i < this.entryCount; i++) {
707                 this.index == i ?
708                     MochiKit.DOM.addElementClass(this.getEntry(i), 'selected') :
709                     MochiKit.DOM.removeElementClass(this.getEntry(i), 'selected');
710             }
711             if (this.hasFocus) {
712                 this.show();
713                 this.active = true;
714             }
715         } else {
716             this.active = false;
717             this.hide();
718         }
719     },
721 /** @id Autocompleter.Base.prototype.markPrevious */
722     markPrevious: function () {
723         if (this.index > 0) {
724             this.index--
725         } else {
726             this.index = this.entryCount-1;
727         }
728     },
730 /** @id Autocompleter.Base.prototype.markNext */
731     markNext: function () {
732         if (this.index < this.entryCount-1) {
733             this.index++
734         } else {
735             this.index = 0;
736         }
737     },
739 /** @id Autocompleter.Base.prototype.getEntry */
740     getEntry: function (index) {
741         return this.update.firstChild.childNodes[index];
742     },
744 /** @id Autocompleter.Base.prototype.getCurrentEntry */
745     getCurrentEntry: function () {
746         return this.getEntry(this.index);
747     },
749 /** @id Autocompleter.Base.prototype.selectEntry */
750     selectEntry: function () {
751         this.active = false;
752         this.updateElement(this.getCurrentEntry());
753     },
755 /** @id Autocompleter.Base.prototype.collectTextNodesIgnoreClass */
756     collectTextNodesIgnoreClass: function (element, className) {
757         return MochiKit.Base.flattenArray(MochiKit.Base.map(function (node) {
758             if (node.nodeType == 3) {
759                 return node.nodeValue;
760             } else if (node.hasChildNodes() && !MochiKit.DOM.hasElementClass(node, className)) {
761                 return this.collectTextNodesIgnoreClass(node, className);
762             }
763             return '';
764         }, MochiKit.DOM.getElement(element).childNodes)).join('');
765     },
767 /** @id Autocompleter.Base.prototype.updateElement */
768     updateElement: function (selectedElement) {
769         if (this.options.updateElement) {
770             this.options.updateElement(selectedElement);
771             return;
772         }
773         var value = '';
774         if (this.options.select) {
775             var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
776             if (nodes.length > 0) {
777                 value = MochiKit.DOM.scrapeText(nodes[0]);
778             }
779         } else {
780             value = this.collectTextNodesIgnoreClass(selectedElement, 'informal');
781         }
782         var lastTokenPos = this.findLastToken();
783         if (lastTokenPos != -1) {
784             var newValue = this.element.value.substr(0, lastTokenPos + 1);
785             var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
786             if (whitespace) {
787                 newValue += whitespace[0];
788             }
789             this.element.value = newValue + value;
790         } else {
791             this.element.value = value;
792         }
793         this.element.focus();
795         if (this.options.afterUpdateElement) {
796             this.options.afterUpdateElement(this.element, selectedElement);
797         }
798     },
800 /** @id Autocompleter.Base.prototype.updateChoices */
801     updateChoices: function (choices) {
802         if (!this.changed && this.hasFocus) {
803             this.update.innerHTML = choices;
804             var d = MochiKit.DOM;
805             d.removeEmptyTextNodes(this.update);
806             d.removeEmptyTextNodes(this.update.firstChild);
808             if (this.update.firstChild && this.update.firstChild.childNodes) {
809                 this.entryCount = this.update.firstChild.childNodes.length;
810                 for (var i = 0; i < this.entryCount; i++) {
811                     var entry = this.getEntry(i);
812                     entry.autocompleteIndex = i;
813                     this.addObservers(entry);
814                 }
815             } else {
816                 this.entryCount = 0;
817             }
819             this.stopIndicator();
821             this.index = 0;
822             this.render();
823         }
824     },
826 /** @id Autocompleter.Base.prototype.addObservers */
827     addObservers: function (element) {
828         MochiKit.Signal.connect(element, 'onmouseover', this, this.onHover);
829         MochiKit.Signal.connect(element, 'onclick', this, this.onClick);
830     },
832 /** @id Autocompleter.Base.prototype.onObserverEvent */
833     onObserverEvent: function () {
834         this.changed = false;
835         if (this.getToken().length >= this.options.minChars) {
836             this.startIndicator();
837             this.getUpdatedChoices();
838         } else {
839             this.active = false;
840             this.hide();
841         }
842     },
844 /** @id Autocompleter.Base.prototype.getToken */
845     getToken: function () {
846         var tokenPos = this.findLastToken();
847         if (tokenPos != -1) {
848             var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
849         } else {
850             var ret = this.element.value;
851         }
852         return /\n/.test(ret) ? '' : ret;
853     },
855 /** @id Autocompleter.Base.prototype.findLastToken */
856     findLastToken: function () {
857         var lastTokenPos = -1;
859         for (var i = 0; i < this.options.tokens.length; i++) {
860             var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
861             if (thisTokenPos > lastTokenPos) {
862                 lastTokenPos = thisTokenPos;
863             }
864         }
865         return lastTokenPos;
866     }
869 /** @id Ajax.Autocompleter */
870 Ajax.Autocompleter = function (element, update, url, options) {
871     this.__init__(element, update, url, options);
874 MochiKit.Base.update(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype);
876 MochiKit.Base.update(Ajax.Autocompleter.prototype, {
877     __init__: function (element, update, url, options) {
878         this.baseInitialize(element, update, options);
879         this.options.asynchronous = true;
880         this.options.onComplete = MochiKit.Base.bind(this.onComplete, this);
881         this.options.defaultParams = this.options.parameters || null;
882         this.url = url;
883     },
885 /** @id Ajax.Autocompleter.prototype.getUpdatedChoices */
886     getUpdatedChoices: function () {
887         var entry = encodeURIComponent(this.options.paramName) + '=' +
888             encodeURIComponent(this.getToken());
890         this.options.parameters = this.options.callback ?
891             this.options.callback(this.element, entry) : entry;
893         if (this.options.defaultParams) {
894             this.options.parameters += '&' + this.options.defaultParams;
895         }
896         new Ajax.Request(this.url, this.options);
897     },
899 /** @id Ajax.Autocompleter.prototype.onComplete */
900     onComplete: function (request) {
901         this.updateChoices(request.responseText);
902     }
905 /***
907 The local array autocompleter. Used when you'd prefer to
908 inject an array of autocompletion options into the page, rather
909 than sending out Ajax queries, which can be quite slow sometimes.
911 The constructor takes four parameters. The first two are, as usual,
912 the id of the monitored textbox, and id of the autocompletion menu.
913 The third is the array you want to autocomplete from, and the fourth
914 is the options block.
916 Extra local autocompletion options:
917 - choices - How many autocompletion choices to offer
919 - partialSearch - If false, the autocompleter will match entered
920                                        text only at the beginning of strings in the
921                                        autocomplete array. Defaults to true, which will
922                                        match text at the beginning of any *word* in the
923                                        strings in the autocomplete array. If you want to
924                                        search anywhere in the string, additionally set
925                                        the option fullSearch to true (default: off).
927 - fullSsearch - Search anywhere in autocomplete array strings.
929 - partialChars - How many characters to enter before triggering
930                                     a partial match (unlike minChars, which defines
931                                     how many characters are required to do any match
932                                     at all). Defaults to 2.
934 - ignoreCase - Whether to ignore case when autocompleting.
935                                 Defaults to true.
937 It's possible to pass in a custom function as the 'selector'
938 option, if you prefer to write your own autocompletion logic.
939 In that case, the other options above will not apply unless
940 you support them.
942 ***/
944 /** @id Autocompleter.Local */
945 Autocompleter.Local = function (element, update, array, options) {
946     this.__init__(element, update, array, options);
949 MochiKit.Base.update(Autocompleter.Local.prototype, Autocompleter.Base.prototype);
951 MochiKit.Base.update(Autocompleter.Local.prototype, {
952     __init__: function (element, update, array, options) {
953         this.baseInitialize(element, update, options);
954         this.options.array = array;
955     },
957 /** @id Autocompleter.Local.prototype.getUpdatedChoices */
958     getUpdatedChoices: function () {
959         this.updateChoices(this.options.selector(this));
960     },
962 /** @id Autocompleter.Local.prototype.setOptions */
963     setOptions: function (options) {
964         this.options = MochiKit.Base.update({
965             choices: 10,
966             partialSearch: true,
967             partialChars: 2,
968             ignoreCase: true,
969             fullSearch: false,
970             selector: function (instance) {
971                 var ret = [];  // Beginning matches
972                 var partial = [];  // Inside matches
973                 var entry = instance.getToken();
974                 var count = 0;
976                 for (var i = 0; i < instance.options.array.length &&
977                     ret.length < instance.options.choices ; i++) {
979                     var elem = instance.options.array[i];
980                     var foundPos = instance.options.ignoreCase ?
981                         elem.toLowerCase().indexOf(entry.toLowerCase()) :
982                         elem.indexOf(entry);
984                     while (foundPos != -1) {
985                         if (foundPos === 0 && elem.length != entry.length) {
986                             ret.push('<li><strong>' + elem.substr(0, entry.length) + '</strong>' +
987                                 elem.substr(entry.length) + '</li>');
988                             break;
989                         } else if (entry.length >= instance.options.partialChars &&
990                             instance.options.partialSearch && foundPos != -1) {
991                             if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos - 1, 1))) {
992                                 partial.push('<li>' + elem.substr(0, foundPos) + '<strong>' +
993                                     elem.substr(foundPos, entry.length) + '</strong>' + elem.substr(
994                                     foundPos + entry.length) + '</li>');
995                                 break;
996                             }
997                         }
999                         foundPos = instance.options.ignoreCase ?
1000                             elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
1001                             elem.indexOf(entry, foundPos + 1);
1003                     }
1004                 }
1005                 if (partial.length) {
1006                     ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
1007                 }
1008                 return '<ul>' + ret.join('') + '</ul>';
1009             }
1010         }, options || {});
1011     }
1014 /***
1016 AJAX in-place editor
1018 see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
1020 Use this if you notice weird scrolling problems on some browsers,
1021 the DOM might be a bit confused when this gets called so do this
1022 waits 1 ms (with setTimeout) until it does the activation
1024 ***/
1026 /** @id Ajax.InPlaceEditor */
1027 Ajax.InPlaceEditor = function (element, url, options) {
1028     this.__init__(element, url, options);
1031 /** @id Ajax.InPlaceEditor.defaultHighlightColor */
1032 Ajax.InPlaceEditor.defaultHighlightColor = '#FFFF99';
1034 Ajax.InPlaceEditor.prototype = {
1035     __init__: function (element, url, options) {
1036         this.url = url;
1037         this.element = MochiKit.DOM.getElement(element);
1039         this.options = MochiKit.Base.update({
1040             okButton: true,
1041             okText: 'ok',
1042             cancelLink: true,
1043             cancelText: 'cancel',
1044             savingText: 'Saving...',
1045             clickToEditText: 'Click to edit',
1046             okText: 'ok',
1047             rows: 1,
1048             onComplete: function (transport, element) {
1049                 new MochiKit.Visual.Highlight(element, {startcolor: this.options.highlightcolor});
1050             },
1051             onFailure: function (transport) {
1052                 alert('Error communicating with the server: ' + MochiKit.Base.stripTags(transport.responseText));
1053             },
1054             callback: function (form) {
1055                 return MochiKit.DOM.formContents(form);
1056             },
1057             handleLineBreaks: true,
1058             loadingText: 'Loading...',
1059             savingClassName: 'inplaceeditor-saving',
1060             loadingClassName: 'inplaceeditor-loading',
1061             formClassName: 'inplaceeditor-form',
1062             highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
1063             highlightendcolor: '#FFFFFF',
1064             externalControl: null,
1065             submitOnBlur: false,
1066             ajaxOptions: {}
1067         }, options || {});
1069         if (!this.options.formId && this.element.id) {
1070             this.options.formId = this.element.id + '-inplaceeditor';
1071             if (MochiKit.DOM.getElement(this.options.formId)) {
1072                 // there's already a form with that name, don't specify an id
1073                 this.options.formId = null;
1074             }
1075         }
1077         if (this.options.externalControl) {
1078             this.options.externalControl = MochiKit.DOM.getElement(this.options.externalControl);
1079         }
1081         this.originalBackground = MochiKit.Style.getStyle(this.element, 'background-color');
1082         if (!this.originalBackground) {
1083             this.originalBackground = 'transparent';
1084         }
1086         this.element.title = this.options.clickToEditText;
1088         this.onclickListener = MochiKit.Signal.connect(this.element, 'onclick', this, this.enterEditMode);
1089         this.mouseoverListener = MochiKit.Signal.connect(this.element, 'onmouseover', this, this.enterHover);
1090         this.mouseoutListener = MochiKit.Signal.connect(this.element, 'onmouseout', this, this.leaveHover);
1091         if (this.options.externalControl) {
1092             this.onclickListenerExternal = MochiKit.Signal.connect(this.options.externalControl,
1093                 'onclick', this, this.enterEditMode);
1094             this.mouseoverListenerExternal = MochiKit.Signal.connect(this.options.externalControl,
1095                 'onmouseover', this, this.enterHover);
1096             this.mouseoutListenerExternal = MochiKit.Signal.connect(this.options.externalControl,
1097                 'onmouseout', this, this.leaveHover);
1098         }
1099     },
1101 /** @id Ajax.InPlaceEditor.prototype.enterEditMode */
1102     enterEditMode: function (evt) {
1103         if (this.saving) {
1104             return;
1105         }
1106         if (this.editing) {
1107             return;
1108         }
1109         this.editing = true;
1110         this.onEnterEditMode();
1111         if (this.options.externalControl) {
1112             MochiKit.Style.hideElement(this.options.externalControl);
1113         }
1114         MochiKit.Style.hideElement(this.element);
1115         this.createForm();
1116         this.element.parentNode.insertBefore(this.form, this.element);
1117         Field.scrollFreeActivate(this.editField);
1118         // stop the event to avoid a page refresh in Safari
1119         if (evt) {
1120             evt.stop();
1121         }
1122         return false;
1123     },
1125 /** @id Ajax.InPlaceEditor.prototype.createForm */
1126     createForm: function () {
1127         this.form = document.createElement('form');
1128         this.form.id = this.options.formId;
1129         MochiKit.DOM.addElementClass(this.form, this.options.formClassName)
1130         this.form.onsubmit = MochiKit.Base.bind(this.onSubmit, this);
1132         this.createEditField();
1134         if (this.options.textarea) {
1135             var br = document.createElement('br');
1136             this.form.appendChild(br);
1137         }
1139         if (this.options.okButton) {
1140             okButton = document.createElement('input');
1141             okButton.type = 'submit';
1142             okButton.value = this.options.okText;
1143             this.form.appendChild(okButton);
1144         }
1146         if (this.options.cancelLink) {
1147             cancelLink = document.createElement('a');
1148             cancelLink.href = '#';
1149             cancelLink.appendChild(document.createTextNode(this.options.cancelText));
1150             cancelLink.onclick = MochiKit.Base.bind(this.onclickCancel, this);
1151             this.form.appendChild(cancelLink);
1152         }
1153     },
1155 /** @id Ajax.InPlaceEditor.prototype.hasHTMLLineBreaks */
1156     hasHTMLLineBreaks: function (string) {
1157         if (!this.options.handleLineBreaks) {
1158             return false;
1159         }
1160         return string.match(/<br/i) || string.match(/<p>/i);
1161     },
1163 /** @id Ajax.InPlaceEditor.prototype.convertHTMLLineBreaks */
1164     convertHTMLLineBreaks: function (string) {
1165         return string.replace(/<br>/gi, '\n').replace(/<br\/>/gi, '\n').replace(/<\/p>/gi, '\n').replace(/<p>/gi, '');
1166     },
1168 /** @id Ajax.InPlaceEditor.prototype.createEditField */
1169     createEditField: function () {
1170         var text;
1171         if (this.options.loadTextURL) {
1172             text = this.options.loadingText;
1173         } else {
1174             text = this.getText();
1175         }
1177         var obj = this;
1179         if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
1180             this.options.textarea = false;
1181             var textField = document.createElement('input');
1182             textField.obj = this;
1183             textField.type = 'text';
1184             textField.name = 'value';
1185             textField.value = text;
1186             textField.style.backgroundColor = this.options.highlightcolor;
1187             var size = this.options.size || this.options.cols || 0;
1188             if (size !== 0) {
1189                 textField.size = size;
1190             }
1191             if (this.options.submitOnBlur) {
1192                 textField.onblur = MochiKit.Base.bind(this.onSubmit, this);
1193             }
1194             this.editField = textField;
1195         } else {
1196             this.options.textarea = true;
1197             var textArea = document.createElement('textarea');
1198             textArea.obj = this;
1199             textArea.name = 'value';
1200             textArea.value = this.convertHTMLLineBreaks(text);
1201             textArea.rows = this.options.rows;
1202             textArea.cols = this.options.cols || 40;
1203             if (this.options.submitOnBlur) {
1204                 textArea.onblur = MochiKit.Base.bind(this.onSubmit, this);
1205             }
1206             this.editField = textArea;
1207         }
1209         if (this.options.loadTextURL) {
1210             this.loadExternalText();
1211         }
1212         this.form.appendChild(this.editField);
1213     },
1215 /** @id Ajax.InPlaceEditor.prototype.getText */
1216     getText: function () {
1217         return this.element.innerHTML;
1218     },
1220 /** @id Ajax.InPlaceEditor.prototype.loadExternalText */
1221     loadExternalText: function () {
1222         MochiKit.DOM.addElementClass(this.form, this.options.loadingClassName);
1223         this.editField.disabled = true;
1224         new Ajax.Request(
1225             this.options.loadTextURL,
1226             MochiKit.Base.update({
1227                 asynchronous: true,
1228                 onComplete: MochiKit.Base.bind(this.onLoadedExternalText, this)
1229             }, this.options.ajaxOptions)
1230         );
1231     },
1233 /** @id Ajax.InPlaceEditor.prototype.onLoadedExternalText */
1234     onLoadedExternalText: function (transport) {
1235         MochiKit.DOM.removeElementClass(this.form, this.options.loadingClassName);
1236         this.editField.disabled = false;
1237         this.editField.value = MochiKit.Base.stripTags(transport);
1238     },
1240 /** @id Ajax.InPlaceEditor.prototype.onclickCancel */
1241     onclickCancel: function () {
1242         this.onComplete();
1243         this.leaveEditMode();
1244         return false;
1245     },
1247 /** @id Ajax.InPlaceEditor.prototype.onFailure */
1248     onFailure: function (transport) {
1249         this.options.onFailure(transport);
1250         if (this.oldInnerHTML) {
1251             this.element.innerHTML = this.oldInnerHTML;
1252             this.oldInnerHTML = null;
1253         }
1254         return false;
1255     },
1257 /** @id Ajax.InPlaceEditor.prototype.onSubmit */
1258     onSubmit: function () {
1259         // onLoading resets these so we need to save them away for the Ajax call
1260         var form = this.form;
1261         var value = this.editField.value;
1263         // do this first, sometimes the ajax call returns before we get a
1264         // chance to switch on Saving which means this will actually switch on
1265         // Saving *after* we have left edit mode causing Saving to be
1266         // displayed indefinitely
1267         this.onLoading();
1269         new Ajax.Updater(
1270             {
1271                 success: this.element,
1272                  // dont update on failure (this could be an option)
1273                 failure: null
1274             },
1275             this.url,
1276             MochiKit.Base.update({
1277                 parameters: this.options.callback(form, value),
1278                 onComplete: MochiKit.Base.bind(this.onComplete, this),
1279                 onFailure: MochiKit.Base.bind(this.onFailure, this)
1280             }, this.options.ajaxOptions)
1281         );
1282         // stop the event to avoid a page refresh in Safari
1283         if (arguments.length > 1) {
1284             arguments[0].stop();
1285         }
1286         return false;
1287     },
1289 /** @id Ajax.InPlaceEditor.prototype.onLoading */
1290     onLoading: function () {
1291         this.saving = true;
1292         this.removeForm();
1293         this.leaveHover();
1294         this.showSaving();
1295     },
1297 /** @id Ajax.InPlaceEditor.prototype.onSaving */
1298     showSaving: function () {
1299         this.oldInnerHTML = this.element.innerHTML;
1300         this.element.innerHTML = this.options.savingText;
1301         MochiKit.DOM.addElementClass(this.element, this.options.savingClassName);
1302         this.element.style.backgroundColor = this.originalBackground;
1303         MochiKit.Style.showElement(this.element);
1304     },
1306 /** @id Ajax.InPlaceEditor.prototype.removeForm */
1307     removeForm: function () {
1308         if (this.form) {
1309             if (this.form.parentNode) {
1310                 MochiKit.DOM.removeElement(this.form);
1311             }
1312             this.form = null;
1313         }
1314     },
1316 /** @id Ajax.InPlaceEditor.prototype.enterHover */
1317     enterHover: function () {
1318         if (this.saving) {
1319             return;
1320         }
1321         this.element.style.backgroundColor = this.options.highlightcolor;
1322         if (this.effect) {
1323             this.effect.cancel();
1324         }
1325         MochiKit.DOM.addElementClass(this.element, this.options.hoverClassName)
1326     },
1328 /** @id Ajax.InPlaceEditor.prototype.leaveHover */
1329     leaveHover: function () {
1330         if (this.options.backgroundColor) {
1331             this.element.style.backgroundColor = this.oldBackground;
1332         }
1333         MochiKit.DOM.removeElementClass(this.element, this.options.hoverClassName)
1334         if (this.saving) {
1335             return;
1336         }
1337         this.effect = new MochiKit.Visual.Highlight(this.element, {
1338             startcolor: this.options.highlightcolor,
1339             endcolor: this.options.highlightendcolor,
1340             restorecolor: this.originalBackground
1341         });
1342     },
1344 /** @id Ajax.InPlaceEditor.prototype.leaveEditMode */
1345     leaveEditMode: function () {
1346         MochiKit.DOM.removeElementClass(this.element, this.options.savingClassName);
1347         this.removeForm();
1348         this.leaveHover();
1349         this.element.style.backgroundColor = this.originalBackground;
1350         MochiKit.Style.showElement(this.element);
1351         if (this.options.externalControl) {
1352             MochiKit.Style.showElement(this.options.externalControl);
1353         }
1354         this.editing = false;
1355         this.saving = false;
1356         this.oldInnerHTML = null;
1357         this.onLeaveEditMode();
1358     },
1360 /** @id Ajax.InPlaceEditor.prototype.onComplete */
1361     onComplete: function (transport) {
1362         this.leaveEditMode();
1363         MochiKit.Base.bind(this.options.onComplete, this)(transport, this.element);
1364     },
1366 /** @id Ajax.InPlaceEditor.prototype.onEnterEditMode */
1367     onEnterEditMode: function () {},
1369 /** @id Ajax.InPlaceEditor.prototype.onLeaveEditMode */
1370     onLeaveEditMode: function () {},
1372  /** @id Ajax.InPlaceEditor.prototype.dispose */
1373     dispose: function () {
1374         if (this.oldInnerHTML) {
1375             this.element.innerHTML = this.oldInnerHTML;
1376         }
1377         this.leaveEditMode();
1378         MochiKit.Signal.disconnect(this.onclickListener);
1379         MochiKit.Signal.disconnect(this.mouseoverListener);
1380         MochiKit.Signal.disconnect(this.mouseoutListener);
1381         if (this.options.externalControl) {
1382             MochiKit.Signal.disconnect(this.onclickListenerExternal);
1383             MochiKit.Signal.disconnect(this.mouseoverListenerExternal);
1384             MochiKit.Signal.disconnect(this.mouseoutListenerExternal);
1385         }
1386     }