]>
git.wh0rd.org - tt-rss.git/blob - lib/scriptaculous/controls.js
1 // script.aculo.us controls.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010
3 // Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
4 // (c) 2005-2010 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
5 // (c) 2005-2010 Jon Tirsen (http://www.tirsen.com)
11 // script.aculo.us is freely distributable under the terms of an MIT-style license.
12 // For details, see the script.aculo.us web site: http://script.aculo.us/
14 // Autocompleter.Base handles all the autocompletion functionality
15 // that's independent of the data source for autocompletion. This
16 // includes drawing the autocompletion menu, observing keyboard
17 // and mouse events, and similar.
19 // Specific autocompleters need to provide, at the very least,
20 // a getUpdatedChoices function that will be invoked every time
21 // the text inside the monitored textbox changes. This method
22 // should get the text for which to provide autocompletion by
23 // invoking this.getToken(), NOT by directly accessing
24 // this.element.value. This is to allow incremental tokenized
25 // autocompletion. Specific auto-completion logic (AJAX, etc)
26 // belongs in getUpdatedChoices.
28 // Tokenized incremental autocompletion is enabled automatically
29 // when an autocompleter is instantiated with the 'tokens' option
30 // in the options parameter, e.g.:
31 // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
32 // will incrementally autocomplete with a comma as the token.
33 // Additionally, ',' in the above example can be replaced with
34 // a token array, e.g. { tokens: [',', '\n'] } which
35 // enables autocompletion on multiple tokens. This is most
36 // useful when one of the tokens is \n (a newline), as it
37 // allows smart autocompletion after linebreaks.
39 if(typeof Effect
== 'undefined')
40 throw("controls.js requires including script.aculo.us' effects.js library");
42 var Autocompleter
= { };
43 Autocompleter
.Base
= Class
.create({
44 baseInitialize: function(element
, update
, options
) {
46 this.element
= element
;
47 this.update
= $(update
);
48 this.hasFocus
= false;
53 this.oldElementValue
= this.element
.value
;
56 this.setOptions(options
);
58 this.options
= options
|| { };
60 this.options
.paramName
= this.options
.paramName
|| this.element
.name
;
61 this.options
.tokens
= this.options
.tokens
|| [];
62 this.options
.frequency
= this.options
.frequency
|| 0.4;
63 this.options
.minChars
= this.options
.minChars
|| 1;
64 this.options
.onShow
= this.options
.onShow
||
65 function(element
, update
){
66 if(!update
.style
.position
|| update
.style
.position
=='absolute') {
67 update
.style
.position
= 'absolute';
68 Position
.clone(element
, update
, {
70 offsetTop
: element
.offsetHeight
73 Effect
.Appear(update
,{duration
:0.15});
75 this.options
.onHide
= this.options
.onHide
||
76 function(element
, update
){ new Effect
.Fade(update
,{duration
:0.15}) };
78 if(typeof(this.options
.tokens
) == 'string')
79 this.options
.tokens
= new Array(this.options
.tokens
);
80 // Force carriage returns as token delimiters anyway
81 if (!this.options
.tokens
.include('\n'))
82 this.options
.tokens
.push('\n');
86 this.element
.setAttribute('autocomplete','off');
88 Element
.hide(this.update
);
90 Event
.observe(this.element
, 'blur', this.onBlur
.bindAsEventListener(this));
91 Event
.observe(this.element
, 'keydown', this.onKeyPress
.bindAsEventListener(this));
95 if(Element
.getStyle(this.update
, 'display')=='none') this.options
.onShow(this.element
, this.update
);
97 (Prototype
.Browser
.IE
) &&
98 (Element
.getStyle(this.update
, 'position')=='absolute')) {
99 new Insertion
.After(this.update
,
100 '<iframe id="' + this.update
.id
+ '_iefix" '+
101 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
102 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
103 this.iefix
= $(this.update
.id
+'_iefix');
105 if(this.iefix
) setTimeout(this.fixIEOverlapping
.bind(this), 50);
108 fixIEOverlapping: function() {
109 Position
.clone(this.update
, this.iefix
, {setTop
:(!this.update
.style
.height
)});
110 this.iefix
.style
.zIndex
= 1;
111 this.update
.style
.zIndex
= 2;
112 Element
.show(this.iefix
);
116 this.stopIndicator();
117 if(Element
.getStyle(this.update
, 'display')!='none') this.options
.onHide(this.element
, this.update
);
118 if(this.iefix
) Element
.hide(this.iefix
);
121 startIndicator: function() {
122 if(this.options
.indicator
) Element
.show(this.options
.indicator
);
125 stopIndicator: function() {
126 if(this.options
.indicator
) Element
.hide(this.options
.indicator
);
129 onKeyPress: function(event
) {
131 switch(event
.keyCode
) {
133 case Event
.KEY_RETURN
:
142 case Event
.KEY_RIGHT
:
156 if(event
.keyCode
==Event
.KEY_TAB
|| event
.keyCode
==Event
.KEY_RETURN
||
157 (Prototype
.Browser
.WebKit
> 0 && event
.keyCode
== 0)) return;
160 this.hasFocus
= true;
162 if(this.observer
) clearTimeout(this.observer
);
164 setTimeout(this.onObserverEvent
.bind(this), this.options
.frequency
*1000);
167 activate: function() {
168 this.changed
= false;
169 this.hasFocus
= true;
170 this.getUpdatedChoices();
173 onHover: function(event
) {
174 var element
= Event
.findElement(event
, 'LI');
175 if(this.index
!= element
.autocompleteIndex
)
177 this.index
= element
.autocompleteIndex
;
183 onClick: function(event
) {
184 var element
= Event
.findElement(event
, 'LI');
185 this.index
= element
.autocompleteIndex
;
190 onBlur: function(event
) {
191 // needed to make click events working
192 setTimeout(this.hide
.bind(this), 250);
193 this.hasFocus
= false;
198 if(this.entryCount
> 0) {
199 for (var i
= 0; i
< this.entryCount
; i
++)
201 Element
.addClassName(this.getEntry(i
),"selected") :
202 Element
.removeClassName(this.getEntry(i
),"selected");
213 markPrevious: function() {
214 if(this.index
> 0) this.index
--;
215 else this.index
= this.entryCount
-1;
216 this.getEntry(this.index
).scrollIntoView(true);
219 markNext: function() {
220 if(this.index
< this.entryCount
-1) this.index
++;
222 this.getEntry(this.index
).scrollIntoView(false);
225 getEntry: function(index
) {
226 return this.update
.firstChild
.childNodes
[index
];
229 getCurrentEntry: function() {
230 return this.getEntry(this.index
);
233 selectEntry: function() {
235 this.updateElement(this.getCurrentEntry());
238 updateElement: function(selectedElement
) {
239 if (this.options
.updateElement
) {
240 this.options
.updateElement(selectedElement
);
244 if (this.options
.select
) {
245 var nodes
= $(selectedElement
).select('.' + this.options
.select
) || [];
246 if(nodes
.length
>0) value
= Element
.collectTextNodes(nodes
[0], this.options
.select
);
248 value
= Element
.collectTextNodesIgnoreClass(selectedElement
, 'informal');
250 var bounds
= this.getTokenBounds();
251 if (bounds
[0] != -1) {
252 var newValue
= this.element
.value
.substr(0, bounds
[0]);
253 var whitespace
= this.element
.value
.substr(bounds
[0]).match(/^\s+/);
255 newValue
+= whitespace
[0];
256 this.element
.value
= newValue
+ value
+ this.element
.value
.substr(bounds
[1]);
258 this.element
.value
= value
;
260 this.oldElementValue
= this.element
.value
;
261 this.element
.focus();
263 if (this.options
.afterUpdateElement
)
264 this.options
.afterUpdateElement(this.element
, selectedElement
);
267 updateChoices: function(choices
) {
268 if(!this.changed
&& this.hasFocus
) {
269 this.update
.innerHTML
= choices
;
270 Element
.cleanWhitespace(this.update
);
271 Element
.cleanWhitespace(this.update
.down());
273 if(this.update
.firstChild
&& this.update
.down().childNodes
) {
275 this.update
.down().childNodes
.length
;
276 for (var i
= 0; i
< this.entryCount
; i
++) {
277 var entry
= this.getEntry(i
);
278 entry
.autocompleteIndex
= i
;
279 this.addObservers(entry
);
285 this.stopIndicator();
288 if(this.entryCount
==1 && this.options
.autoSelect
) {
297 addObservers: function(element
) {
298 Event
.observe(element
, "mouseover", this.onHover
.bindAsEventListener(this));
299 Event
.observe(element
, "click", this.onClick
.bindAsEventListener(this));
302 onObserverEvent: function() {
303 this.changed
= false;
304 this.tokenBounds
= null;
305 if(this.getToken().length
>=this.options
.minChars
) {
306 this.getUpdatedChoices();
311 this.oldElementValue
= this.element
.value
;
314 getToken: function() {
315 var bounds
= this.getTokenBounds();
316 return this.element
.value
.substring(bounds
[0], bounds
[1]).strip();
319 getTokenBounds: function() {
320 if (null != this.tokenBounds
) return this.tokenBounds
;
321 var value
= this.element
.value
;
322 if (value
.strip().empty()) return [-1, 0];
323 var diff
= arguments
.callee
.getFirstDifferencePos(value
, this.oldElementValue
);
324 var offset
= (diff
== this.oldElementValue
.length
? 1 : 0);
325 var prevTokenPos
= -1, nextTokenPos
= value
.length
;
327 for (var index
= 0, l
= this.options
.tokens
.length
; index
< l
; ++index
) {
328 tp
= value
.lastIndexOf(this.options
.tokens
[index
], diff
+ offset
- 1);
329 if (tp
> prevTokenPos
) prevTokenPos
= tp
;
330 tp
= value
.indexOf(this.options
.tokens
[index
], diff
+ offset
);
331 if (-1 != tp
&& tp
< nextTokenPos
) nextTokenPos
= tp
;
333 return (this.tokenBounds
= [prevTokenPos
+ 1, nextTokenPos
]);
337 Autocompleter
.Base
.prototype.getTokenBounds
.getFirstDifferencePos = function(newS
, oldS
) {
338 var boundary
= Math
.min(newS
.length
, oldS
.length
);
339 for (var index
= 0; index
< boundary
; ++index
)
340 if (newS
[index
] != oldS
[index
])
345 Ajax
.Autocompleter
= Class
.create(Autocompleter
.Base
, {
346 initialize: function(element
, update
, url
, options
) {
347 this.baseInitialize(element
, update
, options
);
348 this.options
.asynchronous
= true;
349 this.options
.onComplete
= this.onComplete
.bind(this);
350 this.options
.defaultParams
= this.options
.parameters
|| null;
354 getUpdatedChoices: function() {
355 this.startIndicator();
357 var entry
= encodeURIComponent(this.options
.paramName
) + '=' +
358 encodeURIComponent(this.getToken());
360 this.options
.parameters
= this.options
.callback
?
361 this.options
.callback(this.element
, entry
) : entry
;
363 if(this.options
.defaultParams
)
364 this.options
.parameters
+= '&' + this.options
.defaultParams
;
366 new Ajax
.Request(this.url
, this.options
);
369 onComplete: function(request
) {
370 this.updateChoices(request
.responseText
);
374 // The local array autocompleter. Used when you'd prefer to
375 // inject an array of autocompletion options into the page, rather
376 // than sending out Ajax queries, which can be quite slow sometimes.
378 // The constructor takes four parameters. The first two are, as usual,
379 // the id of the monitored textbox, and id of the autocompletion menu.
380 // The third is the array you want to autocomplete from, and the fourth
381 // is the options block.
383 // Extra local autocompletion options:
384 // - choices - How many autocompletion choices to offer
386 // - partialSearch - If false, the autocompleter will match entered
387 // text only at the beginning of strings in the
388 // autocomplete array. Defaults to true, which will
389 // match text at the beginning of any *word* in the
390 // strings in the autocomplete array. If you want to
391 // search anywhere in the string, additionally set
392 // the option fullSearch to true (default: off).
394 // - fullSsearch - Search anywhere in autocomplete array strings.
396 // - partialChars - How many characters to enter before triggering
397 // a partial match (unlike minChars, which defines
398 // how many characters are required to do any match
399 // at all). Defaults to 2.
401 // - ignoreCase - Whether to ignore case when autocompleting.
404 // It's possible to pass in a custom function as the 'selector'
405 // option, if you prefer to write your own autocompletion logic.
406 // In that case, the other options above will not apply unless
409 Autocompleter
.Local
= Class
.create(Autocompleter
.Base
, {
410 initialize: function(element
, update
, array
, options
) {
411 this.baseInitialize(element
, update
, options
);
412 this.options
.array
= array
;
415 getUpdatedChoices: function() {
416 this.updateChoices(this.options
.selector(this));
419 setOptions: function(options
) {
420 this.options
= Object
.extend({
426 selector: function(instance
) {
427 var ret
= []; // Beginning matches
428 var partial
= []; // Inside matches
429 var entry
= instance
.getToken();
432 for (var i
= 0; i
< instance
.options
.array
.length
&&
433 ret
.length
< instance
.options
.choices
; i
++) {
435 var elem
= instance
.options
.array
[i
];
436 var foundPos
= instance
.options
.ignoreCase
?
437 elem
.toLowerCase().indexOf(entry
.toLowerCase()) :
440 while (foundPos
!= -1) {
441 if (foundPos
== 0 && elem
.length
!= entry
.length
) {
442 ret
.push("<li><strong>" + elem
.substr(0, entry
.length
) + "</strong>" +
443 elem
.substr(entry
.length
) + "</li>");
445 } else if (entry
.length
>= instance
.options
.partialChars
&&
446 instance
.options
.partialSearch
&& foundPos
!= -1) {
447 if (instance
.options
.fullSearch
|| /\s/.test(elem
.substr(foundPos
-1,1))) {
448 partial
.push("<li>" + elem
.substr(0, foundPos
) + "<strong>" +
449 elem
.substr(foundPos
, entry
.length
) + "</strong>" + elem
.substr(
450 foundPos
+ entry
.length
) + "</li>");
455 foundPos
= instance
.options
.ignoreCase
?
456 elem
.toLowerCase().indexOf(entry
.toLowerCase(), foundPos
+ 1) :
457 elem
.indexOf(entry
, foundPos
+ 1);
462 ret
= ret
.concat(partial
.slice(0, instance
.options
.choices
- ret
.length
));
463 return "<ul>" + ret
.join('') + "</ul>";
469 // AJAX in-place editor and collection editor
470 // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
472 // Use this if you notice weird scrolling problems on some browsers,
473 // the DOM might be a bit confused when this gets called so do this
474 // waits 1 ms (with setTimeout) until it does the activation
475 Field
.scrollFreeActivate = function(field
) {
476 setTimeout(function() {
477 Field
.activate(field
);
481 Ajax
.InPlaceEditor
= Class
.create({
482 initialize: function(element
, url
, options
) {
484 this.element
= element
= $(element
);
485 this.prepareOptions();
486 this._controls
= { };
487 arguments
.callee
.dealWithDeprecatedOptions(options
); // DEPRECATION LAYER!!!
488 Object
.extend(this.options
, options
|| { });
489 if (!this.options
.formId
&& this.element
.id
) {
490 this.options
.formId
= this.element
.id
+ '-inplaceeditor';
491 if ($(this.options
.formId
))
492 this.options
.formId
= '';
494 if (this.options
.externalControl
)
495 this.options
.externalControl
= $(this.options
.externalControl
);
496 if (!this.options
.externalControl
)
497 this.options
.externalControlOnly
= false;
498 this._originalBackground
= this.element
.getStyle('background-color') || 'transparent';
499 this.element
.title
= this.options
.clickToEditText
;
500 this._boundCancelHandler
= this.handleFormCancellation
.bind(this);
501 this._boundComplete
= (this.options
.onComplete
|| Prototype
.emptyFunction
).bind(this);
502 this._boundFailureHandler
= this.handleAJAXFailure
.bind(this);
503 this._boundSubmitHandler
= this.handleFormSubmission
.bind(this);
504 this._boundWrapperHandler
= this.wrapUp
.bind(this);
505 this.registerListeners();
507 checkForEscapeOrReturn: function(e
) {
508 if (!this._editing
|| e
.ctrlKey
|| e
.altKey
|| e
.shiftKey
) return;
509 if (Event
.KEY_ESC
== e
.keyCode
)
510 this.handleFormCancellation(e
);
511 else if (Event
.KEY_RETURN
== e
.keyCode
)
512 this.handleFormSubmission(e
);
514 createControl: function(mode
, handler
, extraClasses
) {
515 var control
= this.options
[mode
+ 'Control'];
516 var text
= this.options
[mode
+ 'Text'];
517 if ('button' == control
) {
518 var btn
= document
.createElement('input');
521 btn
.className
= 'editor_' + mode
+ '_button';
522 if ('cancel' == mode
)
523 btn
.onclick
= this._boundCancelHandler
;
524 this._form
.appendChild(btn
);
525 this._controls
[mode
] = btn
;
526 } else if ('link' == control
) {
527 var link
= document
.createElement('a');
529 link
.appendChild(document
.createTextNode(text
));
530 link
.onclick
= 'cancel' == mode
? this._boundCancelHandler
: this._boundSubmitHandler
;
531 link
.className
= 'editor_' + mode
+ '_link';
533 link
.className
+= ' ' + extraClasses
;
534 this._form
.appendChild(link
);
535 this._controls
[mode
] = link
;
538 createEditField: function() {
539 var text
= (this.options
.loadTextURL
? this.options
.loadingText
: this.getText());
541 if (1 >= this.options
.rows
&& !/\r|\n/.test(this.getText())) {
542 fld
= document
.createElement('input');
544 var size
= this.options
.size
|| this.options
.cols
|| 0;
545 if (0 < size
) fld
.size
= size
;
547 fld
= document
.createElement('textarea');
548 fld
.rows
= (1 >= this.options
.rows
? this.options
.autoRows
: this.options
.rows
);
549 fld
.cols
= this.options
.cols
|| 40;
551 fld
.name
= this.options
.paramName
;
552 fld
.value
= text
; // No HTML breaks conversion anymore
553 fld
.className
= 'editor_field';
554 if (this.options
.submitOnBlur
)
555 fld
.onblur
= this._boundSubmitHandler
;
556 this._controls
.editor
= fld
;
557 if (this.options
.loadTextURL
)
558 this.loadExternalText();
559 this._form
.appendChild(this._controls
.editor
);
561 createForm: function() {
563 function addText(mode
, condition
) {
564 var text
= ipe
.options
['text' + mode
+ 'Controls'];
565 if (!text
|| condition
=== false) return;
566 ipe
._form
.appendChild(document
.createTextNode(text
));
568 this._form
= $(document
.createElement('form'));
569 this._form
.id
= this.options
.formId
;
570 this._form
.addClassName(this.options
.formClassName
);
571 this._form
.onsubmit
= this._boundSubmitHandler
;
572 this.createEditField();
573 if ('textarea' == this._controls
.editor
.tagName
.toLowerCase())
574 this._form
.appendChild(document
.createElement('br'));
575 if (this.options
.onFormCustomization
)
576 this.options
.onFormCustomization(this, this._form
);
577 addText('Before', this.options
.okControl
|| this.options
.cancelControl
);
578 this.createControl('ok', this._boundSubmitHandler
);
579 addText('Between', this.options
.okControl
&& this.options
.cancelControl
);
580 this.createControl('cancel', this._boundCancelHandler
, 'editor_cancel');
581 addText('After', this.options
.okControl
|| this.options
.cancelControl
);
583 destroy: function() {
584 if (this._oldInnerHTML
)
585 this.element
.innerHTML
= this._oldInnerHTML
;
586 this.leaveEditMode();
587 this.unregisterListeners();
589 enterEditMode: function(e
) {
590 if (this._saving
|| this._editing
) return;
591 this._editing
= true;
592 this.triggerCallback('onEnterEditMode');
593 if (this.options
.externalControl
)
594 this.options
.externalControl
.hide();
597 this.element
.parentNode
.insertBefore(this._form
, this.element
);
598 if (!this.options
.loadTextURL
)
599 this.postProcessEditField();
600 if (e
) Event
.stop(e
);
602 enterHover: function(e
) {
603 if (this.options
.hoverClassName
)
604 this.element
.addClassName(this.options
.hoverClassName
);
605 if (this._saving
) return;
606 this.triggerCallback('onEnterHover');
608 getText: function() {
609 return this.element
.innerHTML
.unescapeHTML();
611 handleAJAXFailure: function(transport
) {
612 this.triggerCallback('onFailure', transport
);
613 if (this._oldInnerHTML
) {
614 this.element
.innerHTML
= this._oldInnerHTML
;
615 this._oldInnerHTML
= null;
618 handleFormCancellation: function(e
) {
620 if (e
) Event
.stop(e
);
622 handleFormSubmission: function(e
) {
623 var form
= this._form
;
624 var value
= $F(this._controls
.editor
);
625 this.prepareSubmission();
626 var params
= this.options
.callback(form
, value
) || '';
627 if (Object
.isString(params
))
628 params
= params
.toQueryParams();
629 params
.editorId
= this.element
.id
;
630 if (this.options
.htmlResponse
) {
631 var options
= Object
.extend({ evalScripts
: true }, this.options
.ajaxOptions
);
632 Object
.extend(options
, {
634 onComplete
: this._boundWrapperHandler
,
635 onFailure
: this._boundFailureHandler
637 new Ajax
.Updater({ success
: this.element
}, this.url
, options
);
639 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
640 Object
.extend(options
, {
642 onComplete
: this._boundWrapperHandler
,
643 onFailure
: this._boundFailureHandler
645 new Ajax
.Request(this.url
, options
);
647 if (e
) Event
.stop(e
);
649 leaveEditMode: function() {
650 this.element
.removeClassName(this.options
.savingClassName
);
653 this.element
.style
.backgroundColor
= this._originalBackground
;
655 if (this.options
.externalControl
)
656 this.options
.externalControl
.show();
657 this._saving
= false;
658 this._editing
= false;
659 this._oldInnerHTML
= null;
660 this.triggerCallback('onLeaveEditMode');
662 leaveHover: function(e
) {
663 if (this.options
.hoverClassName
)
664 this.element
.removeClassName(this.options
.hoverClassName
);
665 if (this._saving
) return;
666 this.triggerCallback('onLeaveHover');
668 loadExternalText: function() {
669 this._form
.addClassName(this.options
.loadingClassName
);
670 this._controls
.editor
.disabled
= true;
671 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
672 Object
.extend(options
, {
673 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
674 onComplete
: Prototype
.emptyFunction
,
675 onSuccess: function(transport
) {
676 this._form
.removeClassName(this.options
.loadingClassName
);
677 var text
= transport
.responseText
;
678 if (this.options
.stripLoadedTextTags
)
679 text
= text
.stripTags();
680 this._controls
.editor
.value
= text
;
681 this._controls
.editor
.disabled
= false;
682 this.postProcessEditField();
684 onFailure
: this._boundFailureHandler
686 new Ajax
.Request(this.options
.loadTextURL
, options
);
688 postProcessEditField: function() {
689 var fpc
= this.options
.fieldPostCreation
;
691 $(this._controls
.editor
)['focus' == fpc
? 'focus' : 'activate']();
693 prepareOptions: function() {
694 this.options
= Object
.clone(Ajax
.InPlaceEditor
.DefaultOptions
);
695 Object
.extend(this.options
, Ajax
.InPlaceEditor
.DefaultCallbacks
);
696 [this._extraDefaultOptions
].flatten().compact().each(function(defs
) {
697 Object
.extend(this.options
, defs
);
700 prepareSubmission: function() {
706 registerListeners: function() {
707 this._listeners
= { };
709 $H(Ajax
.InPlaceEditor
.Listeners
).each(function(pair
) {
710 listener
= this[pair
.value
].bind(this);
711 this._listeners
[pair
.key
] = listener
;
712 if (!this.options
.externalControlOnly
)
713 this.element
.observe(pair
.key
, listener
);
714 if (this.options
.externalControl
)
715 this.options
.externalControl
.observe(pair
.key
, listener
);
718 removeForm: function() {
719 if (!this._form
) return;
722 this._controls
= { };
724 showSaving: function() {
725 this._oldInnerHTML
= this.element
.innerHTML
;
726 this.element
.innerHTML
= this.options
.savingText
;
727 this.element
.addClassName(this.options
.savingClassName
);
728 this.element
.style
.backgroundColor
= this._originalBackground
;
731 triggerCallback: function(cbName
, arg
) {
732 if ('function' == typeof this.options
[cbName
]) {
733 this.options
[cbName
](this, arg
);
736 unregisterListeners: function() {
737 $H(this._listeners
).each(function(pair
) {
738 if (!this.options
.externalControlOnly
)
739 this.element
.stopObserving(pair
.key
, pair
.value
);
740 if (this.options
.externalControl
)
741 this.options
.externalControl
.stopObserving(pair
.key
, pair
.value
);
744 wrapUp: function(transport
) {
745 this.leaveEditMode();
746 // Can't use triggerCallback due to backward compatibility: requires
747 // binding + direct element
748 this._boundComplete(transport
, this.element
);
752 Object
.extend(Ajax
.InPlaceEditor
.prototype, {
753 dispose
: Ajax
.InPlaceEditor
.prototype.destroy
756 Ajax
.InPlaceCollectionEditor
= Class
.create(Ajax
.InPlaceEditor
, {
757 initialize: function($super, element
, url
, options
) {
758 this._extraDefaultOptions
= Ajax
.InPlaceCollectionEditor
.DefaultOptions
;
759 $super(element
, url
, options
);
762 createEditField: function() {
763 var list
= document
.createElement('select');
764 list
.name
= this.options
.paramName
;
766 this._controls
.editor
= list
;
767 this._collection
= this.options
.collection
|| [];
768 if (this.options
.loadCollectionURL
)
769 this.loadCollection();
771 this.checkForExternalText();
772 this._form
.appendChild(this._controls
.editor
);
775 loadCollection: function() {
776 this._form
.addClassName(this.options
.loadingClassName
);
777 this.showLoadingText(this.options
.loadingCollectionText
);
778 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
779 Object
.extend(options
, {
780 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
781 onComplete
: Prototype
.emptyFunction
,
782 onSuccess: function(transport
) {
783 var js
= transport
.responseText
.strip();
784 if (!/^\[.*\]$/.test(js
)) // TODO: improve sanity check
785 throw('Server returned an invalid collection representation.');
786 this._collection
= eval(js
);
787 this.checkForExternalText();
789 onFailure
: this.onFailure
791 new Ajax
.Request(this.options
.loadCollectionURL
, options
);
794 showLoadingText: function(text
) {
795 this._controls
.editor
.disabled
= true;
796 var tempOption
= this._controls
.editor
.firstChild
;
798 tempOption
= document
.createElement('option');
799 tempOption
.value
= '';
800 this._controls
.editor
.appendChild(tempOption
);
801 tempOption
.selected
= true;
803 tempOption
.update((text
|| '').stripScripts().stripTags());
806 checkForExternalText: function() {
807 this._text
= this.getText();
808 if (this.options
.loadTextURL
)
809 this.loadExternalText();
811 this.buildOptionList();
814 loadExternalText: function() {
815 this.showLoadingText(this.options
.loadingText
);
816 var options
= Object
.extend({ method
: 'get' }, this.options
.ajaxOptions
);
817 Object
.extend(options
, {
818 parameters
: 'editorId=' + encodeURIComponent(this.element
.id
),
819 onComplete
: Prototype
.emptyFunction
,
820 onSuccess: function(transport
) {
821 this._text
= transport
.responseText
.strip();
822 this.buildOptionList();
824 onFailure
: this.onFailure
826 new Ajax
.Request(this.options
.loadTextURL
, options
);
829 buildOptionList: function() {
830 this._form
.removeClassName(this.options
.loadingClassName
);
831 this._collection
= this._collection
.map(function(entry
) {
832 return 2 === entry
.length
? entry
: [entry
, entry
].flatten();
834 var marker
= ('value' in this.options
) ? this.options
.value
: this._text
;
835 var textFound
= this._collection
.any(function(entry
) {
836 return entry
[0] == marker
;
838 this._controls
.editor
.update('');
840 this._collection
.each(function(entry
, index
) {
841 option
= document
.createElement('option');
842 option
.value
= entry
[0];
843 option
.selected
= textFound
? entry
[0] == marker
: 0 == index
;
844 option
.appendChild(document
.createTextNode(entry
[1]));
845 this._controls
.editor
.appendChild(option
);
847 this._controls
.editor
.disabled
= false;
848 Field
.scrollFreeActivate(this._controls
.editor
);
852 //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
853 //**** This only exists for a while, in order to let ****
854 //**** users adapt to the new API. Read up on the new ****
855 //**** API and convert your code to it ASAP! ****
857 Ajax
.InPlaceEditor
.prototype.initialize
.dealWithDeprecatedOptions = function(options
) {
858 if (!options
) return;
859 function fallback(name
, expr
) {
860 if (name
in options
|| expr
=== undefined) return;
861 options
[name
] = expr
;
863 fallback('cancelControl', (options
.cancelLink
? 'link' : (options
.cancelButton
? 'button' :
864 options
.cancelLink
== options
.cancelButton
== false ? false : undefined)));
865 fallback('okControl', (options
.okLink
? 'link' : (options
.okButton
? 'button' :
866 options
.okLink
== options
.okButton
== false ? false : undefined)));
867 fallback('highlightColor', options
.highlightcolor
);
868 fallback('highlightEndColor', options
.highlightendcolor
);
871 Object
.extend(Ajax
.InPlaceEditor
, {
874 autoRows
: 3, // Use when multi-line w/ rows == 1
875 cancelControl
: 'link', // 'link'|'button'|false
876 cancelText
: 'cancel',
877 clickToEditText
: 'Click to edit',
878 externalControl
: null, // id|elt
879 externalControlOnly
: false,
880 fieldPostCreation
: 'activate', // 'activate'|'focus'|false
881 formClassName
: 'inplaceeditor-form',
882 formId
: null, // id|elt
883 highlightColor
: '#ffff99',
884 highlightEndColor
: '#ffffff',
887 loadingClassName
: 'inplaceeditor-loading',
888 loadingText
: 'Loading...',
889 okControl
: 'button', // 'link'|'button'|false
892 rows
: 1, // If 1 and multi-line, uses autoRows
893 savingClassName
: 'inplaceeditor-saving',
894 savingText
: 'Saving...',
896 stripLoadedTextTags
: false,
898 textAfterControls
: '',
899 textBeforeControls
: '',
900 textBetweenControls
: ''
903 callback: function(form
) {
904 return Form
.serialize(form
);
906 onComplete: function(transport
, element
) {
907 // For backward compatibility, this one is bound to the IPE, and passes
908 // the element directly. It was too often customized, so we don't break it.
909 new Effect
.Highlight(element
, {
910 startcolor
: this.options
.highlightColor
, keepBackgroundImage
: true });
912 onEnterEditMode
: null,
913 onEnterHover: function(ipe
) {
914 ipe
.element
.style
.backgroundColor
= ipe
.options
.highlightColor
;
916 ipe
._effect
.cancel();
918 onFailure: function(transport
, ipe
) {
919 alert('Error communication with the server: ' + transport
.responseText
.stripTags());
921 onFormCustomization
: null, // Takes the IPE and its generated form, after editor, before controls.
922 onLeaveEditMode
: null,
923 onLeaveHover: function(ipe
) {
924 ipe
._effect
= new Effect
.Highlight(ipe
.element
, {
925 startcolor
: ipe
.options
.highlightColor
, endcolor
: ipe
.options
.highlightEndColor
,
926 restorecolor
: ipe
._originalBackground
, keepBackgroundImage
: true
931 click
: 'enterEditMode',
932 keydown
: 'checkForEscapeOrReturn',
933 mouseover
: 'enterHover',
934 mouseout
: 'leaveHover'
938 Ajax
.InPlaceCollectionEditor
.DefaultOptions
= {
939 loadingCollectionText
: 'Loading options...'
942 // Delayed observer, like Form.Element.Observer,
943 // but waits for delay after last key input
944 // Ideal for live-search fields
946 Form
.Element
.DelayedObserver
= Class
.create({
947 initialize: function(element
, delay
, callback
) {
948 this.delay
= delay
|| 0.5;
949 this.element
= $(element
);
950 this.callback
= callback
;
952 this.lastValue
= $F(this.element
);
953 Event
.observe(this.element
,'keyup',this.delayedListener
.bindAsEventListener(this));
955 delayedListener: function(event
) {
956 if(this.lastValue
== $F(this.element
)) return;
957 if(this.timer
) clearTimeout(this.timer
);
958 this.timer
= setTimeout(this.onTimerEvent
.bind(this), this.delay
* 1000);
959 this.lastValue
= $F(this.element
);
961 onTimerEvent: function() {
963 this.callback(this.element
, $F(this.element
));