ca29aefdd1fb255e3da9909437735b5c84492ae5
1 // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 // (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3 // (c) 2005-2008 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 if(typeof Effect
== 'undefined')
38 throw("controls.js requires including script.aculo.us' effects.js library");
40 var Autocompleter
= { };
41 Autocompleter
.Base
= Class
.create({
42 baseInitialize: function(element
, update
, options
) {
44 this.element
= element
;
45 this.update
= $(update
);
46 this.hasFocus
= false;
51 this.oldElementValue
= this.element
.value
;
54 this.setOptions(options
);
56 this.options
= options
|| { };
58 this.options
.paramName
= this.options
.paramName
|| this.element
.name
;
59 this.options
.tokens
= this.options
.tokens
|| [];
60 this.options
.frequency
= this.options
.frequency
|| 0.4;
61 this.options
.minChars
= this.options
.minChars
|| 1;
62 this.options
.onShow
= this.options
.onShow
||
63 function(element
, update
){
64 if(!update
.style
.position
|| update
.style
.position
=='absolute') {
65 update
.style
.position
= 'absolute';
66 Position
.clone(element
, update
, {
68 offsetTop
: element
.offsetHeight
71 Effect
.Appear(update
,{duration
:0.15});
73 this.options
.onHide
= this.options
.onHide
||
74 function(element
, update
){ new Effect
.Fade(update
,{duration
:0.15}) };
76 if(typeof(this.options
.tokens
) == 'string')
77 this.options
.tokens
= new Array(this.options
.tokens
);
78 // Force carriage returns as token delimiters anyway
79 if (!this.options
.tokens
.include('\n'))
80 this.options
.tokens
.push('\n');
84 this.element
.setAttribute('autocomplete','off');
86 Element
.hide(this.update
);
88 Event
.observe(this.element
, 'blur', this.onBlur
.bindAsEventListener(this));
89 Event
.observe(this.element
, 'keydown', this.onKeyPress
.bindAsEventListener(this));
93 if(Element
.getStyle(this.update
, 'display')=='none') this.options
.onShow(this.element
, this.update
);
95 (Prototype
.Browser
.IE
) &&
96 (Element
.getStyle(this.update
, 'position')=='absolute')) {
97 new Insertion
.After(this.update
,
98 '<iframe id="' + this.update
.id
+ '_iefix" '+
99 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
100 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
101 this.iefix
= $(this.update
.id
+'_iefix');
103 if(this.iefix
) setTimeout(this.fixIEOverlapping
.bind(this), 50);
106 fixIEOverlapping: function() {
107 Position
.clone(this.update
, this.iefix
, {setTop
:(!this.update
.style
.height
)});
108 this.iefix
.style
.zIndex
= 1;
109 this.update
.style
.zIndex
= 2;
110 Element
.show(this.iefix
);
114 this.stopIndicator();
115 if(Element
.getStyle(this.update
, 'display')!='none') this.options
.onHide(this.element
, this.update
);
116 if(this.iefix
) Element
.hide(this.iefix
);
119 startIndicator: function() {
120 if(this.options
.indicator
) Element
.show(this.options
.indicator
);
123 stopIndicator: function() {
124 if(this.options
.indicator
) Element
.hide(this.options
.indicator
);
127 onKeyPress: function(event
) {
129 switch(event
.keyCode
) {
131 case Event
.KEY_RETURN
:
140 case Event
.KEY_RIGHT
:
154 if(event
.keyCode
==Event
.KEY_TAB
|| event
.keyCode
==Event
.KEY_RETURN
||
155 (Prototype
.Browser
.WebKit
> 0 && event
.keyCode
== 0)) return;
158 this.hasFocus
= true;
160 if(this.observer
) clearTimeout(this.observer
);
162 setTimeout(this.onObserverEvent
.bind(this), this.options
.frequency
*1000);
165 activate: function() {
166 this.changed
= false;
167 this.hasFocus
= true;
168 this.getUpdatedChoices();
171 onHover: function(event
) {
172 var element
= Event
.findElement(event
, 'LI');
173 if(this.index
!= element
.autocompleteIndex
)
175 this.index
= element
.autocompleteIndex
;
181 onClick: function(event
) {
182 var element
= Event
.findElement(event
, 'LI');
183 this.index
= element
.autocompleteIndex
;
188 onBlur: function(event
) {
189 // needed to make click events working
190 setTimeout(this.hide
.bind(this), 250);
191 this.hasFocus
= false;
196 if(this.entryCount
> 0) {
197 for (var i
= 0; i
< this.entryCount
; i
++)
199 Element
.addClassName(this.getEntry(i
),"selected") :
200 Element
.removeClassName(this.getEntry(i
),"selected");
211 markPrevious: function() {
212 if(this.index
> 0) this.index
--;
213 else this.index
= this.entryCount
-1;
214 this.getEntry(this.index
).scrollIntoView(true);
217 markNext: function() {
218 if(this.index
< this.entryCount
-1) this.index
++;
220 this.getEntry(this.index
).scrollIntoView(false);
223 getEntry: function(index
) {
224 return this.update
.firstChild
.childNodes
[index
];
227 getCurrentEntry: function() {
228 return this.getEntry(this.index
);
231 selectEntry: function() {
233 this.updateElement(this.getCurrentEntry());
236 updateElement: function(selectedElement
) {
237 if (this.options
.updateElement
) {
238 this.options
.updateElement(selectedElement
);
242 if (this.options
.select
) {
243 var nodes
= $(selectedElement
).select('.' + this.options
.select
) || [];
244 if(nodes
.length
>0) value
= Element
.collectTextNodes(nodes
[0], this.options
.select
);
246 value
= Element
.collectTextNodesIgnoreClass(selectedElement
, 'informal');
248 var bounds
= this.getTokenBounds();
249 if (bounds
[0] != -1) {
250 var newValue
= this.element
.value
.substr(0, bounds
[0]);
251 var whitespace
= this.element
.value
.substr(bounds
[0]).match(/^\s+/);
253 newValue
+= whitespace
[0];
254 this.element
.value
= newValue
+ value
+ this.element
.value
.substr(bounds
[1]);
256 this.element
.value
= value
;
258 this.oldElementValue
= this.element
.value
;
259 this.element
.focus();
261 if (this.options
.afterUpdateElement
)
262 this.options
.afterUpdateElement(this.element
, selectedElement
);
265 updateChoices: function(choices
) {
266 if(!this.changed
&& this.hasFocus
) {
267 this.update
.innerHTML
= choices
;
268 Element
.cleanWhitespace(this.update
);
269 Element
.cleanWhitespace(this.update
.down());
271 if(this.update
.firstChild
&& this.update
.down().childNodes
) {
273 this.update
.down().childNodes
.length
;
274 for (var i
= 0; i
< this.entryCount
; i
++) {
275 var entry
= this.getEntry(i
);
276 entry
.autocompleteIndex
= i
;
277 this.addObservers(entry
);
283 this.stopIndicator();
286 if(this.entryCount
==1 && this.options
.autoSelect
) {
295 addObservers: function(element
) {
296 Event
.observe(element
, "mouseover", this.onHover
.bindAsEventListener(this));
297 Event
.observe(element
, "click", this.onClick
.bindAsEventListener(this));
300 onObserverEvent: function() {
301 this.changed
= false;
302 this.tokenBounds
= null;
303 if(this.getToken().length
>=this.options
.minChars
) {
304 this.getUpdatedChoices();
309 this.oldElementValue
= this.element
.value
;
312 getToken: function() {
313 var bounds
= this.getTokenBounds();
314 return this.element
.value
.substring(bounds
[0], bounds
[1]).strip();
317 getTokenBounds: function() {
318 if (null != this.tokenBounds
) return this.tokenBounds
;
319 var value
= this.element
.value
;
320 if (value
.strip().empty()) return [-1, 0];
321 var diff
= arguments
.callee
.getFirstDifferencePos(value
, this.oldElementValue
);
322 var offset
= (diff
== this.oldElementValue
.length
? 1 : 0);
323 var prevTokenPos
= -1, nextTokenPos
= value
.length
;
325 for (var index
= 0, l
= this.options
.tokens
.length
; index
< l
; ++index
) {
326 tp
= value
.lastIndexOf(this.options
.tokens
[index
], diff
+ offset
- 1);
327 if (tp
> prevTokenPos
) prevTokenPos
= tp
;
328 tp
= value
.indexOf(this.options
.tokens
[index
], diff
+ offset
);
329 if (-1 != tp
&& tp
< nextTokenPos
) nextTokenPos
= tp
;
331 return (this.tokenBounds
= [prevTokenPos
+ 1, nextTokenPos
]);
335 Autocompleter
.Base
.prototype.getTokenBounds
.getFirstDifferencePos = function(newS
, oldS
) {
336 var boundary
= Math
.min(newS
.length
, oldS
.length
);
337 for (var index
= 0; index
< boundary
; ++index
)
338 if (newS
[index
] != oldS
[index
])
343 Ajax
.Autocompleter
= Class
.create(Autocompleter
.Base
, {
344 initialize: function(element
, update
, url
, options
) {
345 this.baseInitialize(element
, update
, options
);
346 this.options
.asynchronous
= true;
347 this.options
.onComplete
= this.onComplete
.bind(this);
348 this.options
.defaultParams
= this.options
.parameters
|| null;
352 getUpdatedChoices: function() {
353 this.startIndicator();
355 var entry
= encodeURIComponent(this.options
.paramName
) + '=' +
356 encodeURIComponent(this.getToken());
358 this.options
.parameters
= this.options
.callback
?
359 this.options
.callback(this.element
, entry
) : entry
;
361 if(this.options
.defaultParams
)
362 this.options
.parameters
+= '&' + this.options
.defaultParams
;
364 new Ajax
.Request(this.url
, this.options
);
367 onComplete: function(request
) {
368 this.updateChoices(request
.responseText
);
372 // The local array autocompleter. Used when you'd prefer to
373 // inject an array of autocompletion options into the page, rather
374 // than sending out Ajax queries, which can be quite slow sometimes.
376 // The constructor takes four parameters. The first two are, as usual,
377 // the id of the monitored textbox, and id of the autocompletion menu.
378 // The third is the array you want to autocomplete from, and the fourth
379 // is the options block.
381 // Extra local autocompletion options:
382 // - choices - How many autocompletion choices to offer
384 // - partialSearch - If false, the autocompleter will match entered
385 // text only at the beginning of strings in the
386 // autocomplete array. Defaults to true, which will
387 // match text at the beginning of any *word* in the
388 // strings in the autocomplete array. If you want to
389 // search anywhere in the string, additionally set
390 // the option fullSearch to true (default: off).
392 // - fullSsearch - Search anywhere in autocomplete array strings.
394 // - partialChars - How many characters to enter before triggering
395 // a partial match (unlike minChars, which defines
396 // how many characters are required to do any match
397 // at all). Defaults to 2.
399 // - ignoreCase - Whether to ignore case when autocompleting.
402 // It's possible to pass in a custom function as the 'selector'
403 // option, if you prefer to write your own autocompletion logic.
404 // In that case, the other options above will not apply unless
407 Autocompleter
.Local
= Class
.create(Autocompleter
.Base
, {
408 initialize: function(element
, update
, array
, options
) {
409 this.baseInitialize(element
, update
, options
);
410 this.options
.array
= array
;
413 getUpdatedChoices: function() {
414 this.updateChoices(this.options
.selector(this));
417 setOptions: function(options
) {
418 this.options
= Object
.extend({
424 selector: function(instance
) {
425 var ret
= []; // Beginning matches
426 var partial
= []; // Inside matches
427 var entry
= instance
.getToken();
430 for (var i
= 0; i
< instance
.options
.array
.length
&&
431 ret
.length
< instance
.options
.choices
; i
++) {
433 var elem
= instance
.options
.array
[i
];
434 var foundPos
= instance
.options
.ignoreCase
?
435 elem
.toLowerCase().indexOf(entry
.toLowerCase()) :
438 while (foundPos
!= -1) {
439 if (foundPos
== 0 && elem
.length
!= entry
.length
) {
440 ret
.push("<li><strong>" + elem
.substr(0, entry
.length
) + "</strong>" +
441 elem
.substr(entry
.length
) + "</li>");
443 } else if (entry
.length
>= instance
.options
.partialChars
&&
444 instance
.options
.partialSearch
&& foundPos
!= -1) {
445 if (instance
.options
.fullSearch
|| /\s/.test(elem
.substr(foundPos
-1,1))) {
446 partial
.push("<li>" + elem
.substr(0, foundPos
) + "<strong>" +
447 elem
.substr(foundPos
, entry
.length
) + "</strong>" + elem
.substr(
448 foundPos
+ entry
.length
) + "</li>");
453 foundPos
= instance
.options
.ignoreCase
?
454 elem
.toLowerCase().indexOf(entry
.toLowerCase(), foundPos
+ 1) :
455 elem
.indexOf(entry
, foundPos
+ 1);
460 ret
= ret
.concat(partial
.slice(0, instance
.options
.choices
- ret
.length
));
461 return "<ul>" + ret
.join('') + "</ul>";
467 // AJAX in-place editor and collection editor
468 // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
470 // Use this if you notice weird scrolling problems on some browsers,
471 // the DOM might be a bit confused when this gets called so do this
472 // waits 1 ms (with setTimeout) until it does the activation
473 Field
.scrollFreeActivate = function(field
) {
474 setTimeout(function() {
475 Field
.activate(field
);
479 Ajax
.InPlaceEditor
= Class
.create({
480 initialize: function(element
, url
, options
) {
482 this.element
= element
= $(element
);
483 this.prepareOptions();
484 this._controls
= { };
485 arguments
.callee
.dealWithDeprecatedOptions(options
); // DEPRECATION LAYER!!!
486 Object
.extend(this.options
, options
|| { });
487 if (!this.options
.formId
&& this.element
.id
) {
488 this.options
.formId
= this.element
.id
+ '-inplaceeditor';
489 if ($(this.options
.formId
))
490 this.options
.formId
= '';
492 if (this.options
.externalControl
)
493 this.options
.externalControl
= $(this.options
.externalControl
);
494 if (!this.options
.externalControl
)
495 this.options
.externalControlOnly
= false;
496 this._originalBackground
= this.element
.getStyle('background-color') || 'transparent';
497 this.element
.title
= this.options
.clickToEditText
;
498 this._boundCancelHandler
= this.handleFormCancellation
.bind(this);
499 this._boundComplete
= (this.options
.onComplete
|| Prototype
.emptyFunction
).bind(this);
500 this._boundFailureHandler
= this.handleAJAXFailure
.bind(this);
501 this._boundSubmitHandler
= this.handleFormSubmission
.bind(this);
502 this._boundWrapperHandler
= this.wrapUp
.bind(this);
503 this.registerListeners();
505 checkForEscapeOrReturn: function(e
) {
506 if (!this._editing
|| e
.ctrlKey
|| e
.altKey
|| e
.shiftKey
) return;
507 if (Event
.KEY_ESC
== e
.keyCode
)
508 this.handleFormCancellation(e
);
509 else if (Event
.KEY_RETURN
== e
.keyCode
)
510 this.handleFormSubmission(e
);
512 createControl: function(mode
, handler
, extraClasses
) {
513 var control
= this.options
[mode
+ 'Control'];
514 var text
= this.options
[mode
+ 'Text'];
515 if ('button' == control
) {
516 var btn
= document
.createElement('input');
519 btn
.className
= 'editor_' + mode
+ '_button';
520 if ('cancel' == mode
)
521 btn
.onclick
= this._boundCancelHandler
;
522 this._form
.appendChild(btn
);
523 this._controls
[mode
] = btn
;
524 } else if ('link' == control
) {
525 var link
= document
.createElement('a');
527 link
.appendChild(document
.createTextNode(text
));
528 link
.onclick
= 'cancel' == mode
? this._boundCancelHandler
: this._boundSubmitHandler
;
529 link
.className
= 'editor_' + mode
+ '_link';
531 link
.className
+= ' ' + extraClasses
;
532 this._form
.appendChild(link
);
533 this._controls
[mode
] = link
;
536 createEditField: function() {
537 var text
= (this.options
.loadTextURL
? this.options
.loadingText
: this.getText());
539 if (1 >= this.options
.rows
&& !/\r|\n/.test(this.getText())) {
540 fld
= document
.createElement('input');
542 var size
= this.options
.size
|| this.options
.cols
|| 0;
543 if (0 < size
) fld
.size
= size
;
545 fld
= document
.createElement('textarea');
546 fld
.rows
= (1 >= this.options
.rows
? this.options
.autoRows
: this.options
.rows
);
547 fld
.cols
= this.options
.cols
|| 40;
549 fld
.name
= this.options
.paramName
;
550 fld
.value
= text
; // No HTML breaks conversion anymore
551 fld
.className
= 'editor_field';
552 if (this.options
.submitOnBlur
)
553 fld
.onblur
= this._boundSubmitHandler
;
554 this._controls
.editor
= fld
;
555 if (this.options
.loadTextURL
)
556 this.loadExternalText();
557 this._form
.appendChild(this._controls
.editor
);
559 createForm: function() {
561 function addText(mode
, condition
) {
562 var text
= ipe
.options
['text' + mode
+ 'Controls'];
563 if (!text
|| condition
=== false) return;
564 ipe
._form
.appendChild(document
.createTextNode(text
));
566 this._form
= $(document
.createElement('form'));
567 this._form
.id
= this.options
.formId
;
568 this._form
.addClassName(this.options
.formClassName
);
569 this._form
.onsubmit
= this._boundSubmitHandler
;
570 this.createEditField();
571 if ('textarea' == this._controls
.editor
.tagName
.toLowerCase())
572 this._form
.appendChild(document
.createElement('br'));
573 if (this.options
.onFormCustomization
)
574 this.options
.onFormCustomization(this, this._form
);
575 addText('Before', this.options
.okControl
|| this.options
.cancelControl
);
576 this.createControl('ok', this._boundSubmitHandler
);
577 addText('Between', this.options
.okControl
&& this.options
.cancelControl
);
578 this.createControl('cancel', this._boundCancelHandler
, 'editor_cancel');
579 addText('After', this.options
.okControl
|| this.options
.cancelControl
);
581 destroy: function() {
582 if (this._oldInnerHTML
)
583 this.element
.innerHTML
= this._oldInnerHTML
;
584 this.leaveEditMode();
585 this.unregisterListeners();
587 enterEditMode: function(e
) {
588 if (this._saving
|| this._editing
) return;
589 this._editing
= true;
590 this.triggerCallback('onEnterEditMode');
591 if (this.options
.externalControl
)
592 this.options
.externalControl
.hide();
595 this.element
.parentNode
.insertBefore(this._form
, this.element
);
596 if (!this.options
.loadTextURL
)
597 this.postProcessEditField();
598 if (e
) Event
.stop(e
);
600 enterHover: function(e
) {
601 if (this.options
.hoverClassName
)
602 this.element
.addClassName(this.options
.hoverClassName
);
603 if (this._saving
) return;
604 this.triggerCallback('onEnterHover');
606 getText: function() {
607 return this.element
.innerHTML
.unescapeHTML();
609 handleAJAXFailure: function(transport
) {
610 this.triggerCallback('onFailure', transport
);
611 if (this._oldInnerHTML
) {
612 this.element
.innerHTML
= this._oldInnerHTML
;
613 this._oldInnerHTML
= null;
616 handleFormCancellation: function(e
) {
618 if (e
) Event
.stop(e
);
620 handleFormSubmission: function(e
) {
621 var form
= this._form
;
622 var value
= $F(this._controls
.editor
);
623 this.prepareSubmission();
624 var params
= this.options
.callback(form
, value
) || '';
625 if (Object
.isString(params
))
626 params
= params
.toQueryParams();
627 params
.editorId
= this.element
.id
;
628 if (this.options
.htmlResponse
) {
629 var options
= Object
.extend({ evalScripts
: true }, this.options
.ajaxOptions
);
630 Object
.extend(options
, {
632 onComplete
: this._boundWrapperHandler
,
633 onFailure
: this._boundFailureHandler
635 new Ajax
.Updater({ success
: this.element
}, this.url
, options
);
637 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
638 Object
.extend(options
, {
640 onComplete
: this._boundWrapperHandler
,
641 onFailure
: this._boundFailureHandler
643 new Ajax
.Request(this.url
, options
);
645 if (e
) Event
.stop(e
);
647 leaveEditMode: function() {
648 this.element
.removeClassName(this.options
.savingClassName
);
651 this.element
.style
.backgroundColor
= this._originalBackground
;
653 if (this.options
.externalControl
)
654 this.options
.externalControl
.show();
655 this._saving
= false;
656 this._editing
= false;
657 this._oldInnerHTML
= null;
658 this.triggerCallback('onLeaveEditMode');
660 leaveHover: function(e
) {
661 if (this.options
.hoverClassName
)
662 this.element
.removeClassName(this.options
.hoverClassName
);
663 if (this._saving
) return;
664 this.triggerCallback('onLeaveHover');
666 loadExternalText: function() {
667 this._form
.addClassName(this.options
.loadingClassName
);
668 this._controls
.editor
.disabled
= true;
669 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
670 Object
.extend(options
, {
671 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
672 onComplete
: Prototype
.emptyFunction
,
673 onSuccess: function(transport
) {
674 this._form
.removeClassName(this.options
.loadingClassName
);
675 var text
= transport
.responseText
;
676 if (this.options
.stripLoadedTextTags
)
677 text
= text
.stripTags();
678 this._controls
.editor
.value
= text
;
679 this._controls
.editor
.disabled
= false;
680 this.postProcessEditField();
682 onFailure
: this._boundFailureHandler
684 new Ajax
.Request(this.options
.loadTextURL
, options
);
686 postProcessEditField: function() {
687 var fpc
= this.options
.fieldPostCreation
;
689 $(this._controls
.editor
)['focus' == fpc
? 'focus' : 'activate']();
691 prepareOptions: function() {
692 this.options
= Object
.clone(Ajax
.InPlaceEditor
.DefaultOptions
);
693 Object
.extend(this.options
, Ajax
.InPlaceEditor
.DefaultCallbacks
);
694 [this._extraDefaultOptions
].flatten().compact().each(function(defs
) {
695 Object
.extend(this.options
, defs
);
698 prepareSubmission: function() {
704 registerListeners: function() {
705 this._listeners
= { };
707 $H(Ajax
.InPlaceEditor
.Listeners
).each(function(pair
) {
708 listener
= this[pair
.value
].bind(this);
709 this._listeners
[pair
.key
] = listener
;
710 if (!this.options
.externalControlOnly
)
711 this.element
.observe(pair
.key
, listener
);
712 if (this.options
.externalControl
)
713 this.options
.externalControl
.observe(pair
.key
, listener
);
716 removeForm: function() {
717 if (!this._form
) return;
720 this._controls
= { };
722 showSaving: function() {
723 this._oldInnerHTML
= this.element
.innerHTML
;
724 this.element
.innerHTML
= this.options
.savingText
;
725 this.element
.addClassName(this.options
.savingClassName
);
726 this.element
.style
.backgroundColor
= this._originalBackground
;
729 triggerCallback: function(cbName
, arg
) {
730 if ('function' == typeof this.options
[cbName
]) {
731 this.options
[cbName
](this, arg
);
734 unregisterListeners: function() {
735 $H(this._listeners
).each(function(pair
) {
736 if (!this.options
.externalControlOnly
)
737 this.element
.stopObserving(pair
.key
, pair
.value
);
738 if (this.options
.externalControl
)
739 this.options
.externalControl
.stopObserving(pair
.key
, pair
.value
);
742 wrapUp: function(transport
) {
743 this.leaveEditMode();
744 // Can't use triggerCallback due to backward compatibility: requires
745 // binding + direct element
746 this._boundComplete(transport
, this.element
);
750 Object
.extend(Ajax
.InPlaceEditor
.prototype, {
751 dispose
: Ajax
.InPlaceEditor
.prototype.destroy
754 Ajax
.InPlaceCollectionEditor
= Class
.create(Ajax
.InPlaceEditor
, {
755 initialize: function($super, element
, url
, options
) {
756 this._extraDefaultOptions
= Ajax
.InPlaceCollectionEditor
.DefaultOptions
;
757 $super(element
, url
, options
);
760 createEditField: function() {
761 var list
= document
.createElement('select');
762 list
.name
= this.options
.paramName
;
764 this._controls
.editor
= list
;
765 this._collection
= this.options
.collection
|| [];
766 if (this.options
.loadCollectionURL
)
767 this.loadCollection();
769 this.checkForExternalText();
770 this._form
.appendChild(this._controls
.editor
);
773 loadCollection: function() {
774 this._form
.addClassName(this.options
.loadingClassName
);
775 this.showLoadingText(this.options
.loadingCollectionText
);
776 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
777 Object
.extend(options
, {
778 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
779 onComplete
: Prototype
.emptyFunction
,
780 onSuccess: function(transport
) {
781 var js
= transport
.responseText
.strip();
782 if (!/^\[.*\]$/.test(js
)) // TODO: improve sanity check
783 throw('Server returned an invalid collection representation.');
784 this._collection
= eval(js
);
785 this.checkForExternalText();
787 onFailure
: this.onFailure
789 new Ajax
.Request(this.options
.loadCollectionURL
, options
);
792 showLoadingText: function(text
) {
793 this._controls
.editor
.disabled
= true;
794 var tempOption
= this._controls
.editor
.firstChild
;
796 tempOption
= document
.createElement('option');
797 tempOption
.value
= '';
798 this._controls
.editor
.appendChild(tempOption
);
799 tempOption
.selected
= true;
801 tempOption
.update((text
|| '').stripScripts().stripTags());
804 checkForExternalText: function() {
805 this._text
= this.getText();
806 if (this.options
.loadTextURL
)
807 this.loadExternalText();
809 this.buildOptionList();
812 loadExternalText: function() {
813 this.showLoadingText(this.options
.loadingText
);
814 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
815 Object
.extend(options
, {
816 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
817 onComplete
: Prototype
.emptyFunction
,
818 onSuccess: function(transport
) {
819 this._text
= transport
.responseText
.strip();
820 this.buildOptionList();
822 onFailure
: this.onFailure
824 new Ajax
.Request(this.options
.loadTextURL
, options
);
827 buildOptionList: function() {
828 this._form
.removeClassName(this.options
.loadingClassName
);
829 this._collection
= this._collection
.map(function(entry
) {
830 return 2 === entry
.length
? entry
: [entry
, entry
].flatten();
832 var marker
= ('value' in this.options
) ? this.options
.value
: this._text
;
833 var textFound
= this._collection
.any(function(entry
) {
834 return entry
[0] == marker
;
836 this._controls
.editor
.update('');
838 this._collection
.each(function(entry
, index
) {
839 option
= document
.createElement('option');
840 option
.value
= entry
[0];
841 option
.selected
= textFound
? entry
[0] == marker
: 0 == index
;
842 option
.appendChild(document
.createTextNode(entry
[1]));
843 this._controls
.editor
.appendChild(option
);
845 this._controls
.editor
.disabled
= false;
846 Field
.scrollFreeActivate(this._controls
.editor
);
850 //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
851 //**** This only exists for a while, in order to let ****
852 //**** users adapt to the new API. Read up on the new ****
853 //**** API and convert your code to it ASAP! ****
855 Ajax
.InPlaceEditor
.prototype.initialize
.dealWithDeprecatedOptions = function(options
) {
856 if (!options
) return;
857 function fallback(name
, expr
) {
858 if (name
in options
|| expr
=== undefined) return;
859 options
[name
] = expr
;
861 fallback('cancelControl', (options
.cancelLink
? 'link' : (options
.cancelButton
? 'button' :
862 options
.cancelLink
== options
.cancelButton
== false ? false : undefined)));
863 fallback('okControl', (options
.okLink
? 'link' : (options
.okButton
? 'button' :
864 options
.okLink
== options
.okButton
== false ? false : undefined)));
865 fallback('highlightColor', options
.highlightcolor
);
866 fallback('highlightEndColor', options
.highlightendcolor
);
869 Object
.extend(Ajax
.InPlaceEditor
, {
872 autoRows
: 3, // Use when multi-line w/ rows == 1
873 cancelControl
: 'link', // 'link'|'button'|false
874 cancelText
: 'cancel',
875 clickToEditText
: 'Click to edit',
876 externalControl
: null, // id|elt
877 externalControlOnly
: false,
878 fieldPostCreation
: 'activate', // 'activate'|'focus'|false
879 formClassName
: 'inplaceeditor-form',
880 formId
: null, // id|elt
881 highlightColor
: '#ffff99',
882 highlightEndColor
: '#ffffff',
885 loadingClassName
: 'inplaceeditor-loading',
886 loadingText
: 'Loading...',
887 okControl
: 'button', // 'link'|'button'|false
890 rows
: 1, // If 1 and multi-line, uses autoRows
891 savingClassName
: 'inplaceeditor-saving',
892 savingText
: 'Saving...',
894 stripLoadedTextTags
: false,
896 textAfterControls
: '',
897 textBeforeControls
: '',
898 textBetweenControls
: ''
901 callback: function(form
) {
902 return Form
.serialize(form
);
904 onComplete: function(transport
, element
) {
905 // For backward compatibility, this one is bound to the IPE, and passes
906 // the element directly. It was too often customized, so we don't break it.
907 new Effect
.Highlight(element
, {
908 startcolor
: this.options
.highlightColor
, keepBackgroundImage
: true });
910 onEnterEditMode
: null,
911 onEnterHover: function(ipe
) {
912 ipe
.element
.style
.backgroundColor
= ipe
.options
.highlightColor
;
914 ipe
._effect
.cancel();
916 onFailure: function(transport
, ipe
) {
917 alert('Error communication with the server: ' + transport
.responseText
.stripTags());
919 onFormCustomization
: null, // Takes the IPE and its generated form, after editor, before controls.
920 onLeaveEditMode
: null,
921 onLeaveHover: function(ipe
) {
922 ipe
._effect
= new Effect
.Highlight(ipe
.element
, {
923 startcolor
: ipe
.options
.highlightColor
, endcolor
: ipe
.options
.highlightEndColor
,
924 restorecolor
: ipe
._originalBackground
, keepBackgroundImage
: true
929 click
: 'enterEditMode',
930 keydown
: 'checkForEscapeOrReturn',
931 mouseover
: 'enterHover',
932 mouseout
: 'leaveHover'
936 Ajax
.InPlaceCollectionEditor
.DefaultOptions
= {
937 loadingCollectionText
: 'Loading options...'
940 // Delayed observer, like Form.Element.Observer,
941 // but waits for delay after last key input
942 // Ideal for live-search fields
944 Form
.Element
.DelayedObserver
= Class
.create({
945 initialize: function(element
, delay
, callback
) {
946 this.delay
= delay
|| 0.5;
947 this.element
= $(element
);
948 this.callback
= callback
;
950 this.lastValue
= $F(this.element
);
951 Event
.observe(this.element
,'keyup',this.delayedListener
.bindAsEventListener(this));
953 delayedListener: function(event
) {
954 if(this.lastValue
== $F(this.element
)) return;
955 if(this.timer
) clearTimeout(this.timer
);
956 this.timer
= setTimeout(this.onTimerEvent
.bind(this), this.delay
* 1000);
957 this.lastValue
= $F(this.element
);
959 onTimerEvent: function() {
961 this.callback(this.element
, $F(this.element
));