root / web-presentation / trunk / eneraptor-web-presentation / web-app / js / prototype / controls.js @ 92
History | View | Annotate | Download (34 KB)
1 |
// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
|
---|---|
2 |
|
3 |
// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
|
4 |
// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
|
5 |
// (c) 2005-2009 Jon Tirsen (http://www.tirsen.com)
|
6 |
// Contributors:
|
7 |
// Richard Livsey
|
8 |
// Rahul Bhargava
|
9 |
// Rob Wills
|
10 |
//
|
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/
|
13 |
|
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.
|
18 |
//
|
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.
|
27 |
//
|
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.
|
38 |
|
39 |
if(typeof Effect == 'undefined') |
40 |
throw("controls.js requires including script.aculo.us' effects.js library"); |
41 |
|
42 |
var Autocompleter = { };
|
43 |
Autocompleter.Base = Class.create({ |
44 |
baseInitialize: function(element, update, options) { |
45 |
element = $(element);
|
46 |
this.element = element;
|
47 |
this.update = $(update); |
48 |
this.hasFocus = false; |
49 |
this.changed = false; |
50 |
this.active = false; |
51 |
this.index = 0; |
52 |
this.entryCount = 0; |
53 |
this.oldElementValue = this.element.value; |
54 |
|
55 |
if(this.setOptions) |
56 |
this.setOptions(options);
|
57 |
else
|
58 |
this.options = options || { };
|
59 |
|
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, { |
69 |
setHeight: false, |
70 |
offsetTop: element.offsetHeight
|
71 |
}); |
72 |
} |
73 |
Effect.Appear(update,{duration:0.15}); |
74 |
}; |
75 |
this.options.onHide = this.options.onHide || |
76 |
function(element, update){ new Effect.Fade(update,{duration:0.15}) }; |
77 |
|
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'); |
83 |
|
84 |
this.observer = null; |
85 |
|
86 |
this.element.setAttribute('autocomplete','off'); |
87 |
|
88 |
Element.hide(this.update);
|
89 |
|
90 |
Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); |
91 |
Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); |
92 |
}, |
93 |
|
94 |
show: function() { |
95 |
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); |
96 |
if(!this.iefix && |
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'); |
104 |
} |
105 |
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); |
106 |
}, |
107 |
|
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);
|
113 |
}, |
114 |
|
115 |
hide: function() { |
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); |
119 |
}, |
120 |
|
121 |
startIndicator: function() { |
122 |
if(this.options.indicator) Element.show(this.options.indicator); |
123 |
}, |
124 |
|
125 |
stopIndicator: function() { |
126 |
if(this.options.indicator) Element.hide(this.options.indicator); |
127 |
}, |
128 |
|
129 |
onKeyPress: function(event) { |
130 |
if(this.active) |
131 |
switch(event.keyCode) {
|
132 |
case Event.KEY_TAB:
|
133 |
case Event.KEY_RETURN:
|
134 |
this.selectEntry();
|
135 |
Event.stop(event); |
136 |
case Event.KEY_ESC:
|
137 |
this.hide();
|
138 |
this.active = false; |
139 |
Event.stop(event); |
140 |
return;
|
141 |
case Event.KEY_LEFT:
|
142 |
case Event.KEY_RIGHT:
|
143 |
return;
|
144 |
case Event.KEY_UP:
|
145 |
this.markPrevious();
|
146 |
this.render();
|
147 |
Event.stop(event); |
148 |
return;
|
149 |
case Event.KEY_DOWN:
|
150 |
this.markNext();
|
151 |
this.render();
|
152 |
Event.stop(event); |
153 |
return;
|
154 |
} |
155 |
else
|
156 |
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
|
157 |
(Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; |
158 |
|
159 |
this.changed = true; |
160 |
this.hasFocus = true; |
161 |
|
162 |
if(this.observer) clearTimeout(this.observer); |
163 |
this.observer =
|
164 |
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); |
165 |
}, |
166 |
|
167 |
activate: function() { |
168 |
this.changed = false; |
169 |
this.hasFocus = true; |
170 |
this.getUpdatedChoices();
|
171 |
}, |
172 |
|
173 |
onHover: function(event) { |
174 |
var element = Event.findElement(event, 'LI'); |
175 |
if(this.index != element.autocompleteIndex) |
176 |
{ |
177 |
this.index = element.autocompleteIndex;
|
178 |
this.render();
|
179 |
} |
180 |
Event.stop(event); |
181 |
}, |
182 |
|
183 |
onClick: function(event) { |
184 |
var element = Event.findElement(event, 'LI'); |
185 |
this.index = element.autocompleteIndex;
|
186 |
this.selectEntry();
|
187 |
this.hide();
|
188 |
}, |
189 |
|
190 |
onBlur: function(event) { |
191 |
// needed to make click events working
|
192 |
setTimeout(this.hide.bind(this), 250); |
193 |
this.hasFocus = false; |
194 |
this.active = false; |
195 |
}, |
196 |
|
197 |
render: function() { |
198 |
if(this.entryCount > 0) { |
199 |
for (var i = 0; i < this.entryCount; i++) |
200 |
this.index==i ?
|
201 |
Element.addClassName(this.getEntry(i),"selected") : |
202 |
Element.removeClassName(this.getEntry(i),"selected"); |
203 |
if(this.hasFocus) { |
204 |
this.show();
|
205 |
this.active = true; |
206 |
} |
207 |
} else {
|
208 |
this.active = false; |
209 |
this.hide();
|
210 |
} |
211 |
}, |
212 |
|
213 |
markPrevious: function() { |
214 |
if(this.index > 0) this.index--; |
215 |
else this.index = this.entryCount-1; |
216 |
this.getEntry(this.index).scrollIntoView(true); |
217 |
}, |
218 |
|
219 |
markNext: function() { |
220 |
if(this.index < this.entryCount-1) this.index++; |
221 |
else this.index = 0; |
222 |
this.getEntry(this.index).scrollIntoView(false); |
223 |
}, |
224 |
|
225 |
getEntry: function(index) { |
226 |
return this.update.firstChild.childNodes[index]; |
227 |
}, |
228 |
|
229 |
getCurrentEntry: function() { |
230 |
return this.getEntry(this.index); |
231 |
}, |
232 |
|
233 |
selectEntry: function() { |
234 |
this.active = false; |
235 |
this.updateElement(this.getCurrentEntry()); |
236 |
}, |
237 |
|
238 |
updateElement: function(selectedElement) { |
239 |
if (this.options.updateElement) { |
240 |
this.options.updateElement(selectedElement);
|
241 |
return;
|
242 |
} |
243 |
var value = ''; |
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); |
247 |
} else
|
248 |
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
|
249 |
|
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+/); |
254 |
if (whitespace)
|
255 |
newValue += whitespace[0];
|
256 |
this.element.value = newValue + value + this.element.value.substr(bounds[1]); |
257 |
} else {
|
258 |
this.element.value = value;
|
259 |
} |
260 |
this.oldElementValue = this.element.value; |
261 |
this.element.focus();
|
262 |
|
263 |
if (this.options.afterUpdateElement) |
264 |
this.options.afterUpdateElement(this.element, selectedElement); |
265 |
}, |
266 |
|
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());
|
272 |
|
273 |
if(this.update.firstChild && this.update.down().childNodes) { |
274 |
this.entryCount =
|
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);
|
280 |
} |
281 |
} else {
|
282 |
this.entryCount = 0; |
283 |
} |
284 |
|
285 |
this.stopIndicator();
|
286 |
this.index = 0; |
287 |
|
288 |
if(this.entryCount==1 && this.options.autoSelect) { |
289 |
this.selectEntry();
|
290 |
this.hide();
|
291 |
} else {
|
292 |
this.render();
|
293 |
} |
294 |
} |
295 |
}, |
296 |
|
297 |
addObservers: function(element) { |
298 |
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); |
299 |
Event.observe(element, "click", this.onClick.bindAsEventListener(this)); |
300 |
}, |
301 |
|
302 |
onObserverEvent: function() { |
303 |
this.changed = false; |
304 |
this.tokenBounds = null; |
305 |
if(this.getToken().length>=this.options.minChars) { |
306 |
this.getUpdatedChoices();
|
307 |
} else {
|
308 |
this.active = false; |
309 |
this.hide();
|
310 |
} |
311 |
this.oldElementValue = this.element.value; |
312 |
}, |
313 |
|
314 |
getToken: function() { |
315 |
var bounds = this.getTokenBounds(); |
316 |
return this.element.value.substring(bounds[0], bounds[1]).strip(); |
317 |
}, |
318 |
|
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; |
326 |
var tp;
|
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; |
332 |
} |
333 |
return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); |
334 |
} |
335 |
}); |
336 |
|
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])
|
341 |
return index;
|
342 |
return boundary;
|
343 |
}; |
344 |
|
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; |
351 |
this.url = url;
|
352 |
}, |
353 |
|
354 |
getUpdatedChoices: function() { |
355 |
this.startIndicator();
|
356 |
|
357 |
var entry = encodeURIComponent(this.options.paramName) + '=' + |
358 |
encodeURIComponent(this.getToken());
|
359 |
|
360 |
this.options.parameters = this.options.callback ? |
361 |
this.options.callback(this.element, entry) : entry; |
362 |
|
363 |
if(this.options.defaultParams) |
364 |
this.options.parameters += '&' + this.options.defaultParams; |
365 |
|
366 |
new Ajax.Request(this.url, this.options); |
367 |
}, |
368 |
|
369 |
onComplete: function(request) { |
370 |
this.updateChoices(request.responseText);
|
371 |
} |
372 |
}); |
373 |
|
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.
|
377 |
//
|
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.
|
382 |
//
|
383 |
// Extra local autocompletion options:
|
384 |
// - choices - How many autocompletion choices to offer
|
385 |
//
|
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).
|
393 |
//
|
394 |
// - fullSsearch - Search anywhere in autocomplete array strings.
|
395 |
//
|
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.
|
400 |
//
|
401 |
// - ignoreCase - Whether to ignore case when autocompleting.
|
402 |
// Defaults to true.
|
403 |
//
|
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
|
407 |
// you support them.
|
408 |
|
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;
|
413 |
}, |
414 |
|
415 |
getUpdatedChoices: function() { |
416 |
this.updateChoices(this.options.selector(this)); |
417 |
}, |
418 |
|
419 |
setOptions: function(options) { |
420 |
this.options = Object.extend({
|
421 |
choices: 10, |
422 |
partialSearch: true, |
423 |
partialChars: 2, |
424 |
ignoreCase: true, |
425 |
fullSearch: false, |
426 |
selector: function(instance) { |
427 |
var ret = []; // Beginning matches |
428 |
var partial = []; // Inside matches |
429 |
var entry = instance.getToken();
|
430 |
var count = 0; |
431 |
|
432 |
for (var i = 0; i < instance.options.array.length && |
433 |
ret.length < instance.options.choices ; i++) { |
434 |
|
435 |
var elem = instance.options.array[i];
|
436 |
var foundPos = instance.options.ignoreCase ?
|
437 |
elem.toLowerCase().indexOf(entry.toLowerCase()) : |
438 |
elem.indexOf(entry); |
439 |
|
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>");
|
444 |
break;
|
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>");
|
451 |
break;
|
452 |
} |
453 |
} |
454 |
|
455 |
foundPos = instance.options.ignoreCase ? |
456 |
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
|
457 |
elem.indexOf(entry, foundPos + 1);
|
458 |
|
459 |
} |
460 |
} |
461 |
if (partial.length)
|
462 |
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
|
463 |
return "<ul>" + ret.join('') + "</ul>"; |
464 |
} |
465 |
}, options || { }); |
466 |
} |
467 |
}); |
468 |
|
469 |
// AJAX in-place editor and collection editor
|
470 |
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
|
471 |
|
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); |
478 |
}, 1);
|
479 |
}; |
480 |
|
481 |
Ajax.InPlaceEditor = Class.create({ |
482 |
initialize: function(element, url, options) { |
483 |
this.url = url;
|
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 = ''; |
493 |
} |
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();
|
506 |
}, |
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);
|
513 |
}, |
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'); |
519 |
btn.type = 'submit';
|
520 |
btn.value = text; |
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'); |
528 |
link.href = '#';
|
529 |
link.appendChild(document.createTextNode(text)); |
530 |
link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; |
531 |
link.className = 'editor_' + mode + '_link'; |
532 |
if (extraClasses)
|
533 |
link.className += ' ' + extraClasses;
|
534 |
this._form.appendChild(link);
|
535 |
this._controls[mode] = link;
|
536 |
} |
537 |
}, |
538 |
createEditField: function() { |
539 |
var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); |
540 |
var fld;
|
541 |
if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { |
542 |
fld = document.createElement('input');
|
543 |
fld.type = 'text';
|
544 |
var size = this.options.size || this.options.cols || 0; |
545 |
if (0 < size) fld.size = size; |
546 |
} else {
|
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; |
550 |
} |
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); |
560 |
}, |
561 |
createForm: function() { |
562 |
var ipe = this; |
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)); |
567 |
}; |
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); |
582 |
}, |
583 |
destroy: function() { |
584 |
if (this._oldInnerHTML) |
585 |
this.element.innerHTML = this._oldInnerHTML; |
586 |
this.leaveEditMode();
|
587 |
this.unregisterListeners();
|
588 |
}, |
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();
|
595 |
this.element.hide();
|
596 |
this.createForm();
|
597 |
this.element.parentNode.insertBefore(this._form, this.element); |
598 |
if (!this.options.loadTextURL) |
599 |
this.postProcessEditField();
|
600 |
if (e) Event.stop(e);
|
601 |
}, |
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'); |
607 |
}, |
608 |
getText: function() { |
609 |
return this.element.innerHTML.unescapeHTML(); |
610 |
}, |
611 |
handleAJAXFailure: function(transport) { |
612 |
this.triggerCallback('onFailure', transport); |
613 |
if (this._oldInnerHTML) { |
614 |
this.element.innerHTML = this._oldInnerHTML; |
615 |
this._oldInnerHTML = null; |
616 |
} |
617 |
}, |
618 |
handleFormCancellation: function(e) { |
619 |
this.wrapUp();
|
620 |
if (e) Event.stop(e);
|
621 |
}, |
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, { |
633 |
parameters: params,
|
634 |
onComplete: this._boundWrapperHandler, |
635 |
onFailure: this._boundFailureHandler |
636 |
}); |
637 |
new Ajax.Updater({ success: this.element }, this.url, options); |
638 |
} else {
|
639 |
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); |
640 |
Object.extend(options, { |
641 |
parameters: params,
|
642 |
onComplete: this._boundWrapperHandler, |
643 |
onFailure: this._boundFailureHandler |
644 |
}); |
645 |
new Ajax.Request(this.url, options); |
646 |
} |
647 |
if (e) Event.stop(e);
|
648 |
}, |
649 |
leaveEditMode: function() { |
650 |
this.element.removeClassName(this.options.savingClassName); |
651 |
this.removeForm();
|
652 |
this.leaveHover();
|
653 |
this.element.style.backgroundColor = this._originalBackground; |
654 |
this.element.show();
|
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'); |
661 |
}, |
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'); |
667 |
}, |
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();
|
683 |
}.bind(this),
|
684 |
onFailure: this._boundFailureHandler |
685 |
}); |
686 |
new Ajax.Request(this.options.loadTextURL, options); |
687 |
}, |
688 |
postProcessEditField: function() { |
689 |
var fpc = this.options.fieldPostCreation; |
690 |
if (fpc)
|
691 |
$(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); |
692 |
}, |
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);
|
698 |
}.bind(this));
|
699 |
}, |
700 |
prepareSubmission: function() { |
701 |
this._saving = true; |
702 |
this.removeForm();
|
703 |
this.leaveHover();
|
704 |
this.showSaving();
|
705 |
}, |
706 |
registerListeners: function() { |
707 |
this._listeners = { };
|
708 |
var listener;
|
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);
|
716 |
}.bind(this));
|
717 |
}, |
718 |
removeForm: function() { |
719 |
if (!this._form) return; |
720 |
this._form.remove();
|
721 |
this._form = null; |
722 |
this._controls = { };
|
723 |
}, |
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; |
729 |
this.element.show();
|
730 |
}, |
731 |
triggerCallback: function(cbName, arg) { |
732 |
if ('function' == typeof this.options[cbName]) { |
733 |
this.options[cbName](this, arg); |
734 |
} |
735 |
}, |
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);
|
742 |
}.bind(this));
|
743 |
}, |
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); |
749 |
} |
750 |
}); |
751 |
|
752 |
Object.extend(Ajax.InPlaceEditor.prototype, { |
753 |
dispose: Ajax.InPlaceEditor.prototype.destroy
|
754 |
}); |
755 |
|
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);
|
760 |
}, |
761 |
|
762 |
createEditField: function() { |
763 |
var list = document.createElement('select'); |
764 |
list.name = this.options.paramName;
|
765 |
list.size = 1;
|
766 |
this._controls.editor = list;
|
767 |
this._collection = this.options.collection || []; |
768 |
if (this.options.loadCollectionURL) |
769 |
this.loadCollection();
|
770 |
else
|
771 |
this.checkForExternalText();
|
772 |
this._form.appendChild(this._controls.editor); |
773 |
}, |
774 |
|
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();
|
788 |
}.bind(this),
|
789 |
onFailure: this.onFailure |
790 |
}); |
791 |
new Ajax.Request(this.options.loadCollectionURL, options); |
792 |
}, |
793 |
|
794 |
showLoadingText: function(text) { |
795 |
this._controls.editor.disabled = true; |
796 |
var tempOption = this._controls.editor.firstChild; |
797 |
if (!tempOption) {
|
798 |
tempOption = document.createElement('option');
|
799 |
tempOption.value = '';
|
800 |
this._controls.editor.appendChild(tempOption);
|
801 |
tempOption.selected = true;
|
802 |
} |
803 |
tempOption.update((text || '').stripScripts().stripTags());
|
804 |
}, |
805 |
|
806 |
checkForExternalText: function() { |
807 |
this._text = this.getText(); |
808 |
if (this.options.loadTextURL) |
809 |
this.loadExternalText();
|
810 |
else
|
811 |
this.buildOptionList();
|
812 |
}, |
813 |
|
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();
|
823 |
}.bind(this),
|
824 |
onFailure: this.onFailure |
825 |
}); |
826 |
new Ajax.Request(this.options.loadTextURL, options); |
827 |
}, |
828 |
|
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(); |
833 |
}); |
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; |
837 |
}.bind(this));
|
838 |
this._controls.editor.update(''); |
839 |
var option;
|
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);
|
846 |
}.bind(this));
|
847 |
this._controls.editor.disabled = false; |
848 |
Field.scrollFreeActivate(this._controls.editor);
|
849 |
} |
850 |
}); |
851 |
|
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! ****
|
856 |
|
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; |
862 |
}; |
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);
|
869 |
}; |
870 |
|
871 |
Object.extend(Ajax.InPlaceEditor, { |
872 |
DefaultOptions: {
|
873 |
ajaxOptions: { },
|
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', |
885 |
hoverClassName: '', |
886 |
htmlResponse: true, |
887 |
loadingClassName: 'inplaceeditor-loading', |
888 |
loadingText: 'Loading...', |
889 |
okControl: 'button', // 'link'|'button'|false |
890 |
okText: 'ok', |
891 |
paramName: 'value', |
892 |
rows: 1, // If 1 and multi-line, uses autoRows |
893 |
savingClassName: 'inplaceeditor-saving', |
894 |
savingText: 'Saving...', |
895 |
size: 0, |
896 |
stripLoadedTextTags: false, |
897 |
submitOnBlur: false, |
898 |
textAfterControls: '', |
899 |
textBeforeControls: '', |
900 |
textBetweenControls: '' |
901 |
}, |
902 |
DefaultCallbacks: {
|
903 |
callback: function(form) { |
904 |
return Form.serialize(form);
|
905 |
}, |
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 }); |
911 |
}, |
912 |
onEnterEditMode: null, |
913 |
onEnterHover: function(ipe) { |
914 |
ipe.element.style.backgroundColor = ipe.options.highlightColor; |
915 |
if (ipe._effect)
|
916 |
ipe._effect.cancel(); |
917 |
}, |
918 |
onFailure: function(transport, ipe) { |
919 |
alert('Error communication with the server: ' + transport.responseText.stripTags());
|
920 |
}, |
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 |
927 |
}); |
928 |
} |
929 |
}, |
930 |
Listeners: {
|
931 |
click: 'enterEditMode', |
932 |
keydown: 'checkForEscapeOrReturn', |
933 |
mouseover: 'enterHover', |
934 |
mouseout: 'leaveHover' |
935 |
} |
936 |
}); |
937 |
|
938 |
Ajax.InPlaceCollectionEditor.DefaultOptions = { |
939 |
loadingCollectionText: 'Loading options...' |
940 |
}; |
941 |
|
942 |
// Delayed observer, like Form.Element.Observer,
|
943 |
// but waits for delay after last key input
|
944 |
// Ideal for live-search fields
|
945 |
|
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;
|
951 |
this.timer = null; |
952 |
this.lastValue = $F(this.element); |
953 |
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); |
954 |
}, |
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); |
960 |
}, |
961 |
onTimerEvent: function() { |
962 |
this.timer = null; |
963 |
this.callback(this.element, $F(this.element)); |
964 |
} |
965 |
}); |