Commit | Line | Data |
---|---|---|
6a1aa64f DV |
1 | /*** |
2 | Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) | |
3 | (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) | |
4 | (c) 2005 Jon Tirsen (http://www.tirsen.com) | |
5 | Contributors: | |
6 | Richard Livsey | |
7 | Rahul Bhargava | |
8 | Rob Wills | |
9 | Mochi-ized By Thomas Herve (_firstname_@nimail.org) | |
10 | ||
11 | See scriptaculous.js for full license. | |
12 | ||
13 | Autocompleter.Base handles all the autocompletion functionality | |
14 | that's independent of the data source for autocompletion. This | |
15 | includes drawing the autocompletion menu, observing keyboard | |
16 | and mouse events, and similar. | |
17 | ||
18 | Specific autocompleters need to provide, at the very least, | |
19 | a getUpdatedChoices function that will be invoked every time | |
20 | the text inside the monitored textbox changes. This method | |
21 | should get the text for which to provide autocompletion by | |
22 | invoking this.getToken(), NOT by directly accessing | |
23 | this.element.value. This is to allow incremental tokenized | |
24 | autocompletion. Specific auto-completion logic (AJAX, etc) | |
25 | belongs in getUpdatedChoices. | |
26 | ||
27 | Tokenized incremental autocompletion is enabled automatically | |
28 | when an autocompleter is instantiated with the 'tokens' option | |
29 | in the options parameter, e.g.: | |
30 | new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); | |
31 | will incrementally autocomplete with a comma as the token. | |
32 | Additionally, ',' in the above example can be replaced with | |
33 | a token array, e.g. { tokens: [',', '\n'] } which | |
34 | enables autocompletion on multiple tokens. This is most | |
35 | useful when one of the tokens is \n (a newline), as it | |
36 | allows smart autocompletion after linebreaks. | |
37 | ||
38 | ***/ | |
39 | ||
40 | MochiKit.Base.update(MochiKit.Base, { | |
41 | ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)', | |
42 | ||
43 | /** @id MochiKit.Base.stripScripts */ | |
44 | stripScripts: function (str) { | |
45 | return str.replace(new RegExp(MochiKit.Base.ScriptFragment, 'img'), ''); | |
46 | }, | |
47 | ||
48 | /** @id MochiKit.Base.stripTags */ | |
49 | stripTags: function(str) { | |
50 | return str.replace(/<\/?[^>]+>/gi, ''); | |
51 | }, | |
52 | ||
53 | /** @id MochiKit.Base.extractScripts */ | |
54 | extractScripts: function (str) { | |
55 | var matchAll = new RegExp(MochiKit.Base.ScriptFragment, 'img'); | |
56 | var matchOne = new RegExp(MochiKit.Base.ScriptFragment, 'im'); | |
57 | return MochiKit.Base.map(function (scriptTag) { | |
58 | return (scriptTag.match(matchOne) || ['', ''])[1]; | |
59 | }, str.match(matchAll) || []); | |
60 | }, | |
61 | ||
62 | /** @id MochiKit.Base.evalScripts */ | |
63 | evalScripts: function (str) { | |
64 | return MochiKit.Base.map(function (scr) { | |
65 | eval(scr); | |
66 | }, MochiKit.Base.extractScripts(str)); | |
67 | } | |
68 | }); | |
69 | ||
70 | MochiKit.Form = { | |
71 | ||
72 | /** @id MochiKit.Form.serialize */ | |
73 | serialize: function (form) { | |
74 | var elements = MochiKit.Form.getElements(form); | |
75 | var queryComponents = []; | |
76 | ||
77 | for (var i = 0; i < elements.length; i++) { | |
78 | var queryComponent = MochiKit.Form.serializeElement(elements[i]); | |
79 | if (queryComponent) { | |
80 | queryComponents.push(queryComponent); | |
81 | } | |
82 | } | |
83 | ||
84 | return queryComponents.join('&'); | |
85 | }, | |
86 | ||
87 | /** @id MochiKit.Form.getElements */ | |
88 | getElements: function (form) { | |
89 | form = MochiKit.DOM.getElement(form); | |
90 | var elements = []; | |
91 | ||
92 | for (var tagName in MochiKit.Form.Serializers) { | |
93 | var tagElements = form.getElementsByTagName(tagName); | |
94 | for (var j = 0; j < tagElements.length; j++) { | |
95 | elements.push(tagElements[j]); | |
96 | } | |
97 | } | |
98 | return elements; | |
99 | }, | |
100 | ||
101 | /** @id MochiKit.Form.serializeElement */ | |
102 | serializeElement: function (element) { | |
103 | element = MochiKit.DOM.getElement(element); | |
104 | var method = element.tagName.toLowerCase(); | |
105 | var parameter = MochiKit.Form.Serializers[method](element); | |
106 | ||
107 | if (parameter) { | |
108 | var key = encodeURIComponent(parameter[0]); | |
109 | if (key.length === 0) { | |
110 | return; | |
111 | } | |
112 | ||
113 | if (!(parameter[1] instanceof Array)) { | |
114 | parameter[1] = [parameter[1]]; | |
115 | } | |
116 | ||
117 | return parameter[1].map(function (value) { | |
118 | return key + '=' + encodeURIComponent(value); | |
119 | }).join('&'); | |
120 | } | |
121 | } | |
122 | }; | |
123 | ||
124 | MochiKit.Form.Serializers = { | |
125 | ||
126 | /** @id MochiKit.Form.Serializers.input */ | |
127 | input: function (element) { | |
128 | switch (element.type.toLowerCase()) { | |
129 | case 'submit': | |
130 | case 'hidden': | |
131 | case 'password': | |
132 | case 'text': | |
133 | return MochiKit.Form.Serializers.textarea(element); | |
134 | case 'checkbox': | |
135 | case 'radio': | |
136 | return MochiKit.Form.Serializers.inputSelector(element); | |
137 | } | |
138 | return false; | |
139 | }, | |
140 | ||
141 | /** @id MochiKit.Form.Serializers.inputSelector */ | |
142 | inputSelector: function (element) { | |
143 | if (element.checked) { | |
144 | return [element.name, element.value]; | |
145 | } | |
146 | }, | |
147 | ||
148 | /** @id MochiKit.Form.Serializers.textarea */ | |
149 | textarea: function (element) { | |
150 | return [element.name, element.value]; | |
151 | }, | |
152 | ||
153 | /** @id MochiKit.Form.Serializers.select */ | |
154 | select: function (element) { | |
155 | return MochiKit.Form.Serializers[element.type == 'select-one' ? | |
156 | 'selectOne' : 'selectMany'](element); | |
157 | }, | |
158 | ||
159 | /** @id MochiKit.Form.Serializers.selectOne */ | |
160 | selectOne: function (element) { | |
161 | var value = '', opt, index = element.selectedIndex; | |
162 | if (index >= 0) { | |
163 | opt = element.options[index]; | |
164 | value = opt.value; | |
165 | if (!value && !('value' in opt)) { | |
166 | value = opt.text; | |
167 | } | |
168 | } | |
169 | return [element.name, value]; | |
170 | }, | |
171 | ||
172 | /** @id MochiKit.Form.Serializers.selectMany */ | |
173 | selectMany: function (element) { | |
174 | var value = []; | |
175 | for (var i = 0; i < element.length; i++) { | |
176 | var opt = element.options[i]; | |
177 | if (opt.selected) { | |
178 | var optValue = opt.value; | |
179 | if (!optValue && !('value' in opt)) { | |
180 | optValue = opt.text; | |
181 | } | |
182 | value.push(optValue); | |
183 | } | |
184 | } | |
185 | return [element.name, value]; | |
186 | } | |
187 | }; | |
188 | ||
189 | /** @id Ajax */ | |
190 | var Ajax = { | |
191 | activeRequestCount: 0 | |
192 | }; | |
193 | ||
194 | Ajax.Responders = { | |
195 | responders: [], | |
196 | ||
197 | /** @id Ajax.Responders.register */ | |
198 | register: function (responderToAdd) { | |
199 | if (MochiKit.Base.find(this.responders, responderToAdd) == -1) { | |
200 | this.responders.push(responderToAdd); | |
201 | } | |
202 | }, | |
203 | ||
204 | /** @id Ajax.Responders.unregister */ | |
205 | unregister: function (responderToRemove) { | |
206 | this.responders = this.responders.without(responderToRemove); | |
207 | }, | |
208 | ||
209 | /** @id Ajax.Responders.dispatch */ | |
210 | dispatch: function (callback, request, transport, json) { | |
211 | MochiKit.Iter.forEach(this.responders, function (responder) { | |
212 | if (responder[callback] && | |
213 | typeof(responder[callback]) == 'function') { | |
214 | try { | |
215 | responder[callback].apply(responder, [request, transport, json]); | |
216 | } catch (e) {} | |
217 | } | |
218 | }); | |
219 | } | |
220 | }; | |
221 | ||
222 | Ajax.Responders.register({ | |
223 | ||
224 | /** @id Ajax.Responders.onCreate */ | |
225 | onCreate: function () { | |
226 | Ajax.activeRequestCount++; | |
227 | }, | |
228 | ||
229 | /** @id Ajax.Responders.onComplete */ | |
230 | onComplete: function () { | |
231 | Ajax.activeRequestCount--; | |
232 | } | |
233 | }); | |
234 | ||
235 | /** @id Ajax.Base */ | |
236 | Ajax.Base = function () {}; | |
237 | ||
238 | Ajax.Base.prototype = { | |
239 | ||
240 | /** @id Ajax.Base.prototype.setOptions */ | |
241 | setOptions: function (options) { | |
242 | this.options = { | |
243 | method: 'post', | |
244 | asynchronous: true, | |
245 | parameters: '' | |
246 | } | |
247 | MochiKit.Base.update(this.options, options || {}); | |
248 | }, | |
249 | ||
250 | /** @id Ajax.Base.prototype.responseIsSuccess */ | |
251 | responseIsSuccess: function () { | |
252 | return this.transport.status == undefined | |
253 | || this.transport.status === 0 | |
254 | || (this.transport.status >= 200 && this.transport.status < 300); | |
255 | }, | |
256 | ||
257 | /** @id Ajax.Base.prototype.responseIsFailure */ | |
258 | responseIsFailure: function () { | |
259 | return !this.responseIsSuccess(); | |
260 | } | |
261 | }; | |
262 | ||
263 | /** @id Ajax.Request */ | |
264 | Ajax.Request = function (url, options) { | |
265 | this.__init__(url, options); | |
266 | }; | |
267 | ||
268 | /** @id Ajax.Events */ | |
269 | Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', | |
270 | 'Interactive', 'Complete']; | |
271 | ||
272 | MochiKit.Base.update(Ajax.Request.prototype, Ajax.Base.prototype); | |
273 | ||
274 | MochiKit.Base.update(Ajax.Request.prototype, { | |
275 | __init__: function (url, options) { | |
276 | this.transport = MochiKit.Async.getXMLHttpRequest(); | |
277 | this.setOptions(options); | |
278 | this.request(url); | |
279 | }, | |
280 | ||
281 | /** @id Ajax.Request.prototype.request */ | |
282 | request: function (url) { | |
283 | var parameters = this.options.parameters || ''; | |
284 | if (parameters.length > 0){ | |
285 | parameters += '&_='; | |
286 | } | |
287 | ||
288 | try { | |
289 | this.url = url; | |
290 | if (this.options.method == 'get' && parameters.length > 0) { | |
291 | this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; | |
292 | } | |
293 | Ajax.Responders.dispatch('onCreate', this, this.transport); | |
294 | ||
295 | this.transport.open(this.options.method, this.url, | |
296 | this.options.asynchronous); | |
297 | ||
298 | if (this.options.asynchronous) { | |
299 | this.transport.onreadystatechange = MochiKit.Base.bind(this.onStateChange, this); | |
300 | setTimeout(MochiKit.Base.bind(function () { | |
301 | this.respondToReadyState(1); | |
302 | }, this), 10); | |
303 | } | |
304 | ||
305 | this.setRequestHeaders(); | |
306 | ||
307 | var body = this.options.postBody ? this.options.postBody : parameters; | |
308 | this.transport.send(this.options.method == 'post' ? body : null); | |
309 | ||
310 | } catch (e) { | |
311 | this.dispatchException(e); | |
312 | } | |
313 | }, | |
314 | ||
315 | /** @id Ajax.Request.prototype.setRequestHeaders */ | |
316 | setRequestHeaders: function () { | |
317 | var requestHeaders = ['X-Requested-With', 'XMLHttpRequest']; | |
318 | ||
319 | if (this.options.method == 'post') { | |
320 | requestHeaders.push('Content-type', | |
321 | 'application/x-www-form-urlencoded'); | |
322 | ||
323 | /* Force 'Connection: close' for Mozilla browsers to work around | |
324 | * a bug where XMLHttpRequest sends an incorrect Content-length | |
325 | * header. See Mozilla Bugzilla #246651. | |
326 | */ | |
327 | if (this.transport.overrideMimeType) { | |
328 | requestHeaders.push('Connection', 'close'); | |
329 | } | |
330 | } | |
331 | ||
332 | if (this.options.requestHeaders) { | |
333 | requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); | |
334 | } | |
335 | ||
336 | for (var i = 0; i < requestHeaders.length; i += 2) { | |
337 | this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); | |
338 | } | |
339 | }, | |
340 | ||
341 | /** @id Ajax.Request.prototype.onStateChange */ | |
342 | onStateChange: function () { | |
343 | var readyState = this.transport.readyState; | |
344 | if (readyState != 1) { | |
345 | this.respondToReadyState(this.transport.readyState); | |
346 | } | |
347 | }, | |
348 | ||
349 | /** @id Ajax.Request.prototype.header */ | |
350 | header: function (name) { | |
351 | try { | |
352 | return this.transport.getResponseHeader(name); | |
353 | } catch (e) {} | |
354 | }, | |
355 | ||
356 | /** @id Ajax.Request.prototype.evalJSON */ | |
357 | evalJSON: function () { | |
358 | try { | |
359 | return eval(this.header('X-JSON')); | |
360 | } catch (e) {} | |
361 | }, | |
362 | ||
363 | /** @id Ajax.Request.prototype.evalResponse */ | |
364 | evalResponse: function () { | |
365 | try { | |
366 | return eval(this.transport.responseText); | |
367 | } catch (e) { | |
368 | this.dispatchException(e); | |
369 | } | |
370 | }, | |
371 | ||
372 | /** @id Ajax.Request.prototype.respondToReadyState */ | |
373 | respondToReadyState: function (readyState) { | |
374 | var event = Ajax.Request.Events[readyState]; | |
375 | var transport = this.transport, json = this.evalJSON(); | |
376 | ||
377 | if (event == 'Complete') { | |
378 | try { | |
379 | (this.options['on' + this.transport.status] | |
380 | || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] | |
381 | || MochiKit.Base.noop)(transport, json); | |
382 | } catch (e) { | |
383 | this.dispatchException(e); | |
384 | } | |
385 | ||
386 | if ((this.header('Content-type') || '').match(/^text\/javascript/i)) { | |
387 | this.evalResponse(); | |
388 | } | |
389 | } | |
390 | ||
391 | try { | |
392 | (this.options['on' + event] || MochiKit.Base.noop)(transport, json); | |
393 | Ajax.Responders.dispatch('on' + event, this, transport, json); | |
394 | } catch (e) { | |
395 | this.dispatchException(e); | |
396 | } | |
397 | ||
398 | /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ | |
399 | if (event == 'Complete') { | |
400 | this.transport.onreadystatechange = MochiKit.Base.noop; | |
401 | } | |
402 | }, | |
403 | ||
404 | /** @id Ajax.Request.prototype.dispatchException */ | |
405 | dispatchException: function (exception) { | |
406 | (this.options.onException || MochiKit.Base.noop)(this, exception); | |
407 | Ajax.Responders.dispatch('onException', this, exception); | |
408 | } | |
409 | }); | |
410 | ||
411 | /** @id Ajax.Updater */ | |
412 | Ajax.Updater = function (container, url, options) { | |
413 | this.__init__(container, url, options); | |
414 | }; | |
415 | ||
416 | MochiKit.Base.update(Ajax.Updater.prototype, Ajax.Request.prototype); | |
417 | ||
418 | MochiKit.Base.update(Ajax.Updater.prototype, { | |
419 | __init__: function (container, url, options) { | |
420 | this.containers = { | |
421 | success: container.success ? MochiKit.DOM.getElement(container.success) : MochiKit.DOM.getElement(container), | |
422 | failure: container.failure ? MochiKit.DOM.getElement(container.failure) : | |
423 | (container.success ? null : MochiKit.DOM.getElement(container)) | |
424 | } | |
425 | this.transport = MochiKit.Async.getXMLHttpRequest(); | |
426 | this.setOptions(options); | |
427 | ||
428 | var onComplete = this.options.onComplete || MochiKit.Base.noop; | |
429 | this.options.onComplete = MochiKit.Base.bind(function (transport, object) { | |
430 | this.updateContent(); | |
431 | onComplete(transport, object); | |
432 | }, this); | |
433 | ||
434 | this.request(url); | |
435 | }, | |
436 | ||
437 | /** @id Ajax.Updater.prototype.updateContent */ | |
438 | updateContent: function () { | |
439 | var receiver = this.responseIsSuccess() ? | |
440 | this.containers.success : this.containers.failure; | |
441 | var response = this.transport.responseText; | |
442 | ||
443 | if (!this.options.evalScripts) { | |
444 | response = MochiKit.Base.stripScripts(response); | |
445 | } | |
446 | ||
447 | if (receiver) { | |
448 | if (this.options.insertion) { | |
449 | new this.options.insertion(receiver, response); | |
450 | } else { | |
451 | MochiKit.DOM.getElement(receiver).innerHTML = | |
452 | MochiKit.Base.stripScripts(response); | |
453 | setTimeout(function () { | |
454 | MochiKit.Base.evalScripts(response); | |
455 | }, 10); | |
456 | } | |
457 | } | |
458 | ||
459 | if (this.responseIsSuccess()) { | |
460 | if (this.onComplete) { | |
461 | setTimeout(MochiKit.Base.bind(this.onComplete, this), 10); | |
462 | } | |
463 | } | |
464 | } | |
465 | }); | |
466 | ||
467 | /** @id Field */ | |
468 | var Field = { | |
469 | ||
470 | /** @id clear */ | |
471 | clear: function () { | |
472 | for (var i = 0; i < arguments.length; i++) { | |
473 | MochiKit.DOM.getElement(arguments[i]).value = ''; | |
474 | } | |
475 | }, | |
476 | ||
477 | /** @id focus */ | |
478 | focus: function (element) { | |
479 | MochiKit.DOM.getElement(element).focus(); | |
480 | }, | |
481 | ||
482 | /** @id present */ | |
483 | present: function () { | |
484 | for (var i = 0; i < arguments.length; i++) { | |
485 | if (MochiKit.DOM.getElement(arguments[i]).value == '') { | |
486 | return false; | |
487 | } | |
488 | } | |
489 | return true; | |
490 | }, | |
491 | ||
492 | /** @id select */ | |
493 | select: function (element) { | |
494 | MochiKit.DOM.getElement(element).select(); | |
495 | }, | |
496 | ||
497 | /** @id activate */ | |
498 | activate: function (element) { | |
499 | element = MochiKit.DOM.getElement(element); | |
500 | element.focus(); | |
501 | if (element.select) { | |
502 | element.select(); | |
503 | } | |
504 | }, | |
505 | ||
506 | /** @id scrollFreeActivate */ | |
507 | scrollFreeActivate: function (field) { | |
508 | setTimeout(function () { | |
509 | Field.activate(field); | |
510 | }, 1); | |
511 | } | |
512 | }; | |
513 | ||
514 | ||
515 | /** @id Autocompleter */ | |
516 | var Autocompleter = {}; | |
517 | ||
518 | /** @id Autocompleter.Base */ | |
519 | Autocompleter.Base = function () {}; | |
520 | ||
521 | Autocompleter.Base.prototype = { | |
522 | ||
523 | /** @id Autocompleter.Base.prototype.baseInitialize */ | |
524 | baseInitialize: function (element, update, options) { | |
525 | this.element = MochiKit.DOM.getElement(element); | |
526 | this.update = MochiKit.DOM.getElement(update); | |
527 | this.hasFocus = false; | |
528 | this.changed = false; | |
529 | this.active = false; | |
530 | this.index = 0; | |
531 | this.entryCount = 0; | |
532 | ||
533 | if (this.setOptions) { | |
534 | this.setOptions(options); | |
535 | } | |
536 | else { | |
537 | this.options = options || {}; | |
538 | } | |
539 | ||
540 | this.options.paramName = this.options.paramName || this.element.name; | |
541 | this.options.tokens = this.options.tokens || []; | |
542 | this.options.frequency = this.options.frequency || 0.4; | |
543 | this.options.minChars = this.options.minChars || 1; | |
544 | this.options.onShow = this.options.onShow || function (element, update) { | |
545 | if (!update.style.position || update.style.position == 'absolute') { | |
546 | update.style.position = 'absolute'; | |
547 | MochiKit.Position.clone(element, update, { | |
548 | setHeight: false, | |
549 | offsetTop: element.offsetHeight | |
550 | }); | |
551 | } | |
552 | MochiKit.Visual.appear(update, {duration:0.15}); | |
553 | }; | |
554 | this.options.onHide = this.options.onHide || function (element, update) { | |
555 | MochiKit.Visual.fade(update, {duration: 0.15}); | |
556 | }; | |
557 | ||
558 | if (typeof(this.options.tokens) == 'string') { | |
559 | this.options.tokens = new Array(this.options.tokens); | |
560 | } | |
561 | ||
562 | this.observer = null; | |
563 | ||
564 | this.element.setAttribute('autocomplete', 'off'); | |
565 | ||
566 | MochiKit.Style.hideElement(this.update); | |
567 | ||
568 | MochiKit.Signal.connect(this.element, 'onblur', this, this.onBlur); | |
569 | MochiKit.Signal.connect(this.element, 'onkeypress', this, this.onKeyPress, this); | |
570 | }, | |
571 | ||
572 | /** @id Autocompleter.Base.prototype.show */ | |
573 | show: function () { | |
574 | if (MochiKit.Style.getStyle(this.update, 'display') == 'none') { | |
575 | this.options.onShow(this.element, this.update); | |
576 | } | |
577 | if (!this.iefix && /MSIE/.test(navigator.userAgent && | |
578 | (MochiKit.Style.getStyle(this.update, 'position') == 'absolute'))) { | |
579 | new Insertion.After(this.update, | |
580 | '<iframe id="' + this.update.id + '_iefix" '+ | |
581 | 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + | |
582 | 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); | |
583 | this.iefix = MochiKit.DOM.getElement(this.update.id + '_iefix'); | |
584 | } | |
585 | if (this.iefix) { | |
586 | setTimeout(MochiKit.Base.bind(this.fixIEOverlapping, this), 50); | |
587 | } | |
588 | }, | |
589 | ||
590 | /** @id Autocompleter.Base.prototype.fixIEOverlapping */ | |
591 | fixIEOverlapping: function () { | |
592 | MochiKit.Position.clone(this.update, this.iefix); | |
593 | this.iefix.style.zIndex = 1; | |
594 | this.update.style.zIndex = 2; | |
595 | MochiKit.Style.showElement(this.iefix); | |
596 | }, | |
597 | ||
598 | /** @id Autocompleter.Base.prototype.hide */ | |
599 | hide: function () { | |
600 | this.stopIndicator(); | |
601 | if (MochiKit.Style.getStyle(this.update, 'display') != 'none') { | |
602 | this.options.onHide(this.element, this.update); | |
603 | } | |
604 | if (this.iefix) { | |
605 | MochiKit.Style.hideElement(this.iefix); | |
606 | } | |
607 | }, | |
608 | ||
609 | /** @id Autocompleter.Base.prototype.startIndicator */ | |
610 | startIndicator: function () { | |
611 | if (this.options.indicator) { | |
612 | MochiKit.Style.showElement(this.options.indicator); | |
613 | } | |
614 | }, | |
615 | ||
616 | /** @id Autocompleter.Base.prototype.stopIndicator */ | |
617 | stopIndicator: function () { | |
618 | if (this.options.indicator) { | |
619 | MochiKit.Style.hideElement(this.options.indicator); | |
620 | } | |
621 | }, | |
622 | ||
623 | /** @id Autocompleter.Base.prototype.onKeyPress */ | |
624 | onKeyPress: function (event) { | |
625 | if (this.active) { | |
626 | if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") { | |
627 | this.selectEntry(); | |
628 | MochiKit.Event.stop(event); | |
629 | } else if (event.key().string == "KEY_ESCAPE") { | |
630 | this.hide(); | |
631 | this.active = false; | |
632 | MochiKit.Event.stop(event); | |
633 | return; | |
634 | } else if (event.key().string == "KEY_LEFT" || event.key().string == "KEY_RIGHT") { | |
635 | return; | |
636 | } else if (event.key().string == "KEY_UP") { | |
637 | this.markPrevious(); | |
638 | this.render(); | |
639 | if (/AppleWebKit'/.test(navigator.appVersion)) { | |
640 | event.stop(); | |
641 | } | |
642 | return; | |
643 | } else if (event.key().string == "KEY_DOWN") { | |
644 | this.markNext(); | |
645 | this.render(); | |
646 | if (/AppleWebKit'/.test(navigator.appVersion)) { | |
647 | event.stop(); | |
648 | } | |
649 | return; | |
650 | } | |
651 | } else { | |
652 | if (event.key().string == "KEY_TAB" || event.key().string == "KEY_RETURN") { | |
653 | return; | |
654 | } | |
655 | } | |
656 | ||
657 | this.changed = true; | |
658 | this.hasFocus = true; | |
659 | ||
660 | if (this.observer) { | |
661 | clearTimeout(this.observer); | |
662 | } | |
663 | this.observer = setTimeout(MochiKit.Base.bind(this.onObserverEvent, this), | |
664 | this.options.frequency*1000); | |
665 | }, | |
666 | ||
667 | /** @id Autocompleter.Base.prototype.findElement */ | |
668 | findElement: function (event, tagName) { | |
669 | var element = event.target; | |
670 | while (element.parentNode && (!element.tagName || | |
671 | (element.tagName.toUpperCase() != tagName.toUpperCase()))) { | |
672 | element = element.parentNode; | |
673 | } | |
674 | return element; | |
675 | }, | |
676 | ||
677 | /** @id Autocompleter.Base.prototype.hover */ | |
678 | onHover: function (event) { | |
679 | var element = this.findElement(event, 'LI'); | |
680 | if (this.index != element.autocompleteIndex) { | |
681 | this.index = element.autocompleteIndex; | |
682 | this.render(); | |
683 | } | |
684 | event.stop(); | |
685 | }, | |
686 | ||
687 | /** @id Autocompleter.Base.prototype.onClick */ | |
688 | onClick: function (event) { | |
689 | var element = this.findElement(event, 'LI'); | |
690 | this.index = element.autocompleteIndex; | |
691 | this.selectEntry(); | |
692 | this.hide(); | |
693 | }, | |
694 | ||
695 | /** @id Autocompleter.Base.prototype.onBlur */ | |
696 | onBlur: function (event) { | |
697 | // needed to make click events working | |
698 | setTimeout(MochiKit.Base.bind(this.hide, this), 250); | |
699 | this.hasFocus = false; | |
700 | this.active = false; | |
701 | }, | |
702 | ||
703 | /** @id Autocompleter.Base.prototype.render */ | |
704 | render: function () { | |
705 | if (this.entryCount > 0) { | |
706 | for (var i = 0; i < this.entryCount; i++) { | |
707 | this.index == i ? | |
708 | MochiKit.DOM.addElementClass(this.getEntry(i), 'selected') : | |
709 | MochiKit.DOM.removeElementClass(this.getEntry(i), 'selected'); | |
710 | } | |
711 | if (this.hasFocus) { | |
712 | this.show(); | |
713 | this.active = true; | |
714 | } | |
715 | } else { | |
716 | this.active = false; | |
717 | this.hide(); | |
718 | } | |
719 | }, | |
720 | ||
721 | /** @id Autocompleter.Base.prototype.markPrevious */ | |
722 | markPrevious: function () { | |
723 | if (this.index > 0) { | |
724 | this.index-- | |
725 | } else { | |
726 | this.index = this.entryCount-1; | |
727 | } | |
728 | }, | |
729 | ||
730 | /** @id Autocompleter.Base.prototype.markNext */ | |
731 | markNext: function () { | |
732 | if (this.index < this.entryCount-1) { | |
733 | this.index++ | |
734 | } else { | |
735 | this.index = 0; | |
736 | } | |
737 | }, | |
738 | ||
739 | /** @id Autocompleter.Base.prototype.getEntry */ | |
740 | getEntry: function (index) { | |
741 | return this.update.firstChild.childNodes[index]; | |
742 | }, | |
743 | ||
744 | /** @id Autocompleter.Base.prototype.getCurrentEntry */ | |
745 | getCurrentEntry: function () { | |
746 | return this.getEntry(this.index); | |
747 | }, | |
748 | ||
749 | /** @id Autocompleter.Base.prototype.selectEntry */ | |
750 | selectEntry: function () { | |
751 | this.active = false; | |
752 | this.updateElement(this.getCurrentEntry()); | |
753 | }, | |
754 | ||
755 | /** @id Autocompleter.Base.prototype.collectTextNodesIgnoreClass */ | |
756 | collectTextNodesIgnoreClass: function (element, className) { | |
757 | return MochiKit.Base.flattenArray(MochiKit.Base.map(function (node) { | |
758 | if (node.nodeType == 3) { | |
759 | return node.nodeValue; | |
760 | } else if (node.hasChildNodes() && !MochiKit.DOM.hasElementClass(node, className)) { | |
761 | return this.collectTextNodesIgnoreClass(node, className); | |
762 | } | |
763 | return ''; | |
764 | }, MochiKit.DOM.getElement(element).childNodes)).join(''); | |
765 | }, | |
766 | ||
767 | /** @id Autocompleter.Base.prototype.updateElement */ | |
768 | updateElement: function (selectedElement) { | |
769 | if (this.options.updateElement) { | |
770 | this.options.updateElement(selectedElement); | |
771 | return; | |
772 | } | |
773 | var value = ''; | |
774 | if (this.options.select) { | |
775 | var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; | |
776 | if (nodes.length > 0) { | |
777 | value = MochiKit.DOM.scrapeText(nodes[0]); | |
778 | } | |
779 | } else { | |
780 | value = this.collectTextNodesIgnoreClass(selectedElement, 'informal'); | |
781 | } | |
782 | var lastTokenPos = this.findLastToken(); | |
783 | if (lastTokenPos != -1) { | |
784 | var newValue = this.element.value.substr(0, lastTokenPos + 1); | |
785 | var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); | |
786 | if (whitespace) { | |
787 | newValue += whitespace[0]; | |
788 | } | |
789 | this.element.value = newValue + value; | |
790 | } else { | |
791 | this.element.value = value; | |
792 | } | |
793 | this.element.focus(); | |
794 | ||
795 | if (this.options.afterUpdateElement) { | |
796 | this.options.afterUpdateElement(this.element, selectedElement); | |
797 | } | |
798 | }, | |
799 | ||
800 | /** @id Autocompleter.Base.prototype.updateChoices */ | |
801 | updateChoices: function (choices) { | |
802 | if (!this.changed && this.hasFocus) { | |
803 | this.update.innerHTML = choices; | |
804 | var d = MochiKit.DOM; | |
805 | d.removeEmptyTextNodes(this.update); | |
806 | d.removeEmptyTextNodes(this.update.firstChild); | |
807 | ||
808 | if (this.update.firstChild && this.update.firstChild.childNodes) { | |
809 | this.entryCount = this.update.firstChild.childNodes.length; | |
810 | for (var i = 0; i < this.entryCount; i++) { | |
811 | var entry = this.getEntry(i); | |
812 | entry.autocompleteIndex = i; | |
813 | this.addObservers(entry); | |
814 | } | |
815 | } else { | |
816 | this.entryCount = 0; | |
817 | } | |
818 | ||
819 | this.stopIndicator(); | |
820 | ||
821 | this.index = 0; | |
822 | this.render(); | |
823 | } | |
824 | }, | |
825 | ||
826 | /** @id Autocompleter.Base.prototype.addObservers */ | |
827 | addObservers: function (element) { | |
828 | MochiKit.Signal.connect(element, 'onmouseover', this, this.onHover); | |
829 | MochiKit.Signal.connect(element, 'onclick', this, this.onClick); | |
830 | }, | |
831 | ||
832 | /** @id Autocompleter.Base.prototype.onObserverEvent */ | |
833 | onObserverEvent: function () { | |
834 | this.changed = false; | |
835 | if (this.getToken().length >= this.options.minChars) { | |
836 | this.startIndicator(); | |
837 | this.getUpdatedChoices(); | |
838 | } else { | |
839 | this.active = false; | |
840 | this.hide(); | |
841 | } | |
842 | }, | |
843 | ||
844 | /** @id Autocompleter.Base.prototype.getToken */ | |
845 | getToken: function () { | |
846 | var tokenPos = this.findLastToken(); | |
847 | if (tokenPos != -1) { | |
848 | var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); | |
849 | } else { | |
850 | var ret = this.element.value; | |
851 | } | |
852 | return /\n/.test(ret) ? '' : ret; | |
853 | }, | |
854 | ||
855 | /** @id Autocompleter.Base.prototype.findLastToken */ | |
856 | findLastToken: function () { | |
857 | var lastTokenPos = -1; | |
858 | ||
859 | for (var i = 0; i < this.options.tokens.length; i++) { | |
860 | var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]); | |
861 | if (thisTokenPos > lastTokenPos) { | |
862 | lastTokenPos = thisTokenPos; | |
863 | } | |
864 | } | |
865 | return lastTokenPos; | |
866 | } | |
867 | } | |
868 | ||
869 | /** @id Ajax.Autocompleter */ | |
870 | Ajax.Autocompleter = function (element, update, url, options) { | |
871 | this.__init__(element, update, url, options); | |
872 | }; | |
873 | ||
874 | MochiKit.Base.update(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype); | |
875 | ||
876 | MochiKit.Base.update(Ajax.Autocompleter.prototype, { | |
877 | __init__: function (element, update, url, options) { | |
878 | this.baseInitialize(element, update, options); | |
879 | this.options.asynchronous = true; | |
880 | this.options.onComplete = MochiKit.Base.bind(this.onComplete, this); | |
881 | this.options.defaultParams = this.options.parameters || null; | |
882 | this.url = url; | |
883 | }, | |
884 | ||
885 | /** @id Ajax.Autocompleter.prototype.getUpdatedChoices */ | |
886 | getUpdatedChoices: function () { | |
887 | var entry = encodeURIComponent(this.options.paramName) + '=' + | |
888 | encodeURIComponent(this.getToken()); | |
889 | ||
890 | this.options.parameters = this.options.callback ? | |
891 | this.options.callback(this.element, entry) : entry; | |
892 | ||
893 | if (this.options.defaultParams) { | |
894 | this.options.parameters += '&' + this.options.defaultParams; | |
895 | } | |
896 | new Ajax.Request(this.url, this.options); | |
897 | }, | |
898 | ||
899 | /** @id Ajax.Autocompleter.prototype.onComplete */ | |
900 | onComplete: function (request) { | |
901 | this.updateChoices(request.responseText); | |
902 | } | |
903 | }); | |
904 | ||
905 | /*** | |
906 | ||
907 | The local array autocompleter. Used when you'd prefer to | |
908 | inject an array of autocompletion options into the page, rather | |
909 | than sending out Ajax queries, which can be quite slow sometimes. | |
910 | ||
911 | The constructor takes four parameters. The first two are, as usual, | |
912 | the id of the monitored textbox, and id of the autocompletion menu. | |
913 | The third is the array you want to autocomplete from, and the fourth | |
914 | is the options block. | |
915 | ||
916 | Extra local autocompletion options: | |
917 | - choices - How many autocompletion choices to offer | |
918 | ||
919 | - partialSearch - If false, the autocompleter will match entered | |
920 | text only at the beginning of strings in the | |
921 | autocomplete array. Defaults to true, which will | |
922 | match text at the beginning of any *word* in the | |
923 | strings in the autocomplete array. If you want to | |
924 | search anywhere in the string, additionally set | |
925 | the option fullSearch to true (default: off). | |
926 | ||
927 | - fullSsearch - Search anywhere in autocomplete array strings. | |
928 | ||
929 | - partialChars - How many characters to enter before triggering | |
930 | a partial match (unlike minChars, which defines | |
931 | how many characters are required to do any match | |
932 | at all). Defaults to 2. | |
933 | ||
934 | - ignoreCase - Whether to ignore case when autocompleting. | |
935 | Defaults to true. | |
936 | ||
937 | It's possible to pass in a custom function as the 'selector' | |
938 | option, if you prefer to write your own autocompletion logic. | |
939 | In that case, the other options above will not apply unless | |
940 | you support them. | |
941 | ||
942 | ***/ | |
943 | ||
944 | /** @id Autocompleter.Local */ | |
945 | Autocompleter.Local = function (element, update, array, options) { | |
946 | this.__init__(element, update, array, options); | |
947 | }; | |
948 | ||
949 | MochiKit.Base.update(Autocompleter.Local.prototype, Autocompleter.Base.prototype); | |
950 | ||
951 | MochiKit.Base.update(Autocompleter.Local.prototype, { | |
952 | __init__: function (element, update, array, options) { | |
953 | this.baseInitialize(element, update, options); | |
954 | this.options.array = array; | |
955 | }, | |
956 | ||
957 | /** @id Autocompleter.Local.prototype.getUpdatedChoices */ | |
958 | getUpdatedChoices: function () { | |
959 | this.updateChoices(this.options.selector(this)); | |
960 | }, | |
961 | ||
962 | /** @id Autocompleter.Local.prototype.setOptions */ | |
963 | setOptions: function (options) { | |
964 | this.options = MochiKit.Base.update({ | |
965 | choices: 10, | |
966 | partialSearch: true, | |
967 | partialChars: 2, | |
968 | ignoreCase: true, | |
969 | fullSearch: false, | |
970 | selector: function (instance) { | |
971 | var ret = []; // Beginning matches | |
972 | var partial = []; // Inside matches | |
973 | var entry = instance.getToken(); | |
974 | var count = 0; | |
975 | ||
976 | for (var i = 0; i < instance.options.array.length && | |
977 | ret.length < instance.options.choices ; i++) { | |
978 | ||
979 | var elem = instance.options.array[i]; | |
980 | var foundPos = instance.options.ignoreCase ? | |
981 | elem.toLowerCase().indexOf(entry.toLowerCase()) : | |
982 | elem.indexOf(entry); | |
983 | ||
984 | while (foundPos != -1) { | |
985 | if (foundPos === 0 && elem.length != entry.length) { | |
986 | ret.push('<li><strong>' + elem.substr(0, entry.length) + '</strong>' + | |
987 | elem.substr(entry.length) + '</li>'); | |
988 | break; | |
989 | } else if (entry.length >= instance.options.partialChars && | |
990 | instance.options.partialSearch && foundPos != -1) { | |
991 | if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos - 1, 1))) { | |
992 | partial.push('<li>' + elem.substr(0, foundPos) + '<strong>' + | |
993 | elem.substr(foundPos, entry.length) + '</strong>' + elem.substr( | |
994 | foundPos + entry.length) + '</li>'); | |
995 | break; | |
996 | } | |
997 | } | |
998 | ||
999 | foundPos = instance.options.ignoreCase ? | |
1000 | elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : | |
1001 | elem.indexOf(entry, foundPos + 1); | |
1002 | ||
1003 | } | |
1004 | } | |
1005 | if (partial.length) { | |
1006 | ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) | |
1007 | } | |
1008 | return '<ul>' + ret.join('') + '</ul>'; | |
1009 | } | |
1010 | }, options || {}); | |
1011 | } | |
1012 | }); | |
1013 | ||
1014 | /*** | |
1015 | ||
1016 | AJAX in-place editor | |
1017 | ||
1018 | see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor | |
1019 | ||
1020 | Use this if you notice weird scrolling problems on some browsers, | |
1021 | the DOM might be a bit confused when this gets called so do this | |
1022 | waits 1 ms (with setTimeout) until it does the activation | |
1023 | ||
1024 | ***/ | |
1025 | ||
1026 | /** @id Ajax.InPlaceEditor */ | |
1027 | Ajax.InPlaceEditor = function (element, url, options) { | |
1028 | this.__init__(element, url, options); | |
1029 | }; | |
1030 | ||
1031 | /** @id Ajax.InPlaceEditor.defaultHighlightColor */ | |
1032 | Ajax.InPlaceEditor.defaultHighlightColor = '#FFFF99'; | |
1033 | ||
1034 | Ajax.InPlaceEditor.prototype = { | |
1035 | __init__: function (element, url, options) { | |
1036 | this.url = url; | |
1037 | this.element = MochiKit.DOM.getElement(element); | |
1038 | ||
1039 | this.options = MochiKit.Base.update({ | |
1040 | okButton: true, | |
1041 | okText: 'ok', | |
1042 | cancelLink: true, | |
1043 | cancelText: 'cancel', | |
1044 | savingText: 'Saving...', | |
1045 | clickToEditText: 'Click to edit', | |
1046 | okText: 'ok', | |
1047 | rows: 1, | |
1048 | onComplete: function (transport, element) { | |
1049 | new MochiKit.Visual.Highlight(element, {startcolor: this.options.highlightcolor}); | |
1050 | }, | |
1051 | onFailure: function (transport) { | |
1052 | alert('Error communicating with the server: ' + MochiKit.Base.stripTags(transport.responseText)); | |
1053 | }, | |
1054 | callback: function (form) { | |
1055 | return MochiKit.DOM.formContents(form); | |
1056 | }, | |
1057 | handleLineBreaks: true, | |
1058 | loadingText: 'Loading...', | |
1059 | savingClassName: 'inplaceeditor-saving', | |
1060 | loadingClassName: 'inplaceeditor-loading', | |
1061 | formClassName: 'inplaceeditor-form', | |
1062 | highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, | |
1063 | highlightendcolor: '#FFFFFF', | |
1064 | externalControl: null, | |
1065 | submitOnBlur: false, | |
1066 | ajaxOptions: {} | |
1067 | }, options || {}); | |
1068 | ||
1069 | if (!this.options.formId && this.element.id) { | |
1070 | this.options.formId = this.element.id + '-inplaceeditor'; | |
1071 | if (MochiKit.DOM.getElement(this.options.formId)) { | |
1072 | // there's already a form with that name, don't specify an id | |
1073 | this.options.formId = null; | |
1074 | } | |
1075 | } | |
1076 | ||
1077 | if (this.options.externalControl) { | |
1078 | this.options.externalControl = MochiKit.DOM.getElement(this.options.externalControl); | |
1079 | } | |
1080 | ||
1081 | this.originalBackground = MochiKit.Style.getStyle(this.element, 'background-color'); | |
1082 | if (!this.originalBackground) { | |
1083 | this.originalBackground = 'transparent'; | |
1084 | } | |
1085 | ||
1086 | this.element.title = this.options.clickToEditText; | |
1087 | ||
1088 | this.onclickListener = MochiKit.Signal.connect(this.element, 'onclick', this, this.enterEditMode); | |
1089 | this.mouseoverListener = MochiKit.Signal.connect(this.element, 'onmouseover', this, this.enterHover); | |
1090 | this.mouseoutListener = MochiKit.Signal.connect(this.element, 'onmouseout', this, this.leaveHover); | |
1091 | if (this.options.externalControl) { | |
1092 | this.onclickListenerExternal = MochiKit.Signal.connect(this.options.externalControl, | |
1093 | 'onclick', this, this.enterEditMode); | |
1094 | this.mouseoverListenerExternal = MochiKit.Signal.connect(this.options.externalControl, | |
1095 | 'onmouseover', this, this.enterHover); | |
1096 | this.mouseoutListenerExternal = MochiKit.Signal.connect(this.options.externalControl, | |
1097 | 'onmouseout', this, this.leaveHover); | |
1098 | } | |
1099 | }, | |
1100 | ||
1101 | /** @id Ajax.InPlaceEditor.prototype.enterEditMode */ | |
1102 | enterEditMode: function (evt) { | |
1103 | if (this.saving) { | |
1104 | return; | |
1105 | } | |
1106 | if (this.editing) { | |
1107 | return; | |
1108 | } | |
1109 | this.editing = true; | |
1110 | this.onEnterEditMode(); | |
1111 | if (this.options.externalControl) { | |
1112 | MochiKit.Style.hideElement(this.options.externalControl); | |
1113 | } | |
1114 | MochiKit.Style.hideElement(this.element); | |
1115 | this.createForm(); | |
1116 | this.element.parentNode.insertBefore(this.form, this.element); | |
1117 | Field.scrollFreeActivate(this.editField); | |
1118 | // stop the event to avoid a page refresh in Safari | |
1119 | if (evt) { | |
1120 | evt.stop(); | |
1121 | } | |
1122 | return false; | |
1123 | }, | |
1124 | ||
1125 | /** @id Ajax.InPlaceEditor.prototype.createForm */ | |
1126 | createForm: function () { | |
1127 | this.form = document.createElement('form'); | |
1128 | this.form.id = this.options.formId; | |
1129 | MochiKit.DOM.addElementClass(this.form, this.options.formClassName) | |
1130 | this.form.onsubmit = MochiKit.Base.bind(this.onSubmit, this); | |
1131 | ||
1132 | this.createEditField(); | |
1133 | ||
1134 | if (this.options.textarea) { | |
1135 | var br = document.createElement('br'); | |
1136 | this.form.appendChild(br); | |
1137 | } | |
1138 | ||
1139 | if (this.options.okButton) { | |
1140 | okButton = document.createElement('input'); | |
1141 | okButton.type = 'submit'; | |
1142 | okButton.value = this.options.okText; | |
1143 | this.form.appendChild(okButton); | |
1144 | } | |
1145 | ||
1146 | if (this.options.cancelLink) { | |
1147 | cancelLink = document.createElement('a'); | |
1148 | cancelLink.href = '#'; | |
1149 | cancelLink.appendChild(document.createTextNode(this.options.cancelText)); | |
1150 | cancelLink.onclick = MochiKit.Base.bind(this.onclickCancel, this); | |
1151 | this.form.appendChild(cancelLink); | |
1152 | } | |
1153 | }, | |
1154 | ||
1155 | /** @id Ajax.InPlaceEditor.prototype.hasHTMLLineBreaks */ | |
1156 | hasHTMLLineBreaks: function (string) { | |
1157 | if (!this.options.handleLineBreaks) { | |
1158 | return false; | |
1159 | } | |
1160 | return string.match(/<br/i) || string.match(/<p>/i); | |
1161 | }, | |
1162 | ||
1163 | /** @id Ajax.InPlaceEditor.prototype.convertHTMLLineBreaks */ | |
1164 | convertHTMLLineBreaks: function (string) { | |
1165 | return string.replace(/<br>/gi, '\n').replace(/<br\/>/gi, '\n').replace(/<\/p>/gi, '\n').replace(/<p>/gi, ''); | |
1166 | }, | |
1167 | ||
1168 | /** @id Ajax.InPlaceEditor.prototype.createEditField */ | |
1169 | createEditField: function () { | |
1170 | var text; | |
1171 | if (this.options.loadTextURL) { | |
1172 | text = this.options.loadingText; | |
1173 | } else { | |
1174 | text = this.getText(); | |
1175 | } | |
1176 | ||
1177 | var obj = this; | |
1178 | ||
1179 | if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { | |
1180 | this.options.textarea = false; | |
1181 | var textField = document.createElement('input'); | |
1182 | textField.obj = this; | |
1183 | textField.type = 'text'; | |
1184 | textField.name = 'value'; | |
1185 | textField.value = text; | |
1186 | textField.style.backgroundColor = this.options.highlightcolor; | |
1187 | var size = this.options.size || this.options.cols || 0; | |
1188 | if (size !== 0) { | |
1189 | textField.size = size; | |
1190 | } | |
1191 | if (this.options.submitOnBlur) { | |
1192 | textField.onblur = MochiKit.Base.bind(this.onSubmit, this); | |
1193 | } | |
1194 | this.editField = textField; | |
1195 | } else { | |
1196 | this.options.textarea = true; | |
1197 | var textArea = document.createElement('textarea'); | |
1198 | textArea.obj = this; | |
1199 | textArea.name = 'value'; | |
1200 | textArea.value = this.convertHTMLLineBreaks(text); | |
1201 | textArea.rows = this.options.rows; | |
1202 | textArea.cols = this.options.cols || 40; | |
1203 | if (this.options.submitOnBlur) { | |
1204 | textArea.onblur = MochiKit.Base.bind(this.onSubmit, this); | |
1205 | } | |
1206 | this.editField = textArea; | |
1207 | } | |
1208 | ||
1209 | if (this.options.loadTextURL) { | |
1210 | this.loadExternalText(); | |
1211 | } | |
1212 | this.form.appendChild(this.editField); | |
1213 | }, | |
1214 | ||
1215 | /** @id Ajax.InPlaceEditor.prototype.getText */ | |
1216 | getText: function () { | |
1217 | return this.element.innerHTML; | |
1218 | }, | |
1219 | ||
1220 | /** @id Ajax.InPlaceEditor.prototype.loadExternalText */ | |
1221 | loadExternalText: function () { | |
1222 | MochiKit.DOM.addElementClass(this.form, this.options.loadingClassName); | |
1223 | this.editField.disabled = true; | |
1224 | new Ajax.Request( | |
1225 | this.options.loadTextURL, | |
1226 | MochiKit.Base.update({ | |
1227 | asynchronous: true, | |
1228 | onComplete: MochiKit.Base.bind(this.onLoadedExternalText, this) | |
1229 | }, this.options.ajaxOptions) | |
1230 | ); | |
1231 | }, | |
1232 | ||
1233 | /** @id Ajax.InPlaceEditor.prototype.onLoadedExternalText */ | |
1234 | onLoadedExternalText: function (transport) { | |
1235 | MochiKit.DOM.removeElementClass(this.form, this.options.loadingClassName); | |
1236 | this.editField.disabled = false; | |
1237 | this.editField.value = MochiKit.Base.stripTags(transport); | |
1238 | }, | |
1239 | ||
1240 | /** @id Ajax.InPlaceEditor.prototype.onclickCancel */ | |
1241 | onclickCancel: function () { | |
1242 | this.onComplete(); | |
1243 | this.leaveEditMode(); | |
1244 | return false; | |
1245 | }, | |
1246 | ||
1247 | /** @id Ajax.InPlaceEditor.prototype.onFailure */ | |
1248 | onFailure: function (transport) { | |
1249 | this.options.onFailure(transport); | |
1250 | if (this.oldInnerHTML) { | |
1251 | this.element.innerHTML = this.oldInnerHTML; | |
1252 | this.oldInnerHTML = null; | |
1253 | } | |
1254 | return false; | |
1255 | }, | |
1256 | ||
1257 | /** @id Ajax.InPlaceEditor.prototype.onSubmit */ | |
1258 | onSubmit: function () { | |
1259 | // onLoading resets these so we need to save them away for the Ajax call | |
1260 | var form = this.form; | |
1261 | var value = this.editField.value; | |
1262 | ||
1263 | // do this first, sometimes the ajax call returns before we get a | |
1264 | // chance to switch on Saving which means this will actually switch on | |
1265 | // Saving *after* we have left edit mode causing Saving to be | |
1266 | // displayed indefinitely | |
1267 | this.onLoading(); | |
1268 | ||
1269 | new Ajax.Updater( | |
1270 | { | |
1271 | success: this.element, | |
1272 | // dont update on failure (this could be an option) | |
1273 | failure: null | |
1274 | }, | |
1275 | this.url, | |
1276 | MochiKit.Base.update({ | |
1277 | parameters: this.options.callback(form, value), | |
1278 | onComplete: MochiKit.Base.bind(this.onComplete, this), | |
1279 | onFailure: MochiKit.Base.bind(this.onFailure, this) | |
1280 | }, this.options.ajaxOptions) | |
1281 | ); | |
1282 | // stop the event to avoid a page refresh in Safari | |
1283 | if (arguments.length > 1) { | |
1284 | arguments[0].stop(); | |
1285 | } | |
1286 | return false; | |
1287 | }, | |
1288 | ||
1289 | /** @id Ajax.InPlaceEditor.prototype.onLoading */ | |
1290 | onLoading: function () { | |
1291 | this.saving = true; | |
1292 | this.removeForm(); | |
1293 | this.leaveHover(); | |
1294 | this.showSaving(); | |
1295 | }, | |
1296 | ||
1297 | /** @id Ajax.InPlaceEditor.prototype.onSaving */ | |
1298 | showSaving: function () { | |
1299 | this.oldInnerHTML = this.element.innerHTML; | |
1300 | this.element.innerHTML = this.options.savingText; | |
1301 | MochiKit.DOM.addElementClass(this.element, this.options.savingClassName); | |
1302 | this.element.style.backgroundColor = this.originalBackground; | |
1303 | MochiKit.Style.showElement(this.element); | |
1304 | }, | |
1305 | ||
1306 | /** @id Ajax.InPlaceEditor.prototype.removeForm */ | |
1307 | removeForm: function () { | |
1308 | if (this.form) { | |
1309 | if (this.form.parentNode) { | |
1310 | MochiKit.DOM.removeElement(this.form); | |
1311 | } | |
1312 | this.form = null; | |
1313 | } | |
1314 | }, | |
1315 | ||
1316 | /** @id Ajax.InPlaceEditor.prototype.enterHover */ | |
1317 | enterHover: function () { | |
1318 | if (this.saving) { | |
1319 | return; | |
1320 | } | |
1321 | this.element.style.backgroundColor = this.options.highlightcolor; | |
1322 | if (this.effect) { | |
1323 | this.effect.cancel(); | |
1324 | } | |
1325 | MochiKit.DOM.addElementClass(this.element, this.options.hoverClassName) | |
1326 | }, | |
1327 | ||
1328 | /** @id Ajax.InPlaceEditor.prototype.leaveHover */ | |
1329 | leaveHover: function () { | |
1330 | if (this.options.backgroundColor) { | |
1331 | this.element.style.backgroundColor = this.oldBackground; | |
1332 | } | |
1333 | MochiKit.DOM.removeElementClass(this.element, this.options.hoverClassName) | |
1334 | if (this.saving) { | |
1335 | return; | |
1336 | } | |
1337 | this.effect = new MochiKit.Visual.Highlight(this.element, { | |
1338 | startcolor: this.options.highlightcolor, | |
1339 | endcolor: this.options.highlightendcolor, | |
1340 | restorecolor: this.originalBackground | |
1341 | }); | |
1342 | }, | |
1343 | ||
1344 | /** @id Ajax.InPlaceEditor.prototype.leaveEditMode */ | |
1345 | leaveEditMode: function () { | |
1346 | MochiKit.DOM.removeElementClass(this.element, this.options.savingClassName); | |
1347 | this.removeForm(); | |
1348 | this.leaveHover(); | |
1349 | this.element.style.backgroundColor = this.originalBackground; | |
1350 | MochiKit.Style.showElement(this.element); | |
1351 | if (this.options.externalControl) { | |
1352 | MochiKit.Style.showElement(this.options.externalControl); | |
1353 | } | |
1354 | this.editing = false; | |
1355 | this.saving = false; | |
1356 | this.oldInnerHTML = null; | |
1357 | this.onLeaveEditMode(); | |
1358 | }, | |
1359 | ||
1360 | /** @id Ajax.InPlaceEditor.prototype.onComplete */ | |
1361 | onComplete: function (transport) { | |
1362 | this.leaveEditMode(); | |
1363 | MochiKit.Base.bind(this.options.onComplete, this)(transport, this.element); | |
1364 | }, | |
1365 | ||
1366 | /** @id Ajax.InPlaceEditor.prototype.onEnterEditMode */ | |
1367 | onEnterEditMode: function () {}, | |
1368 | ||
1369 | /** @id Ajax.InPlaceEditor.prototype.onLeaveEditMode */ | |
1370 | onLeaveEditMode: function () {}, | |
1371 | ||
1372 | /** @id Ajax.InPlaceEditor.prototype.dispose */ | |
1373 | dispose: function () { | |
1374 | if (this.oldInnerHTML) { | |
1375 | this.element.innerHTML = this.oldInnerHTML; | |
1376 | } | |
1377 | this.leaveEditMode(); | |
1378 | MochiKit.Signal.disconnect(this.onclickListener); | |
1379 | MochiKit.Signal.disconnect(this.mouseoverListener); | |
1380 | MochiKit.Signal.disconnect(this.mouseoutListener); | |
1381 | if (this.options.externalControl) { | |
1382 | MochiKit.Signal.disconnect(this.onclickListenerExternal); | |
1383 | MochiKit.Signal.disconnect(this.mouseoverListenerExternal); | |
1384 | MochiKit.Signal.disconnect(this.mouseoutListenerExternal); | |
1385 | } | |
1386 | } | |
1387 | }; | |
1388 |