Merge pull request #967 from solgenomics/topic/expression_atlas_export
[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'), '');
48 /** @id MochiKit.Base.stripTags */
49 stripTags: function(str) {
50 return str.replace(/<\/?[^>]+>/gi, '');
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) || []);
62 /** @id MochiKit.Base.evalScripts */
63 evalScripts: function (str) {
64 return MochiKit.Base.map(function (scr) {
65 eval(scr);
66 }, MochiKit.Base.extractScripts(str));
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);
84 return queryComponents.join('&');
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]);
98 return elements;
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;
113 if (!(parameter[1] instanceof Array)) {
114 parameter[1] = [parameter[1]];
117 return parameter[1].map(function (value) {
118 return key + '=' + encodeURIComponent(value);
119 }).join('&');
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);
138 return false;
141 /** @id MochiKit.Form.Serializers.inputSelector */
142 inputSelector: function (element) {
143 if (element.checked) {
144 return [element.name, element.value];
148 /** @id MochiKit.Form.Serializers.textarea */
149 textarea: function (element) {
150 return [element.name, element.value];
153 /** @id MochiKit.Form.Serializers.select */
154 select: function (element) {
155 return MochiKit.Form.Serializers[element.type == 'select-one' ?
156 'selectOne' : 'selectMany'](element);
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;
169 return [element.name, value];
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;
182 value.push(optValue);
185 return [element.name, value];
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);
204 /** @id Ajax.Responders.unregister */
205 unregister: function (responderToRemove) {
206 this.responders = this.responders.without(responderToRemove);
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) {}
222 Ajax.Responders.register({
224 /** @id Ajax.Responders.onCreate */
225 onCreate: function () {
226 Ajax.activeRequestCount++;
229 /** @id Ajax.Responders.onComplete */
230 onComplete: function () {
231 Ajax.activeRequestCount--;
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: ''
247 MochiKit.Base.update(this.options, options || {});
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);
257 /** @id Ajax.Base.prototype.responseIsFailure */
258 responseIsFailure: function () {
259 return !this.responseIsSuccess();
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);
281 /** @id Ajax.Request.prototype.request */
282 request: function (url) {
283 var parameters = this.options.parameters || '';
284 if (parameters.length > 0){
285 parameters += '&_=';
288 try {
289 this.url = url;
290 if (this.options.method == 'get' && parameters.length > 0) {
291 this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
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);
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);
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.
327 if (this.transport.overrideMimeType) {
328 requestHeaders.push('Connection', 'close');
332 if (this.options.requestHeaders) {
333 requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
336 for (var i = 0; i < requestHeaders.length; i += 2) {
337 this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
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);
349 /** @id Ajax.Request.prototype.header */
350 header: function (name) {
351 try {
352 return this.transport.getResponseHeader(name);
353 } catch (e) {}
356 /** @id Ajax.Request.prototype.evalJSON */
357 evalJSON: function () {
358 try {
359 return eval(this.header('X-JSON'));
360 } catch (e) {}
363 /** @id Ajax.Request.prototype.evalResponse */
364 evalResponse: function () {
365 try {
366 return eval(this.transport.responseText);
367 } catch (e) {
368 this.dispatchException(e);
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);
386 if ((this.header('Content-type') || '').match(/^text\/javascript/i)) {
387 this.evalResponse();
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);
398 /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
399 if (event == 'Complete') {
400 this.transport.onreadystatechange = MochiKit.Base.noop;
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);
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))
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);
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);
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);
459 if (this.responseIsSuccess()) {
460 if (this.onComplete) {
461 setTimeout(MochiKit.Base.bind(this.onComplete, this), 10);
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 = '';
477 /** @id focus */
478 focus: function (element) {
479 MochiKit.DOM.getElement(element).focus();
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;
489 return true;
492 /** @id select */
493 select: function (element) {
494 MochiKit.DOM.getElement(element).select();
497 /** @id activate */
498 activate: function (element) {
499 element = MochiKit.DOM.getElement(element);
500 element.focus();
501 if (element.select) {
502 element.select();
506 /** @id scrollFreeActivate */
507 scrollFreeActivate: function (field) {
508 setTimeout(function () {
509 Field.activate(field);
510 }, 1);
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);
536 else {
537 this.options = options || {};
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
552 MochiKit.Visual.appear(update, {duration:0.15});
554 this.options.onHide = this.options.onHide || function (element, update) {
555 MochiKit.Visual.fade(update, {duration: 0.15});
558 if (typeof(this.options.tokens) == 'string') {
559 this.options.tokens = new Array(this.options.tokens);
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);
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);
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');
585 if (this.iefix) {
586 setTimeout(MochiKit.Base.bind(this.fixIEOverlapping, this), 50);
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);
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);
604 if (this.iefix) {
605 MochiKit.Style.hideElement(this.iefix);
609 /** @id Autocompleter.Base.prototype.startIndicator */
610 startIndicator: function () {
611 if (this.options.indicator) {
612 MochiKit.Style.showElement(this.options.indicator);
616 /** @id Autocompleter.Base.prototype.stopIndicator */
617 stopIndicator: function () {
618 if (this.options.indicator) {
619 MochiKit.Style.hideElement(this.options.indicator);
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();
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();
649 return;
651 } else {
652 if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") {
653 return;
657 this.changed = true;
658 this.hasFocus = true;
660 if (this.observer) {
661 clearTimeout(this.observer);
663 this.observer = setTimeout(MochiKit.Base.bind(this.onObserverEvent, this),
664 this.options.frequency*1000);
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;
674 return element;
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();
684 event.stop();
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();
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;
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');
711 if (this.hasFocus) {
712 this.show();
713 this.active = true;
715 } else {
716 this.active = false;
717 this.hide();
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;
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;
739 /** @id Autocompleter.Base.prototype.getEntry */
740 getEntry: function (index) {
741 return this.update.firstChild.childNodes[index];
744 /** @id Autocompleter.Base.prototype.getCurrentEntry */
745 getCurrentEntry: function () {
746 return this.getEntry(this.index);
749 /** @id Autocompleter.Base.prototype.selectEntry */
750 selectEntry: function () {
751 this.active = false;
752 this.updateElement(this.getCurrentEntry());
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);
763 return '';
764 }, MochiKit.DOM.getElement(element).childNodes)).join('');
767 /** @id Autocompleter.Base.prototype.updateElement */
768 updateElement: function (selectedElement) {
769 if (this.options.updateElement) {
770 this.options.updateElement(selectedElement);
771 return;
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]);
779 } else {
780 value = this.collectTextNodesIgnoreClass(selectedElement, 'informal');
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];
789 this.element.value = newValue + value;
790 } else {
791 this.element.value = value;
793 this.element.focus();
795 if (this.options.afterUpdateElement) {
796 this.options.afterUpdateElement(this.element, selectedElement);
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);
815 } else {
816 this.entryCount = 0;
819 this.stopIndicator();
821 this.index = 0;
822 this.render();
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);
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();
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;
852 return /\n/.test(ret) ? '' : ret;
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;
865 return lastTokenPos;
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;
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;
896 new Ajax.Request(this.url, this.options);
899 /** @id Ajax.Autocompleter.prototype.onComplete */
900 onComplete: function (request) {
901 this.updateChoices(request.responseText);
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;
957 /** @id Autocompleter.Local.prototype.getUpdatedChoices */
958 getUpdatedChoices: function () {
959 this.updateChoices(this.options.selector(this));
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;
999 foundPos = instance.options.ignoreCase ?
1000 elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
1001 elem.indexOf(entry, foundPos + 1);
1005 if (partial.length) {
1006 ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
1008 return '<ul>' + ret.join('') + '</ul>';
1010 }, options || {});
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});
1051 onFailure: function (transport) {
1052 alert('Error communicating with the server: ' + MochiKit.Base.stripTags(transport.responseText));
1054 callback: function (form) {
1055 return MochiKit.DOM.formContents(form);
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;
1077 if (this.options.externalControl) {
1078 this.options.externalControl = MochiKit.DOM.getElement(this.options.externalControl);
1081 this.originalBackground = MochiKit.Style.getStyle(this.element, 'background-color');
1082 if (!this.originalBackground) {
1083 this.originalBackground = 'transparent';
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);
1101 /** @id Ajax.InPlaceEditor.prototype.enterEditMode */
1102 enterEditMode: function (evt) {
1103 if (this.saving) {
1104 return;
1106 if (this.editing) {
1107 return;
1109 this.editing = true;
1110 this.onEnterEditMode();
1111 if (this.options.externalControl) {
1112 MochiKit.Style.hideElement(this.options.externalControl);
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();
1122 return false;
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);
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);
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);
1155 /** @id Ajax.InPlaceEditor.prototype.hasHTMLLineBreaks */
1156 hasHTMLLineBreaks: function (string) {
1157 if (!this.options.handleLineBreaks) {
1158 return false;
1160 return string.match(/<br/i) || string.match(/<p>/i);
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, '');
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();
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;
1191 if (this.options.submitOnBlur) {
1192 textField.onblur = MochiKit.Base.bind(this.onSubmit, this);
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);
1206 this.editField = textArea;
1209 if (this.options.loadTextURL) {
1210 this.loadExternalText();
1212 this.form.appendChild(this.editField);
1215 /** @id Ajax.InPlaceEditor.prototype.getText */
1216 getText: function () {
1217 return this.element.innerHTML;
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)
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);
1240 /** @id Ajax.InPlaceEditor.prototype.onclickCancel */
1241 onclickCancel: function () {
1242 this.onComplete();
1243 this.leaveEditMode();
1244 return false;
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;
1254 return false;
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(
1271 success: this.element,
1272 // dont update on failure (this could be an option)
1273 failure: null
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)
1282 // stop the event to avoid a page refresh in Safari
1283 if (arguments.length > 1) {
1284 arguments[0].stop();
1286 return false;
1289 /** @id Ajax.InPlaceEditor.prototype.onLoading */
1290 onLoading: function () {
1291 this.saving = true;
1292 this.removeForm();
1293 this.leaveHover();
1294 this.showSaving();
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);
1306 /** @id Ajax.InPlaceEditor.prototype.removeForm */
1307 removeForm: function () {
1308 if (this.form) {
1309 if (this.form.parentNode) {
1310 MochiKit.DOM.removeElement(this.form);
1312 this.form = null;
1316 /** @id Ajax.InPlaceEditor.prototype.enterHover */
1317 enterHover: function () {
1318 if (this.saving) {
1319 return;
1321 this.element.style.backgroundColor = this.options.highlightcolor;
1322 if (this.effect) {
1323 this.effect.cancel();
1325 MochiKit.DOM.addElementClass(this.element, this.options.hoverClassName)
1328 /** @id Ajax.InPlaceEditor.prototype.leaveHover */
1329 leaveHover: function () {
1330 if (this.options.backgroundColor) {
1331 this.element.style.backgroundColor = this.oldBackground;
1333 MochiKit.DOM.removeElementClass(this.element, this.options.hoverClassName)
1334 if (this.saving) {
1335 return;
1337 this.effect = new MochiKit.Visual.Highlight(this.element, {
1338 startcolor: this.options.highlightcolor,
1339 endcolor: this.options.highlightendcolor,
1340 restorecolor: this.originalBackground
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);
1354 this.editing = false;
1355 this.saving = false;
1356 this.oldInnerHTML = null;
1357 this.onLeaveEditMode();
1360 /** @id Ajax.InPlaceEditor.prototype.onComplete */
1361 onComplete: function (transport) {
1362 this.leaveEditMode();
1363 MochiKit.Base.bind(this.options.onComplete, this)(transport, this.element);
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;
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);