Initial check-in
[dygraphs.git] / mochikit_v14 / MochiKit / Controls.js
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)
10
11 See scriptaculous.js for full license.
12
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.
17
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.
26
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.
37
38 ***/
39
40 MochiKit.Base.update(MochiKit.Base, {
41 ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
42
43 /** @id MochiKit.Base.stripScripts */
44 stripScripts: function (str) {
45 return str.replace(new RegExp(MochiKit.Base.ScriptFragment, 'img'), '');
46 },
47
48 /** @id MochiKit.Base.stripTags */
49 stripTags: function(str) {
50 return str.replace(/<\/?[^>]+>/gi, '');
51 },
52
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 },
61
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 });
69
70 MochiKit.Form = {
71
72 /** @id MochiKit.Form.serialize */
73 serialize: function (form) {
74 var elements = MochiKit.Form.getElements(form);
75 var queryComponents = [];
76
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 }
83
84 return queryComponents.join('&');
85 },
86
87 /** @id MochiKit.Form.getElements */
88 getElements: function (form) {
89 form = MochiKit.DOM.getElement(form);
90 var elements = [];
91
92 for (var 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 },
100
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);
106
107 if (parameter) {
108 var key = encodeURIComponent(parameter[0]);
109 if (key.length === 0) {
110 return;
111 }
112
113 if (!(parameter[1] instanceof Array)) {
114 parameter[1] = [parameter[1]];
115 }
116
117 return parameter[1].map(function (value) {
118 return key + '=' + encodeURIComponent(value);
119 }).join('&');
120 }
121 }
122 };
123
124 MochiKit.Form.Serializers = {
125
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 },
140
141 /** @id MochiKit.Form.Serializers.inputSelector */
142 inputSelector: function (element) {
143 if (element.checked) {
144 return [element.name, element.value];
145 }
146 },
147
148 /** @id MochiKit.Form.Serializers.textarea */
149 textarea: function (element) {
150 return [element.name, element.value];
151 },
152
153 /** @id MochiKit.Form.Serializers.select */
154 select: function (element) {
155 return MochiKit.Form.Serializers[element.type == 'select-one' ?
156 'selectOne' : 'selectMany'](element);
157 },
158
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 },
171
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 }
187 };
188
189 /** @id Ajax */
190 var Ajax = {
191 activeRequestCount: 0
192 };
193
194 Ajax.Responders = {
195 responders: [],
196
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 },
203
204 /** @id Ajax.Responders.unregister */
205 unregister: function (responderToRemove) {
206 this.responders = this.responders.without(responderToRemove);
207 },
208
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 }
220 };
221
222 Ajax.Responders.register({
223
224 /** @id Ajax.Responders.onCreate */
225 onCreate: function () {
226 Ajax.activeRequestCount++;
227 },
228
229 /** @id Ajax.Responders.onComplete */
230 onComplete: function () {
231 Ajax.activeRequestCount--;
232 }
233 });
234
235 /** @id Ajax.Base */
236 Ajax.Base = function () {};
237
238 Ajax.Base.prototype = {
239
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 },
249
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 },
256
257 /** @id Ajax.Base.prototype.responseIsFailure */
258 responseIsFailure: function () {
259 return !this.responseIsSuccess();
260 }
261 };
262
263 /** @id Ajax.Request */
264 Ajax.Request = function (url, options) {
265 this.__init__(url, options);
266 };
267
268 /** @id Ajax.Events */
269 Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded',
270 'Interactive', 'Complete'];
271
272 MochiKit.Base.update(Ajax.Request.prototype, Ajax.Base.prototype);
273
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 },
280
281 /** @id Ajax.Request.prototype.request */
282 request: function (url) {
283 var parameters = this.options.parameters || '';
284 if (parameters.length > 0){
285 parameters += '&_=';
286 }
287
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);
294
295 this.transport.open(this.options.method, this.url,
296 this.options.asynchronous);
297
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 }
304
305 this.setRequestHeaders();
306
307 var body = this.options.postBody ? this.options.postBody : parameters;
308 this.transport.send(this.options.method == 'post' ? body : null);
309
310 } catch (e) {
311 this.dispatchException(e);
312 }
313 },
314
315 /** @id Ajax.Request.prototype.setRequestHeaders */
316 setRequestHeaders: function () {
317 var requestHeaders = ['X-Requested-With', 'XMLHttpRequest'];
318
319 if (this.options.method == 'post') {
320 requestHeaders.push('Content-type',
321 'application/x-www-form-urlencoded');
322
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 }
331
332 if (this.options.requestHeaders) {
333 requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
334 }
335
336 for (var i = 0; i < requestHeaders.length; i += 2) {
337 this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
338 }
339 },
340
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 },
348
349 /** @id Ajax.Request.prototype.header */
350 header: function (name) {
351 try {
352 return this.transport.getResponseHeader(name);
353 } catch (e) {}
354 },
355
356 /** @id Ajax.Request.prototype.evalJSON */
357 evalJSON: function () {
358 try {
359 return eval(this.header('X-JSON'));
360 } catch (e) {}
361 },
362
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 },
371
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();
376
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 }
385
386 if ((this.header('Content-type') || '').match(/^text\/javascript/i)) {
387 this.evalResponse();
388 }
389 }
390
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 }
397
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 },
403
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 }
409 });
410
411 /** @id Ajax.Updater */
412 Ajax.Updater = function (container, url, options) {
413 this.__init__(container, url, options);
414 };
415
416 MochiKit.Base.update(Ajax.Updater.prototype, Ajax.Request.prototype);
417
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);
427
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);
433
434 this.request(url);
435 },
436
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;
442
443 if (!this.options.evalScripts) {
444 response = MochiKit.Base.stripScripts(response);
445 }
446
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 }
458
459 if (this.responseIsSuccess()) {
460 if (this.onComplete) {
461 setTimeout(MochiKit.Base.bind(this.onComplete, this), 10);
462 }
463 }
464 }
465 });
466
467 /** @id Field */
468 var Field = {
469
470 /** @id clear */
471 clear: function () {
472 for (var i = 0; i < arguments.length; i++) {
473 MochiKit.DOM.getElement(arguments[i]).value = '';
474 }
475 },
476
477 /** @id focus */
478 focus: function (element) {
479 MochiKit.DOM.getElement(element).focus();
480 },
481
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 },
491
492 /** @id select */
493 select: function (element) {
494 MochiKit.DOM.getElement(element).select();
495 },
496
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 },
505
506 /** @id scrollFreeActivate */
507 scrollFreeActivate: function (field) {
508 setTimeout(function () {
509 Field.activate(field);
510 }, 1);
511 }
512 };
513
514
515 /** @id Autocompleter */
516 var Autocompleter = {};
517
518 /** @id Autocompleter.Base */
519 Autocompleter.Base = function () {};
520
521 Autocompleter.Base.prototype = {
522
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;
532
533 if (this.setOptions) {
534 this.setOptions(options);
535 }
536 else {
537 this.options = options || {};
538 }
539
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 };
557
558 if (typeof(this.options.tokens) == 'string') {
559 this.options.tokens = new Array(this.options.tokens);
560 }
561
562 this.observer = null;
563
564 this.element.setAttribute('autocomplete', 'off');
565
566 MochiKit.Style.hideElement(this.update);
567
568 MochiKit.Signal.connect(this.element, 'onblur', this, this.onBlur);
569 MochiKit.Signal.connect(this.element, 'onkeypress', this, this.onKeyPress, this);
570 },
571
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 },
589
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 },
597
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 },
608
609 /** @id Autocompleter.Base.prototype.startIndicator */
610 startIndicator: function () {
611 if (this.options.indicator) {
612 MochiKit.Style.showElement(this.options.indicator);
613 }
614 },
615
616 /** @id Autocompleter.Base.prototype.stopIndicator */
617 stopIndicator: function () {
618 if (this.options.indicator) {
619 MochiKit.Style.hideElement(this.options.indicator);
620 }
621 },
622
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 }
656
657 this.changed = true;
658 this.hasFocus = true;
659
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 },
666
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 },
676
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 },
686
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 },
694
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 },
702
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 },
720
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 },
729
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 },
738
739 /** @id Autocompleter.Base.prototype.getEntry */
740 getEntry: function (index) {
741 return this.update.firstChild.childNodes[index];
742 },
743
744 /** @id Autocompleter.Base.prototype.getCurrentEntry */
745 getCurrentEntry: function () {
746 return this.getEntry(this.index);
747 },
748
749 /** @id Autocompleter.Base.prototype.selectEntry */
750 selectEntry: function () {
751 this.active = false;
752 this.updateElement(this.getCurrentEntry());
753 },
754
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 },
766
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();
794
795 if (this.options.afterUpdateElement) {
796 this.options.afterUpdateElement(this.element, selectedElement);
797 }
798 },
799
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);
807
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 }
818
819 this.stopIndicator();
820
821 this.index = 0;
822 this.render();
823 }
824 },
825
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 },
831
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 },
843
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 },
854
855 /** @id Autocompleter.Base.prototype.findLastToken */
856 findLastToken: function () {
857 var lastTokenPos = -1;
858
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 }
867 }
868
869 /** @id Ajax.Autocompleter */
870 Ajax.Autocompleter = function (element, update, url, options) {
871 this.__init__(element, update, url, options);
872 };
873
874 MochiKit.Base.update(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype);
875
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 },
884
885 /** @id Ajax.Autocompleter.prototype.getUpdatedChoices */
886 getUpdatedChoices: function () {
887 var entry = encodeURIComponent(this.options.paramName) + '=' +
888 encodeURIComponent(this.getToken());
889
890 this.options.parameters = this.options.callback ?
891 this.options.callback(this.element, entry) : entry;
892
893 if (this.options.defaultParams) {
894 this.options.parameters += '&' + this.options.defaultParams;
895 }
896 new Ajax.Request(this.url, this.options);
897 },
898
899 /** @id Ajax.Autocompleter.prototype.onComplete */
900 onComplete: function (request) {
901 this.updateChoices(request.responseText);
902 }
903 });
904
905 /***
906
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.
910
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.
915
916 Extra local autocompletion options:
917 - choices - How many autocompletion choices to offer
918
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).
926
927 - fullSsearch - Search anywhere in autocomplete array strings.
928
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.
933
934 - ignoreCase - Whether to ignore case when autocompleting.
935 Defaults to true.
936
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.
941
942 ***/
943
944 /** @id Autocompleter.Local */
945 Autocompleter.Local = function (element, update, array, options) {
946 this.__init__(element, update, array, options);
947 };
948
949 MochiKit.Base.update(Autocompleter.Local.prototype, Autocompleter.Base.prototype);
950
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 },
956
957 /** @id Autocompleter.Local.prototype.getUpdatedChoices */
958 getUpdatedChoices: function () {
959 this.updateChoices(this.options.selector(this));
960 },
961
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;
975
976 for (var i = 0; i < instance.options.array.length &&
977 ret.length < instance.options.choices ; i++) {
978
979 var elem = instance.options.array[i];
980 var foundPos = instance.options.ignoreCase ?
981 elem.toLowerCase().indexOf(entry.toLowerCase()) :
982 elem.indexOf(entry);
983
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 }
998
999 foundPos = instance.options.ignoreCase ?
1000 elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
1001 elem.indexOf(entry, foundPos + 1);
1002
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 }
1012 });
1013
1014 /***
1015
1016 AJAX in-place editor
1017
1018 see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
1019
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
1023
1024 ***/
1025
1026 /** @id Ajax.InPlaceEditor */
1027 Ajax.InPlaceEditor = function (element, url, options) {
1028 this.__init__(element, url, options);
1029 };
1030
1031 /** @id Ajax.InPlaceEditor.defaultHighlightColor */
1032 Ajax.InPlaceEditor.defaultHighlightColor = '#FFFF99';
1033
1034 Ajax.InPlaceEditor.prototype = {
1035 __init__: function (element, url, options) {
1036 this.url = url;
1037 this.element = MochiKit.DOM.getElement(element);
1038
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 || {});
1068
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 }
1076
1077 if (this.options.externalControl) {
1078 this.options.externalControl = MochiKit.DOM.getElement(this.options.externalControl);
1079 }
1080
1081 this.originalBackground = MochiKit.Style.getStyle(this.element, 'background-color');
1082 if (!this.originalBackground) {
1083 this.originalBackground = 'transparent';
1084 }
1085
1086 this.element.title = this.options.clickToEditText;
1087
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 },
1100
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 },
1124
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);
1131
1132 this.createEditField();
1133
1134 if (this.options.textarea) {
1135 var br = document.createElement('br');
1136 this.form.appendChild(br);
1137 }
1138
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 }
1145
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 },
1154
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 },
1162
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 },
1167
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 }
1176
1177 var obj = this;
1178
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 }
1208
1209 if (this.options.loadTextURL) {
1210 this.loadExternalText();
1211 }
1212 this.form.appendChild(this.editField);
1213 },
1214
1215 /** @id Ajax.InPlaceEditor.prototype.getText */
1216 getText: function () {
1217 return this.element.innerHTML;
1218 },
1219
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 },
1232
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 },
1239
1240 /** @id Ajax.InPlaceEditor.prototype.onclickCancel */
1241 onclickCancel: function () {
1242 this.onComplete();
1243 this.leaveEditMode();
1244 return false;
1245 },
1246
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 },
1256
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;
1262
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();
1268
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 },
1288
1289 /** @id Ajax.InPlaceEditor.prototype.onLoading */
1290 onLoading: function () {
1291 this.saving = true;
1292 this.removeForm();
1293 this.leaveHover();
1294 this.showSaving();
1295 },
1296
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 },
1305
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 },
1315
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 },
1327
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 },
1343
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 },
1359
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 },
1365
1366 /** @id Ajax.InPlaceEditor.prototype.onEnterEditMode */
1367 onEnterEditMode: function () {},
1368
1369 /** @id Ajax.InPlaceEditor.prototype.onLeaveEditMode */
1370 onLeaveEditMode: function () {},
1371
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 }
1387 };
1388