1 // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 // (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3 // (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
9 // script.aculo.us is freely distributable under the terms of an MIT-style license.
10 // For details, see the script.aculo.us web site: http://script.aculo.us/
12 // Autocompleter.Base handles all the autocompletion functionality
13 // that's independent of the data source for autocompletion. This
14 // includes drawing the autocompletion menu, observing keyboard
15 // and mouse events, and similar.
17 // Specific autocompleters need to provide, at the very least,
18 // a getUpdatedChoices function that will be invoked every time
19 // the text inside the monitored textbox changes. This method
20 // should get the text for which to provide autocompletion by
21 // invoking this.getToken(), NOT by directly accessing
22 // this.element.value. This is to allow incremental tokenized
23 // autocompletion. Specific auto-completion logic (AJAX, etc)
24 // belongs in getUpdatedChoices.
26 // Tokenized incremental autocompletion is enabled automatically
27 // when an autocompleter is instantiated with the 'tokens' option
28 // in the options parameter, e.g.:
29 // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
30 // will incrementally autocomplete with a comma as the token.
31 // Additionally, ',' in the above example can be replaced with
32 // a token array, e.g. { tokens: [',', '\n'] } which
33 // enables autocompletion on multiple tokens. This is most
34 // useful when one of the tokens is \n (a newline), as it
35 // allows smart autocompletion after linebreaks.
37 JSAN
.use('Prototype');
38 JSAN
.use('Scriptaculous.Effects');
40 if(typeof Effect
== 'undefined')
41 throw("Controls.js requires including script.aculo.us' Effects.js library");
43 var Autocompleter
= { }
44 Autocompleter
.Base
= Class
.create({
45 baseInitialize: function(element
, update
, options
) {
47 this.element
= element
;
48 this.update
= $(update
);
49 this.hasFocus
= false;
54 this.oldElementValue
= this.element
.value
;
57 this.setOptions(options
);
59 this.options
= options
|| { };
61 this.options
.paramName
= this.options
.paramName
|| this.element
.name
;
62 this.options
.tokens
= this.options
.tokens
|| [];
63 this.options
.frequency
= this.options
.frequency
|| 0.4;
64 this.options
.minChars
= this.options
.minChars
|| 1;
65 this.options
.onShow
= this.options
.onShow
||
66 function(element
, update
){
67 if(!update
.style
.position
|| update
.style
.position
=='absolute') {
68 update
.style
.position
= 'absolute';
69 Position
.clone(element
, update
, {
71 offsetTop
: element
.offsetHeight
74 Effect
.Appear(update
,{duration
:0.15});
76 this.options
.onHide
= this.options
.onHide
||
77 function(element
, update
){ new Effect
.Fade(update
,{duration
:0.15}) };
79 if(typeof(this.options
.tokens
) == 'string')
80 this.options
.tokens
= new Array(this.options
.tokens
);
81 // Force carriage returns as token delimiters anyway
82 if (!this.options
.tokens
.include('\n'))
83 this.options
.tokens
.push('\n');
87 this.element
.setAttribute('autocomplete','off');
89 Element
.hide(this.update
);
91 Event
.observe(this.element
, 'blur', this.onBlur
.bindAsEventListener(this));
92 Event
.observe(this.element
, 'keypress', this.onKeyPress
.bindAsEventListener(this));
96 if(Element
.getStyle(this.update
, 'display')=='none') this.options
.onShow(this.element
, this.update
);
98 (Prototype
.Browser
.IE
) &&
99 (Element
.getStyle(this.update
, 'position')=='absolute')) {
100 new Insertion
.After(this.update
,
101 '<iframe id="' + this.update
.id
+ '_iefix" '+
102 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
103 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
104 this.iefix
= $(this.update
.id
+'_iefix');
106 if(this.iefix
) setTimeout(this.fixIEOverlapping
.bind(this), 50);
109 fixIEOverlapping: function() {
110 Position
.clone(this.update
, this.iefix
, {setTop
:(!this.update
.style
.height
)});
111 this.iefix
.style
.zIndex
= 1;
112 this.update
.style
.zIndex
= 2;
113 Element
.show(this.iefix
);
117 this.stopIndicator();
118 if(Element
.getStyle(this.update
, 'display')!='none') this.options
.onHide(this.element
, this.update
);
119 if(this.iefix
) Element
.hide(this.iefix
);
122 startIndicator: function() {
123 if(this.options
.indicator
) Element
.show(this.options
.indicator
);
126 stopIndicator: function() {
127 if(this.options
.indicator
) Element
.hide(this.options
.indicator
);
130 onKeyPress: function(event
) {
132 switch(event
.keyCode
) {
134 case Event
.KEY_RETURN
:
143 case Event
.KEY_RIGHT
:
148 if(Prototype
.Browser
.WebKit
) Event
.stop(event
);
153 if(Prototype
.Browser
.WebKit
) Event
.stop(event
);
157 if(event
.keyCode
==Event
.KEY_TAB
|| event
.keyCode
==Event
.KEY_RETURN
||
158 (Prototype
.Browser
.WebKit
> 0 && event
.keyCode
== 0)) return;
161 this.hasFocus
= true;
163 if(this.observer
) clearTimeout(this.observer
);
165 setTimeout(this.onObserverEvent
.bind(this), this.options
.frequency
*1000);
168 activate: function() {
169 this.changed
= false;
170 this.hasFocus
= true;
171 this.getUpdatedChoices();
174 onHover: function(event
) {
175 var element
= Event
.findElement(event
, 'LI');
176 if(this.index
!= element
.autocompleteIndex
)
178 this.index
= element
.autocompleteIndex
;
184 onClick: function(event
) {
185 var element
= Event
.findElement(event
, 'LI');
186 this.index
= element
.autocompleteIndex
;
191 onBlur: function(event
) {
192 // needed to make click events working
193 setTimeout(this.hide
.bind(this), 250);
194 this.hasFocus
= false;
199 if(this.entryCount
> 0) {
200 for (var i
= 0; i
< this.entryCount
; i
++)
202 Element
.addClassName(this.getEntry(i
),"selected") :
203 Element
.removeClassName(this.getEntry(i
),"selected");
214 markPrevious: function() {
215 if(this.index
> 0) this.index
--
216 else this.index
= this.entryCount
-1;
217 this.getEntry(this.index
).scrollIntoView(true);
220 markNext: function() {
221 if(this.index
< this.entryCount
-1) this.index
++
223 this.getEntry(this.index
).scrollIntoView(false);
226 getEntry: function(index
) {
227 return this.update
.firstChild
.childNodes
[index
];
230 getCurrentEntry: function() {
231 return this.getEntry(this.index
);
234 selectEntry: function() {
236 this.updateElement(this.getCurrentEntry());
239 updateElement: function(selectedElement
) {
240 if (this.options
.updateElement
) {
241 this.options
.updateElement(selectedElement
);
245 if (this.options
.select
) {
246 var nodes
= $(selectedElement
).select('.' + this.options
.select
) || [];
247 if(nodes
.length
>0) value
= Element
.collectTextNodes(nodes
[0], this.options
.select
);
249 value
= Element
.collectTextNodesIgnoreClass(selectedElement
, 'informal');
251 var bounds
= this.getTokenBounds();
252 if (bounds
[0] != -1) {
253 var newValue
= this.element
.value
.substr(0, bounds
[0]);
254 var whitespace
= this.element
.value
.substr(bounds
[0]).match(/^\s+/);
256 newValue
+= whitespace
[0];
257 this.element
.value
= newValue
+ value
+ this.element
.value
.substr(bounds
[1]);
259 this.element
.value
= value
;
261 this.oldElementValue
= this.element
.value
;
262 this.element
.focus();
264 if (this.options
.afterUpdateElement
)
265 this.options
.afterUpdateElement(this.element
, selectedElement
);
268 updateChoices: function(choices
) {
269 if(!this.changed
&& this.hasFocus
) {
270 this.update
.innerHTML
= choices
;
271 Element
.cleanWhitespace(this.update
);
272 Element
.cleanWhitespace(this.update
.down());
274 if(this.update
.firstChild
&& this.update
.down().childNodes
) {
276 this.update
.down().childNodes
.length
;
277 for (var i
= 0; i
< this.entryCount
; i
++) {
278 var entry
= this.getEntry(i
);
279 entry
.autocompleteIndex
= i
;
280 this.addObservers(entry
);
286 this.stopIndicator();
289 if(this.entryCount
==1 && this.options
.autoSelect
) {
298 addObservers: function(element
) {
299 Event
.observe(element
, "mouseover", this.onHover
.bindAsEventListener(this));
300 Event
.observe(element
, "click", this.onClick
.bindAsEventListener(this));
303 onObserverEvent: function() {
304 this.changed
= false;
305 this.tokenBounds
= null;
306 if(this.getToken().length
>=this.options
.minChars
) {
307 this.getUpdatedChoices();
312 this.oldElementValue
= this.element
.value
;
315 getToken: function() {
316 var bounds
= this.getTokenBounds();
317 return this.element
.value
.substring(bounds
[0], bounds
[1]).strip();
320 getTokenBounds: function() {
321 if (null != this.tokenBounds
) return this.tokenBounds
;
322 var value
= this.element
.value
;
323 if (value
.strip().empty()) return [-1, 0];
324 var diff
= arguments
.callee
.getFirstDifferencePos(value
, this.oldElementValue
);
325 var offset
= (diff
== this.oldElementValue
.length
? 1 : 0);
326 var prevTokenPos
= -1, nextTokenPos
= value
.length
;
328 for (var index
= 0, l
= this.options
.tokens
.length
; index
< l
; ++index
) {
329 tp
= value
.lastIndexOf(this.options
.tokens
[index
], diff
+ offset
- 1);
330 if (tp
> prevTokenPos
) prevTokenPos
= tp
;
331 tp
= value
.indexOf(this.options
.tokens
[index
], diff
+ offset
);
332 if (-1 != tp
&& tp
< nextTokenPos
) nextTokenPos
= tp
;
334 return (this.tokenBounds
= [prevTokenPos
+ 1, nextTokenPos
]);
338 Autocompleter
.Base
.prototype.getTokenBounds
.getFirstDifferencePos = function(newS
, oldS
) {
339 var boundary
= Math
.min(newS
.length
, oldS
.length
);
340 for (var index
= 0; index
< boundary
; ++index
)
341 if (newS
[index
] != oldS
[index
])
346 Ajax
.Autocompleter
= Class
.create(Autocompleter
.Base
, {
347 initialize: function(element
, update
, url
, options
) {
348 this.baseInitialize(element
, update
, options
);
349 this.options
.asynchronous
= true;
350 this.options
.onComplete
= this.onComplete
.bind(this);
351 this.options
.defaultParams
= this.options
.parameters
|| null;
355 getUpdatedChoices: function() {
356 this.startIndicator();
358 var entry
= encodeURIComponent(this.options
.paramName
) + '=' +
359 encodeURIComponent(this.getToken());
361 this.options
.parameters
= this.options
.callback
?
362 this.options
.callback(this.element
, entry
) : entry
;
364 if(this.options
.defaultParams
)
365 this.options
.parameters
+= '&' + this.options
.defaultParams
;
367 new Ajax
.Request(this.url
, this.options
);
370 onComplete: function(request
) {
371 this.updateChoices(request
.responseText
);
375 // The local array autocompleter. Used when you'd prefer to
376 // inject an array of autocompletion options into the page, rather
377 // than sending out Ajax queries, which can be quite slow sometimes.
379 // The constructor takes four parameters. The first two are, as usual,
380 // the id of the monitored textbox, and id of the autocompletion menu.
381 // The third is the array you want to autocomplete from, and the fourth
382 // is the options block.
384 // Extra local autocompletion options:
385 // - choices - How many autocompletion choices to offer
387 // - partialSearch - If false, the autocompleter will match entered
388 // text only at the beginning of strings in the
389 // autocomplete array. Defaults to true, which will
390 // match text at the beginning of any *word* in the
391 // strings in the autocomplete array. If you want to
392 // search anywhere in the string, additionally set
393 // the option fullSearch to true (default: off).
395 // - fullSsearch - Search anywhere in autocomplete array strings.
397 // - partialChars - How many characters to enter before triggering
398 // a partial match (unlike minChars, which defines
399 // how many characters are required to do any match
400 // at all). Defaults to 2.
402 // - ignoreCase - Whether to ignore case when autocompleting.
405 // It's possible to pass in a custom function as the 'selector'
406 // option, if you prefer to write your own autocompletion logic.
407 // In that case, the other options above will not apply unless
410 Autocompleter
.Local
= Class
.create(Autocompleter
.Base
, {
411 initialize: function(element
, update
, array
, options
) {
412 this.baseInitialize(element
, update
, options
);
413 this.options
.array
= array
;
416 getUpdatedChoices: function() {
417 this.updateChoices(this.options
.selector(this));
420 setOptions: function(options
) {
421 this.options
= Object
.extend({
427 selector: function(instance
) {
428 var ret
= []; // Beginning matches
429 var partial
= []; // Inside matches
430 var entry
= instance
.getToken();
433 for (var i
= 0; i
< instance
.options
.array
.length
&&
434 ret
.length
< instance
.options
.choices
; i
++) {
436 var elem
= instance
.options
.array
[i
];
437 var foundPos
= instance
.options
.ignoreCase
?
438 elem
.toLowerCase().indexOf(entry
.toLowerCase()) :
441 while (foundPos
!= -1) {
442 if (foundPos
== 0 && elem
.length
!= entry
.length
) {
443 ret
.push("<li><strong>" + elem
.substr(0, entry
.length
) + "</strong>" +
444 elem
.substr(entry
.length
) + "</li>");
446 } else if (entry
.length
>= instance
.options
.partialChars
&&
447 instance
.options
.partialSearch
&& foundPos
!= -1) {
448 if (instance
.options
.fullSearch
|| /\s/.test(elem
.substr(foundPos
-1,1))) {
449 partial
.push("<li>" + elem
.substr(0, foundPos
) + "<strong>" +
450 elem
.substr(foundPos
, entry
.length
) + "</strong>" + elem
.substr(
451 foundPos
+ entry
.length
) + "</li>");
456 foundPos
= instance
.options
.ignoreCase
?
457 elem
.toLowerCase().indexOf(entry
.toLowerCase(), foundPos
+ 1) :
458 elem
.indexOf(entry
, foundPos
+ 1);
463 ret
= ret
.concat(partial
.slice(0, instance
.options
.choices
- ret
.length
))
464 return "<ul>" + ret
.join('') + "</ul>";
470 // AJAX in-place editor and collection editor
471 // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
473 // Use this if you notice weird scrolling problems on some browsers,
474 // the DOM might be a bit confused when this gets called so do this
475 // waits 1 ms (with setTimeout) until it does the activation
476 Field
.scrollFreeActivate = function(field
) {
477 setTimeout(function() {
478 Field
.activate(field
);
482 Ajax
.InPlaceEditor
= Class
.create({
483 initialize: function(element
, url
, options
) {
485 this.element
= element
= $(element
);
486 this.prepareOptions();
487 this._controls
= { };
488 arguments
.callee
.dealWithDeprecatedOptions(options
); // DEPRECATION LAYER!!!
489 Object
.extend(this.options
, options
|| { });
490 if (!this.options
.formId
&& this.element
.id
) {
491 this.options
.formId
= this.element
.id
+ '-inplaceeditor';
492 if ($(this.options
.formId
))
493 this.options
.formId
= '';
495 if (this.options
.externalControl
)
496 this.options
.externalControl
= $(this.options
.externalControl
);
497 if (!this.options
.externalControl
)
498 this.options
.externalControlOnly
= false;
499 this._originalBackground
= this.element
.getStyle('background-color') || 'transparent';
500 this.element
.title
= this.options
.clickToEditText
;
501 this._boundCancelHandler
= this.handleFormCancellation
.bind(this);
502 this._boundComplete
= (this.options
.onComplete
|| Prototype
.emptyFunction
).bind(this);
503 this._boundFailureHandler
= this.handleAJAXFailure
.bind(this);
504 this._boundSubmitHandler
= this.handleFormSubmission
.bind(this);
505 this._boundWrapperHandler
= this.wrapUp
.bind(this);
506 this.registerListeners();
508 checkForEscapeOrReturn: function(e
) {
509 if (!this._editing
|| e
.ctrlKey
|| e
.altKey
|| e
.shiftKey
) return;
510 if (Event
.KEY_ESC
== e
.keyCode
)
511 this.handleFormCancellation(e
);
512 else if (Event
.KEY_RETURN
== e
.keyCode
)
513 this.handleFormSubmission(e
);
515 createControl: function(mode
, handler
, extraClasses
) {
516 var control
= this.options
[mode
+ 'Control'];
517 var text
= this.options
[mode
+ 'Text'];
518 if ('button' == control
) {
519 var btn
= document
.createElement('input');
522 btn
.className
= 'editor_' + mode
+ '_button';
523 if ('cancel' == mode
)
524 btn
.onclick
= this._boundCancelHandler
;
525 this._form
.appendChild(btn
);
526 this._controls
[mode
] = btn
;
527 } else if ('link' == control
) {
528 var link
= document
.createElement('a');
530 link
.appendChild(document
.createTextNode(text
));
531 link
.onclick
= 'cancel' == mode
? this._boundCancelHandler
: this._boundSubmitHandler
;
532 link
.className
= 'editor_' + mode
+ '_link';
534 link
.className
+= ' ' + extraClasses
;
535 this._form
.appendChild(link
);
536 this._controls
[mode
] = link
;
539 createEditField: function() {
540 var text
= (this.options
.loadTextURL
? this.options
.loadingText
: this.getText());
542 if (1 >= this.options
.rows
&& !/\r|\n/.test(this.getText())) {
543 fld
= document
.createElement('input');
545 var size
= this.options
.size
|| this.options
.cols
|| 0;
546 if (0 < size
) fld
.size
= size
;
548 fld
= document
.createElement('textarea');
549 fld
.rows
= (1 >= this.options
.rows
? this.options
.autoRows
: this.options
.rows
);
550 fld
.cols
= this.options
.cols
|| 40;
552 fld
.name
= this.options
.paramName
;
553 fld
.value
= text
; // No HTML breaks conversion anymore
554 fld
.className
= 'editor_field';
555 if (this.options
.submitOnBlur
)
556 fld
.onblur
= this._boundSubmitHandler
;
557 this._controls
.editor
= fld
;
558 if (this.options
.loadTextURL
)
559 this.loadExternalText();
560 this._form
.appendChild(this._controls
.editor
);
562 createForm: function() {
564 function addText(mode
, condition
) {
565 var text
= ipe
.options
['text' + mode
+ 'Controls'];
566 if (!text
|| condition
=== false) return;
567 ipe
._form
.appendChild(document
.createTextNode(text
));
569 this._form
= $(document
.createElement('form'));
570 this._form
.id
= this.options
.formId
;
571 this._form
.addClassName(this.options
.formClassName
);
572 this._form
.onsubmit
= this._boundSubmitHandler
;
573 this.createEditField();
574 if ('textarea' == this._controls
.editor
.tagName
.toLowerCase())
575 this._form
.appendChild(document
.createElement('br'));
576 if (this.options
.onFormCustomization
)
577 this.options
.onFormCustomization(this, this._form
);
578 addText('Before', this.options
.okControl
|| this.options
.cancelControl
);
579 this.createControl('ok', this._boundSubmitHandler
);
580 addText('Between', this.options
.okControl
&& this.options
.cancelControl
);
581 this.createControl('cancel', this._boundCancelHandler
, 'editor_cancel');
582 addText('After', this.options
.okControl
|| this.options
.cancelControl
);
584 destroy: function() {
585 if (this._oldInnerHTML
)
586 this.element
.innerHTML
= this._oldInnerHTML
;
587 this.leaveEditMode();
588 this.unregisterListeners();
590 enterEditMode: function(e
) {
591 if (this._saving
|| this._editing
) return;
592 this._editing
= true;
593 this.triggerCallback('onEnterEditMode');
594 if (this.options
.externalControl
)
595 this.options
.externalControl
.hide();
598 this.element
.parentNode
.insertBefore(this._form
, this.element
);
599 if (!this.options
.loadTextURL
)
600 this.postProcessEditField();
601 if (e
) Event
.stop(e
);
603 enterHover: function(e
) {
604 if (this.options
.hoverClassName
)
605 this.element
.addClassName(this.options
.hoverClassName
);
606 if (this._saving
) return;
607 this.triggerCallback('onEnterHover');
609 getText: function() {
610 return this.element
.innerHTML
;
612 handleAJAXFailure: function(transport
) {
613 this.triggerCallback('onFailure', transport
);
614 if (this._oldInnerHTML
) {
615 this.element
.innerHTML
= this._oldInnerHTML
;
616 this._oldInnerHTML
= null;
619 handleFormCancellation: function(e
) {
621 if (e
) Event
.stop(e
);
623 handleFormSubmission: function(e
) {
624 var form
= this._form
;
625 var value
= $F(this._controls
.editor
);
626 this.prepareSubmission();
627 var params
= this.options
.callback(form
, value
);
628 params
= (params
? params
+ '&' : '?') + 'editorId=' + this.element
.id
;
629 if (this.options
.htmlResponse
) {
630 var options
= Object
.extend({ evalScripts
: true }, this.options
.ajaxOptions
);
631 Object
.extend(options
, {
633 onComplete
: this._boundWrapperHandler
,
634 onFailure
: this._boundFailureHandler
636 new Ajax
.Updater({ success
: this.element
}, this.url
, options
);
638 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
639 Object
.extend(options
, {
641 onComplete
: this._boundWrapperHandler
,
642 onFailure
: this._boundFailureHandler
644 new Ajax
.Request(this.url
, options
);
646 if (e
) Event
.stop(e
);
648 leaveEditMode: function() {
649 this.element
.removeClassName(this.options
.savingClassName
);
652 this.element
.style
.backgroundColor
= this._originalBackground
;
654 if (this.options
.externalControl
)
655 this.options
.externalControl
.show();
656 this._saving
= false;
657 this._editing
= false;
658 this._oldInnerHTML
= null;
659 this.triggerCallback('onLeaveEditMode');
661 leaveHover: function(e
) {
662 if (this.options
.hoverClassName
)
663 this.element
.removeClassName(this.options
.hoverClassName
);
664 if (this._saving
) return;
665 this.triggerCallback('onLeaveHover');
667 loadExternalText: function() {
668 this._form
.addClassName(this.options
.loadingClassName
);
669 this._controls
.editor
.disabled
= true;
670 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
671 Object
.extend(options
, {
672 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
673 onComplete
: Prototype
.emptyFunction
,
674 onSuccess: function(transport
) {
675 this._form
.removeClassName(this.options
.loadingClassName
);
676 var text
= transport
.responseText
;
677 if (this.options
.stripLoadedTextTags
)
678 text
= text
.stripTags();
679 this._controls
.editor
.value
= text
;
680 this._controls
.editor
.disabled
= false;
681 this.postProcessEditField();
683 onFailure
: this._boundFailureHandler
685 new Ajax
.Request(this.options
.loadTextURL
, options
);
687 postProcessEditField: function() {
688 var fpc
= this.options
.fieldPostCreation
;
690 $(this._controls
.editor
)['focus' == fpc
? 'focus' : 'activate']();
692 prepareOptions: function() {
693 this.options
= Object
.clone(Ajax
.InPlaceEditor
.DefaultOptions
);
694 Object
.extend(this.options
, Ajax
.InPlaceEditor
.DefaultCallbacks
);
695 [this._extraDefaultOptions
].flatten().compact().each(function(defs
) {
696 Object
.extend(this.options
, defs
);
699 prepareSubmission: function() {
705 registerListeners: function() {
706 this._listeners
= { };
708 $H(Ajax
.InPlaceEditor
.Listeners
).each(function(pair
) {
709 listener
= this[pair
.value
].bind(this);
710 this._listeners
[pair
.key
] = listener
;
711 if (!this.options
.externalControlOnly
)
712 this.element
.observe(pair
.key
, listener
);
713 if (this.options
.externalControl
)
714 this.options
.externalControl
.observe(pair
.key
, listener
);
717 removeForm: function() {
718 if (!this._form
) return;
721 this._controls
= { };
723 showSaving: function() {
724 this._oldInnerHTML
= this.element
.innerHTML
;
725 this.element
.innerHTML
= this.options
.savingText
;
726 this.element
.addClassName(this.options
.savingClassName
);
727 this.element
.style
.backgroundColor
= this._originalBackground
;
730 triggerCallback: function(cbName
, arg
) {
731 if ('function' == typeof this.options
[cbName
]) {
732 this.options
[cbName
](this, arg
);
735 unregisterListeners: function() {
736 $H(this._listeners
).each(function(pair
) {
737 if (!this.options
.externalControlOnly
)
738 this.element
.stopObserving(pair
.key
, pair
.value
);
739 if (this.options
.externalControl
)
740 this.options
.externalControl
.stopObserving(pair
.key
, pair
.value
);
743 wrapUp: function(transport
) {
744 this.leaveEditMode();
745 // Can't use triggerCallback due to backward compatibility: requires
746 // binding + direct element
747 this._boundComplete(transport
, this.element
);
751 Object
.extend(Ajax
.InPlaceEditor
.prototype, {
752 dispose
: Ajax
.InPlaceEditor
.prototype.destroy
755 Ajax
.InPlaceCollectionEditor
= Class
.create(Ajax
.InPlaceEditor
, {
756 initialize: function($super, element
, url
, options
) {
757 this._extraDefaultOptions
= Ajax
.InPlaceCollectionEditor
.DefaultOptions
;
758 $super(element
, url
, options
);
761 createEditField: function() {
762 var list
= document
.createElement('select');
763 list
.name
= this.options
.paramName
;
765 this._controls
.editor
= list
;
766 this._collection
= this.options
.collection
|| [];
767 if (this.options
.loadCollectionURL
)
768 this.loadCollection();
770 this.checkForExternalText();
771 this._form
.appendChild(this._controls
.editor
);
774 loadCollection: function() {
775 this._form
.addClassName(this.options
.loadingClassName
);
776 this.showLoadingText(this.options
.loadingCollectionText
);
777 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
778 Object
.extend(options
, {
779 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
780 onComplete
: Prototype
.emptyFunction
,
781 onSuccess: function(transport
) {
782 var js
= transport
.responseText
.strip();
783 if (!/^\[.*\]$/.test(js
)) // TODO: improve sanity check
784 throw 'Server returned an invalid collection representation.';
785 this._collection
= eval(js
);
786 this.checkForExternalText();
788 onFailure
: this.onFailure
790 new Ajax
.Request(this.options
.loadCollectionURL
, options
);
793 showLoadingText: function(text
) {
794 this._controls
.editor
.disabled
= true;
795 var tempOption
= this._controls
.editor
.firstChild
;
797 tempOption
= document
.createElement('option');
798 tempOption
.value
= '';
799 this._controls
.editor
.appendChild(tempOption
);
800 tempOption
.selected
= true;
802 tempOption
.update((text
|| '').stripScripts().stripTags());
805 checkForExternalText: function() {
806 this._text
= this.getText();
807 if (this.options
.loadTextURL
)
808 this.loadExternalText();
810 this.buildOptionList();
813 loadExternalText: function() {
814 this.showLoadingText(this.options
.loadingText
);
815 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
816 Object
.extend(options
, {
817 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
818 onComplete
: Prototype
.emptyFunction
,
819 onSuccess: function(transport
) {
820 this._text
= transport
.responseText
.strip();
821 this.buildOptionList();
823 onFailure
: this.onFailure
825 new Ajax
.Request(this.options
.loadTextURL
, options
);
828 buildOptionList: function() {
829 this._form
.removeClassName(this.options
.loadingClassName
);
830 this._collection
= this._collection
.map(function(entry
) {
831 return 2 === entry
.length
? entry
: [entry
, entry
].flatten();
833 var marker
= ('value' in this.options
) ? this.options
.value
: this._text
;
834 var textFound
= this._collection
.any(function(entry
) {
835 return entry
[0] == marker
;
837 this._controls
.editor
.update('');
839 this._collection
.each(function(entry
, index
) {
840 option
= document
.createElement('option');
841 option
.value
= entry
[0];
842 option
.selected
= textFound
? entry
[0] == marker
: 0 == index
;
843 option
.appendChild(document
.createTextNode(entry
[1]));
844 this._controls
.editor
.appendChild(option
);
846 this._controls
.editor
.disabled
= false;
847 Field
.scrollFreeActivate(this._controls
.editor
);
851 //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
852 //**** This only exists for a while, in order to let ****
853 //**** users adapt to the new API. Read up on the new ****
854 //**** API and convert your code to it ASAP! ****
856 Ajax
.InPlaceEditor
.prototype.initialize
.dealWithDeprecatedOptions = function(options
) {
857 if (!options
) return;
858 function fallback(name
, expr
) {
859 if (name
in options
|| expr
=== undefined) return;
860 options
[name
] = expr
;
862 fallback('cancelControl', (options
.cancelLink
? 'link' : (options
.cancelButton
? 'button' :
863 options
.cancelLink
== options
.cancelButton
== false ? false : undefined)));
864 fallback('okControl', (options
.okLink
? 'link' : (options
.okButton
? 'button' :
865 options
.okLink
== options
.okButton
== false ? false : undefined)));
866 fallback('highlightColor', options
.highlightcolor
);
867 fallback('highlightEndColor', options
.highlightendcolor
);
870 Object
.extend(Ajax
.InPlaceEditor
, {
873 autoRows
: 3, // Use when multi-line w/ rows == 1
874 cancelControl
: 'link', // 'link'|'button'|false
875 cancelText
: 'cancel',
876 clickToEditText
: 'Click to edit',
877 externalControl
: null, // id|elt
878 externalControlOnly
: false,
879 fieldPostCreation
: 'activate', // 'activate'|'focus'|false
880 formClassName
: 'inplaceeditor-form',
881 formId
: null, // id|elt
882 highlightColor
: '#ffff99',
883 highlightEndColor
: '#ffffff',
886 loadingClassName
: 'inplaceeditor-loading',
887 loadingText
: 'Loading...',
888 okControl
: 'button', // 'link'|'button'|false
891 rows
: 1, // If 1 and multi-line, uses autoRows
892 savingClassName
: 'inplaceeditor-saving',
893 savingText
: 'Saving...',
895 stripLoadedTextTags
: false,
897 textAfterControls
: '',
898 textBeforeControls
: '',
899 textBetweenControls
: ''
902 callback: function(form
) {
903 return Form
.serialize(form
);
905 onComplete: function(transport
, element
) {
906 // For backward compatibility, this one is bound to the IPE, and passes
907 // the element directly. It was too often customized, so we don't break it.
908 new Effect
.Highlight(element
, {
909 startcolor
: this.options
.highlightColor
, keepBackgroundImage
: true });
911 onEnterEditMode
: null,
912 onEnterHover: function(ipe
) {
913 ipe
.element
.style
.backgroundColor
= ipe
.options
.highlightColor
;
915 ipe
._effect
.cancel();
917 onFailure: function(transport
, ipe
) {
918 alert('Error communication with the server: ' + transport
.responseText
.stripTags());
920 onFormCustomization
: null, // Takes the IPE and its generated form, after editor, before controls.
921 onLeaveEditMode
: null,
922 onLeaveHover: function(ipe
) {
923 ipe
._effect
= new Effect
.Highlight(ipe
.element
, {
924 startcolor
: ipe
.options
.highlightColor
, endcolor
: ipe
.options
.highlightEndColor
,
925 restorecolor
: ipe
._originalBackground
, keepBackgroundImage
: true
930 click
: 'enterEditMode',
931 keydown
: 'checkForEscapeOrReturn',
932 mouseover
: 'enterHover',
933 mouseout
: 'leaveHover'
937 Ajax
.InPlaceCollectionEditor
.DefaultOptions
= {
938 loadingCollectionText
: 'Loading options...'
941 // Delayed observer, like Form.Element.Observer,
942 // but waits for delay after last key input
943 // Ideal for live-search fields
945 Form
.Element
.DelayedObserver
= Class
.create({
946 initialize: function(element
, delay
, callback
) {
947 this.delay
= delay
|| 0.5;
948 this.element
= $(element
);
949 this.callback
= callback
;
951 this.lastValue
= $F(this.element
);
952 Event
.observe(this.element
,'keyup',this.delayedListener
.bindAsEventListener(this));
954 delayedListener: function(event
) {
955 if(this.lastValue
== $F(this.element
)) return;
956 if(this.timer
) clearTimeout(this.timer
);
957 this.timer
= setTimeout(this.onTimerEvent
.bind(this), this.delay
* 1000);
958 this.lastValue
= $F(this.element
);
960 onTimerEvent: function() {
962 this.callback(this.element
, $F(this.element
));