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)
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.
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) {
66 }, MochiKit.Base.extractScripts(str));
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]);
80 queryComponents.push(queryComponent);
84 return queryComponents.join('&');
87 /** @id MochiKit.Form.getElements */
88 getElements: function (form) {
89 form = MochiKit.DOM.getElement(form);
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]);
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);
108 var key = encodeURIComponent(parameter[0]);
109 if (key.length === 0) {
113 if (!(parameter[1] instanceof Array)) {
114 parameter[1] = [parameter[1]];
117 return parameter[1].map(function (value) {
118 return key + '=' + encodeURIComponent(value);
124 MochiKit.Form.Serializers = {
126 /** @id MochiKit.Form.Serializers.input */
127 input: function (element) {
128 switch (element.type.toLowerCase()) {
133 return MochiKit.Form.Serializers.textarea(element);
136 return MochiKit.Form.Serializers.inputSelector(element);
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;
163 opt = element.options[index];
165 if (!value && !('value' in opt)) {
169 return [element.name, value];
172 /** @id MochiKit.Form.Serializers.selectMany */
173 selectMany: function (element) {
175 for (var i = 0; i < element.length; i++) {
176 var opt = element.options[i];
178 var optValue = opt.value;
179 if (!optValue && !('value' in opt)) {
182 value.push(optValue);
185 return [element.name, value];
191 activeRequestCount: 0
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') {
215 responder[callback].apply(responder, [request, transport, json]);
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--;
236 Ajax.Base = function () {};
238 Ajax.Base.prototype = {
240 /** @id Ajax.Base.prototype.setOptions */
241 setOptions: function (options) {
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);
281 /** @id Ajax.Request.prototype.request */
282 request: function (url) {
283 var parameters = this.options.parameters || '';
284 if (parameters.length > 0){
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);
305 this.setRequestHeaders();
307 var body = this.options.postBody ? this.options.postBody : parameters;
308 this.transport.send(this.options.method == 'post' ? body : null);
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) {
352 return this.transport.getResponseHeader(name);
356 /** @id Ajax.Request.prototype.evalJSON */
357 evalJSON: function () {
359 return eval(this.header('X-JSON'));
363 /** @id Ajax.Request.prototype.evalResponse */
364 evalResponse: function () {
366 return eval(this.transport.responseText);
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') {
379 (this.options['on' + this.transport.status]
380 || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
381 || MochiKit.Base.noop)(transport, json);
383 this.dispatchException(e);
386 if ((this.header('Content-type') || '').match(/^text\/javascript/i)) {
392 (this.options['on' + event] || MochiKit.Base.noop)(transport, json);
393 Ajax.Responders.dispatch('on' + event, this, transport, json);
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) {
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);
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);
448 if (this.options.insertion) {
449 new this.options.insertion(receiver, response);
451 MochiKit.DOM.getElement(receiver).innerHTML =
452 MochiKit.Base.stripScripts(response);
453 setTimeout(function () {
454 MochiKit.Base.evalScripts(response);
459 if (this.responseIsSuccess()) {
460 if (this.onComplete) {
461 setTimeout(MochiKit.Base.bind(this.onComplete, this), 10);
472 for (var i = 0; i < arguments.length; i++) {
473 MochiKit.DOM.getElement(arguments[i]).value = '';
478 focus: function (element) {
479 MochiKit.DOM.getElement(element).focus();
483 present: function () {
484 for (var i = 0; i < arguments.length; i++) {
485 if (MochiKit.DOM.getElement(arguments[i]).value == '') {
493 select: function (element) {
494 MochiKit.DOM.getElement(element).select();
498 activate: function (element) {
499 element = MochiKit.DOM.getElement(element);
501 if (element.select) {
506 /** @id scrollFreeActivate */
507 scrollFreeActivate: function (field) {
508 setTimeout(function () {
509 Field.activate(field);
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;
533 if (this.setOptions) {
534 this.setOptions(options);
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, {
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 */
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');
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 */
600 this.stopIndicator();
601 if (MochiKit.Style.getStyle(this.update, 'display') != 'none') {
602 this.options.onHide(this.element, this.update);
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) {
626 if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") {
628 MochiKit.Event.stop(event);
629 } else if (event.key().string == "KEY_ESCAPE") {
632 MochiKit.Event.stop(event);
634 } else if (event.key().string == "KEY_LEFT" || event.key().string == "KEY_RIGHT") {
636 } else if (event.key().string == "KEY_UP") {
639 if (/AppleWebKit'/.test(navigator.appVersion)) {
643 } else if (event.key().string == "KEY_DOWN") {
646 if (/AppleWebKit'/.test(navigator.appVersion)) {
652 if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") {
658 this.hasFocus = true;
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;
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;
687 /** @id Autocompleter.Base.prototype.onClick */
688 onClick: function (event) {
689 var element = this.findElement(event, 'LI');
690 this.index = element.autocompleteIndex;
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;
703 /** @id Autocompleter.Base.prototype.render */
704 render: function () {
705 if (this.entryCount > 0) {
706 for (var i = 0; i < this.entryCount; i++) {
708 MochiKit.DOM.addElementClass(this.getEntry(i), 'selected') :
709 MochiKit.DOM.removeElementClass(this.getEntry(i), 'selected');
721 /** @id Autocompleter.Base.prototype.markPrevious */
722 markPrevious: function () {
723 if (this.index > 0) {
726 this.index = this.entryCount-1;
730 /** @id Autocompleter.Base.prototype.markNext */
731 markNext: function () {
732 if (this.index < this.entryCount-1) {
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 () {
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);
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);
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]);
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+/);
787 newValue += whitespace[0];
789 this.element.value = newValue + value;
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);
819 this.stopIndicator();
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();
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+$/,'');
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;
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;
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);
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.
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
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({
970 selector: function (instance) {
971 var ret = []; // Beginning matches
972 var partial = []; // Inside matches
973 var entry = instance.getToken();
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()) :
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>');
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>');
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>';
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
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) {
1037 this.element = MochiKit.DOM.getElement(element);
1039 this.options = MochiKit.Base.update({
1043 cancelText: 'cancel',
1044 savingText: 'Saving...',
1045 clickToEditText: 'Click to edit',
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,
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) {
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);
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
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) {
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 () {
1171 if (this.options.loadTextURL) {
1172 text = this.options.loadingText;
1174 text = this.getText();
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;
1189 textField.size = size;
1191 if (this.options.submitOnBlur) {
1192 textField.onblur = MochiKit.Base.bind(this.onSubmit, this);
1194 this.editField = textField;
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;
1225 this.options.loadTextURL,
1226 MochiKit.Base.update({
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 () {
1243 this.leaveEditMode();
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;
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
1271 success: this.element,
1272 // dont update on failure (this could be an option)
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();
1289 /** @id Ajax.InPlaceEditor.prototype.onLoading */
1290 onLoading: function () {
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 () {
1309 if (this.form.parentNode) {
1310 MochiKit.DOM.removeElement(this.form);
1316 /** @id Ajax.InPlaceEditor.prototype.enterHover */
1317 enterHover: function () {
1321 this.element.style.backgroundColor = this.options.highlightcolor;
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)
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);
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);