To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / public / javascripts / controls.js @ 442:753f1380d6bc

History | View | Annotate | Download (33.9 KB)

1
// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
//           (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3
//           (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
4
// Contributors:
5
//  Richard Livsey
6
//  Rahul Bhargava
7
//  Rob Wills
8
//
9
// script.aculo.us is freely distributable under the terms of an MIT-style license.
10
// For details, see the script.aculo.us web site: http://script.aculo.us/
11

    
12
// Autocompleter.Base handles all the autocompletion functionality
13
// that's independent of the data source for autocompletion. This
14
// includes drawing the autocompletion menu, observing keyboard
15
// and mouse events, and similar.
16
//
17
// Specific autocompleters need to provide, at the very least,
18
// a getUpdatedChoices function that will be invoked every time
19
// the text inside the monitored textbox changes. This method
20
// should get the text for which to provide autocompletion by
21
// invoking this.getToken(), NOT by directly accessing
22
// this.element.value. This is to allow incremental tokenized
23
// autocompletion. Specific auto-completion logic (AJAX, etc)
24
// belongs in getUpdatedChoices.
25
//
26
// Tokenized incremental autocompletion is enabled automatically
27
// when an autocompleter is instantiated with the 'tokens' option
28
// in the options parameter, e.g.:
29
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
30
// will incrementally autocomplete with a comma as the token.
31
// Additionally, ',' in the above example can be replaced with
32
// a token array, e.g. { tokens: [',', '\n'] } which
33
// enables autocompletion on multiple tokens. This is most
34
// useful when one of the tokens is \n (a newline), as it
35
// allows smart autocompletion after linebreaks.
36

    
37
if(typeof Effect == 'undefined')
38
  throw("controls.js requires including script.aculo.us' effects.js library");
39

    
40
var Autocompleter = { };
41
Autocompleter.Base = Class.create({
42
  baseInitialize: function(element, update, options) {
43
    element          = $(element);
44
    this.element     = element;
45
    this.update      = $(update);
46
    this.hasFocus    = false;
47
    this.changed     = false;
48
    this.active      = false;
49
    this.index       = 0;
50
    this.entryCount  = 0;
51
    this.oldElementValue = this.element.value;
52

    
53
    if(this.setOptions)
54
      this.setOptions(options);
55
    else
56
      this.options = options || { };
57

    
58
    this.options.paramName    = this.options.paramName || this.element.name;
59
    this.options.tokens       = this.options.tokens || [];
60
    this.options.frequency    = this.options.frequency || 0.4;
61
    this.options.minChars     = this.options.minChars || 1;
62
    this.options.onShow       = this.options.onShow ||
63
      function(element, update){
64
        if(!update.style.position || update.style.position=='absolute') {
65
          update.style.position = 'absolute';
66
          Position.clone(element, update, {
67
            setHeight: false,
68
            offsetTop: element.offsetHeight
69
          });
70
        }
71
        Effect.Appear(update,{duration:0.15});
72
      };
73
    this.options.onHide = this.options.onHide ||
74
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };
75

    
76
    if(typeof(this.options.tokens) == 'string')
77
      this.options.tokens = new Array(this.options.tokens);
78
    // Force carriage returns as token delimiters anyway
79
    if (!this.options.tokens.include('\n'))
80
      this.options.tokens.push('\n');
81

    
82
    this.observer = null;
83

    
84
    this.element.setAttribute('autocomplete','off');
85

    
86
    Element.hide(this.update);
87

    
88
    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
89
    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
90
  },
91

    
92
  show: function() {
93
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
94
    if(!this.iefix &&
95
      (Prototype.Browser.IE) &&
96
      (Element.getStyle(this.update, 'position')=='absolute')) {
97
      new Insertion.After(this.update,
98
       '<iframe id="' + this.update.id + '_iefix" '+
99
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
100
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
101
      this.iefix = $(this.update.id+'_iefix');
102
    }
103
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
104
  },
105

    
106
  fixIEOverlapping: function() {
107
    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
108
    this.iefix.style.zIndex = 1;
109
    this.update.style.zIndex = 2;
110
    Element.show(this.iefix);
111
  },
112

    
113
  hide: function() {
114
    this.stopIndicator();
115
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
116
    if(this.iefix) Element.hide(this.iefix);
117
  },
118

    
119
  startIndicator: function() {
120
    if(this.options.indicator) Element.show(this.options.indicator);
121
  },
122

    
123
  stopIndicator: function() {
124
    if(this.options.indicator) Element.hide(this.options.indicator);
125
  },
126

    
127
  onKeyPress: function(event) {
128
    if(this.active)
129
      switch(event.keyCode) {
130
       case Event.KEY_TAB:
131
       case Event.KEY_RETURN:
132
         this.selectEntry();
133
         Event.stop(event);
134
       case Event.KEY_ESC:
135
         this.hide();
136
         this.active = false;
137
         Event.stop(event);
138
         return;
139
       case Event.KEY_LEFT:
140
       case Event.KEY_RIGHT:
141
         return;
142
       case Event.KEY_UP:
143
         this.markPrevious();
144
         this.render();
145
         Event.stop(event);
146
         return;
147
       case Event.KEY_DOWN:
148
         this.markNext();
149
         this.render();
150
         Event.stop(event);
151
         return;
152
      }
153
     else
154
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
155
         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
156

    
157
    this.changed = true;
158
    this.hasFocus = true;
159

    
160
    if(this.observer) clearTimeout(this.observer);
161
      this.observer =
162
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
163
  },
164

    
165
  activate: function() {
166
    this.changed = false;
167
    this.hasFocus = true;
168
    this.getUpdatedChoices();
169
  },
170

    
171
  onHover: function(event) {
172
    var element = Event.findElement(event, 'LI');
173
    if(this.index != element.autocompleteIndex)
174
    {
175
        this.index = element.autocompleteIndex;
176
        this.render();
177
    }
178
    Event.stop(event);
179
  },
180

    
181
  onClick: function(event) {
182
    var element = Event.findElement(event, 'LI');
183
    this.index = element.autocompleteIndex;
184
    this.selectEntry();
185
    this.hide();
186
  },
187

    
188
  onBlur: function(event) {
189
    // needed to make click events working
190
    setTimeout(this.hide.bind(this), 250);
191
    this.hasFocus = false;
192
    this.active = false;
193
  },
194

    
195
  render: function() {
196
    if(this.entryCount > 0) {
197
      for (var i = 0; i < this.entryCount; i++)
198
        this.index==i ?
199
          Element.addClassName(this.getEntry(i),"selected") :
200
          Element.removeClassName(this.getEntry(i),"selected");
201
      if(this.hasFocus) {
202
        this.show();
203
        this.active = true;
204
      }
205
    } else {
206
      this.active = false;
207
      this.hide();
208
    }
209
  },
210

    
211
  markPrevious: function() {
212
    if(this.index > 0) this.index--;
213
      else this.index = this.entryCount-1;
214
    this.getEntry(this.index).scrollIntoView(true);
215
  },
216

    
217
  markNext: function() {
218
    if(this.index < this.entryCount-1) this.index++;
219
      else this.index = 0;
220
    this.getEntry(this.index).scrollIntoView(false);
221
  },
222

    
223
  getEntry: function(index) {
224
    return this.update.firstChild.childNodes[index];
225
  },
226

    
227
  getCurrentEntry: function() {
228
    return this.getEntry(this.index);
229
  },
230

    
231
  selectEntry: function() {
232
    this.active = false;
233
    this.updateElement(this.getCurrentEntry());
234
  },
235

    
236
  updateElement: function(selectedElement) {
237
    if (this.options.updateElement) {
238
      this.options.updateElement(selectedElement);
239
      return;
240
    }
241
    var value = '';
242
    if (this.options.select) {
243
      var nodes = $(selectedElement).select('.' + this.options.select) || [];
244
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
245
    } else
246
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
247

    
248
    var bounds = this.getTokenBounds();
249
    if (bounds[0] != -1) {
250
      var newValue = this.element.value.substr(0, bounds[0]);
251
      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
252
      if (whitespace)
253
        newValue += whitespace[0];
254
      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
255
    } else {
256
      this.element.value = value;
257
    }
258
    this.oldElementValue = this.element.value;
259
    this.element.focus();
260

    
261
    if (this.options.afterUpdateElement)
262
      this.options.afterUpdateElement(this.element, selectedElement);
263
  },
264

    
265
  updateChoices: function(choices) {
266
    if(!this.changed && this.hasFocus) {
267
      this.update.innerHTML = choices;
268
      Element.cleanWhitespace(this.update);
269
      Element.cleanWhitespace(this.update.down());
270

    
271
      if(this.update.firstChild && this.update.down().childNodes) {
272
        this.entryCount =
273
          this.update.down().childNodes.length;
274
        for (var i = 0; i < this.entryCount; i++) {
275
          var entry = this.getEntry(i);
276
          entry.autocompleteIndex = i;
277
          this.addObservers(entry);
278
        }
279
      } else {
280
        this.entryCount = 0;
281
      }
282

    
283
      this.stopIndicator();
284
      this.index = 0;
285

    
286
      if(this.entryCount==1 && this.options.autoSelect) {
287
        this.selectEntry();
288
        this.hide();
289
      } else {
290
        this.render();
291
      }
292
    }
293
  },
294

    
295
  addObservers: function(element) {
296
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
297
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
298
  },
299

    
300
  onObserverEvent: function() {
301
    this.changed = false;
302
    this.tokenBounds = null;
303
    if(this.getToken().length>=this.options.minChars) {
304
      this.getUpdatedChoices();
305
    } else {
306
      this.active = false;
307
      this.hide();
308
    }
309
    this.oldElementValue = this.element.value;
310
  },
311

    
312
  getToken: function() {
313
    var bounds = this.getTokenBounds();
314
    return this.element.value.substring(bounds[0], bounds[1]).strip();
315
  },
316

    
317
  getTokenBounds: function() {
318
    if (null != this.tokenBounds) return this.tokenBounds;
319
    var value = this.element.value;
320
    if (value.strip().empty()) return [-1, 0];
321
    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
322
    var offset = (diff == this.oldElementValue.length ? 1 : 0);
323
    var prevTokenPos = -1, nextTokenPos = value.length;
324
    var tp;
325
    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
326
      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
327
      if (tp > prevTokenPos) prevTokenPos = tp;
328
      tp = value.indexOf(this.options.tokens[index], diff + offset);
329
      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
330
    }
331
    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
332
  }
333
});
334

    
335
Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
336
  var boundary = Math.min(newS.length, oldS.length);
337
  for (var index = 0; index < boundary; ++index)
338
    if (newS[index] != oldS[index])
339
      return index;
340
  return boundary;
341
};
342

    
343
Ajax.Autocompleter = Class.create(Autocompleter.Base, {
344
  initialize: function(element, update, url, options) {
345
    this.baseInitialize(element, update, options);
346
    this.options.asynchronous  = true;
347
    this.options.onComplete    = this.onComplete.bind(this);
348
    this.options.defaultParams = this.options.parameters || null;
349
    this.url                   = url;
350
  },
351

    
352
  getUpdatedChoices: function() {
353
    this.startIndicator();
354

    
355
    var entry = encodeURIComponent(this.options.paramName) + '=' +
356
      encodeURIComponent(this.getToken());
357

    
358
    this.options.parameters = this.options.callback ?
359
      this.options.callback(this.element, entry) : entry;
360

    
361
    if(this.options.defaultParams)
362
      this.options.parameters += '&' + this.options.defaultParams;
363

    
364
    new Ajax.Request(this.url, this.options);
365
  },
366

    
367
  onComplete: function(request) {
368
    this.updateChoices(request.responseText);
369
  }
370
});
371

    
372
// The local array autocompleter. Used when you'd prefer to
373
// inject an array of autocompletion options into the page, rather
374
// than sending out Ajax queries, which can be quite slow sometimes.
375
//
376
// The constructor takes four parameters. The first two are, as usual,
377
// the id of the monitored textbox, and id of the autocompletion menu.
378
// The third is the array you want to autocomplete from, and the fourth
379
// is the options block.
380
//
381
// Extra local autocompletion options:
382
// - choices - How many autocompletion choices to offer
383
//
384
// - partialSearch - If false, the autocompleter will match entered
385
//                    text only at the beginning of strings in the
386
//                    autocomplete array. Defaults to true, which will
387
//                    match text at the beginning of any *word* in the
388
//                    strings in the autocomplete array. If you want to
389
//                    search anywhere in the string, additionally set
390
//                    the option fullSearch to true (default: off).
391
//
392
// - fullSsearch - Search anywhere in autocomplete array strings.
393
//
394
// - partialChars - How many characters to enter before triggering
395
//                   a partial match (unlike minChars, which defines
396
//                   how many characters are required to do any match
397
//                   at all). Defaults to 2.
398
//
399
// - ignoreCase - Whether to ignore case when autocompleting.
400
//                 Defaults to true.
401
//
402
// It's possible to pass in a custom function as the 'selector'
403
// option, if you prefer to write your own autocompletion logic.
404
// In that case, the other options above will not apply unless
405
// you support them.
406

    
407
Autocompleter.Local = Class.create(Autocompleter.Base, {
408
  initialize: function(element, update, array, options) {
409
    this.baseInitialize(element, update, options);
410
    this.options.array = array;
411
  },
412

    
413
  getUpdatedChoices: function() {
414
    this.updateChoices(this.options.selector(this));
415
  },
416

    
417
  setOptions: function(options) {
418
    this.options = Object.extend({
419
      choices: 10,
420
      partialSearch: true,
421
      partialChars: 2,
422
      ignoreCase: true,
423
      fullSearch: false,
424
      selector: function(instance) {
425
        var ret       = []; // Beginning matches
426
        var partial   = []; // Inside matches
427
        var entry     = instance.getToken();
428
        var count     = 0;
429

    
430
        for (var i = 0; i < instance.options.array.length &&
431
          ret.length < instance.options.choices ; i++) {
432

    
433
          var elem = instance.options.array[i];
434
          var foundPos = instance.options.ignoreCase ?
435
            elem.toLowerCase().indexOf(entry.toLowerCase()) :
436
            elem.indexOf(entry);
437

    
438
          while (foundPos != -1) {
439
            if (foundPos == 0 && elem.length != entry.length) {
440
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
441
                elem.substr(entry.length) + "</li>");
442
              break;
443
            } else if (entry.length >= instance.options.partialChars &&
444
              instance.options.partialSearch && foundPos != -1) {
445
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
446
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
447
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
448
                  foundPos + entry.length) + "</li>");
449
                break;
450
              }
451
            }
452

    
453
            foundPos = instance.options.ignoreCase ?
454
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
455
              elem.indexOf(entry, foundPos + 1);
456

    
457
          }
458
        }
459
        if (partial.length)
460
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
461
        return "<ul>" + ret.join('') + "</ul>";
462
      }
463
    }, options || { });
464
  }
465
});
466

    
467
// AJAX in-place editor and collection editor
468
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
469

    
470
// Use this if you notice weird scrolling problems on some browsers,
471
// the DOM might be a bit confused when this gets called so do this
472
// waits 1 ms (with setTimeout) until it does the activation
473
Field.scrollFreeActivate = function(field) {
474
  setTimeout(function() {
475
    Field.activate(field);
476
  }, 1);
477
};
478

    
479
Ajax.InPlaceEditor = Class.create({
480
  initialize: function(element, url, options) {
481
    this.url = url;
482
    this.element = element = $(element);
483
    this.prepareOptions();
484
    this._controls = { };
485
    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
486
    Object.extend(this.options, options || { });
487
    if (!this.options.formId && this.element.id) {
488
      this.options.formId = this.element.id + '-inplaceeditor';
489
      if ($(this.options.formId))
490
        this.options.formId = '';
491
    }
492
    if (this.options.externalControl)
493
      this.options.externalControl = $(this.options.externalControl);
494
    if (!this.options.externalControl)
495
      this.options.externalControlOnly = false;
496
    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
497
    this.element.title = this.options.clickToEditText;
498
    this._boundCancelHandler = this.handleFormCancellation.bind(this);
499
    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
500
    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
501
    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
502
    this._boundWrapperHandler = this.wrapUp.bind(this);
503
    this.registerListeners();
504
  },
505
  checkForEscapeOrReturn: function(e) {
506
    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
507
    if (Event.KEY_ESC == e.keyCode)
508
      this.handleFormCancellation(e);
509
    else if (Event.KEY_RETURN == e.keyCode)
510
      this.handleFormSubmission(e);
511
  },
512
  createControl: function(mode, handler, extraClasses) {
513
    var control = this.options[mode + 'Control'];
514
    var text = this.options[mode + 'Text'];
515
    if ('button' == control) {
516
      var btn = document.createElement('input');
517
      btn.type = 'submit';
518
      btn.value = text;
519
      btn.className = 'editor_' + mode + '_button';
520
      if ('cancel' == mode)
521
        btn.onclick = this._boundCancelHandler;
522
      this._form.appendChild(btn);
523
      this._controls[mode] = btn;
524
    } else if ('link' == control) {
525
      var link = document.createElement('a');
526
      link.href = '#';
527
      link.appendChild(document.createTextNode(text));
528
      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
529
      link.className = 'editor_' + mode + '_link';
530
      if (extraClasses)
531
        link.className += ' ' + extraClasses;
532
      this._form.appendChild(link);
533
      this._controls[mode] = link;
534
    }
535
  },
536
  createEditField: function() {
537
    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
538
    var fld;
539
    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
540
      fld = document.createElement('input');
541
      fld.type = 'text';
542
      var size = this.options.size || this.options.cols || 0;
543
      if (0 < size) fld.size = size;
544
    } else {
545
      fld = document.createElement('textarea');
546
      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
547
      fld.cols = this.options.cols || 40;
548
    }
549
    fld.name = this.options.paramName;
550
    fld.value = text; // No HTML breaks conversion anymore
551
    fld.className = 'editor_field';
552
    if (this.options.submitOnBlur)
553
      fld.onblur = this._boundSubmitHandler;
554
    this._controls.editor = fld;
555
    if (this.options.loadTextURL)
556
      this.loadExternalText();
557
    this._form.appendChild(this._controls.editor);
558
  },
559
  createForm: function() {
560
    var ipe = this;
561
    function addText(mode, condition) {
562
      var text = ipe.options['text' + mode + 'Controls'];
563
      if (!text || condition === false) return;
564
      ipe._form.appendChild(document.createTextNode(text));
565
    };
566
    this._form = $(document.createElement('form'));
567
    this._form.id = this.options.formId;
568
    this._form.addClassName(this.options.formClassName);
569
    this._form.onsubmit = this._boundSubmitHandler;
570
    this.createEditField();
571
    if ('textarea' == this._controls.editor.tagName.toLowerCase())
572
      this._form.appendChild(document.createElement('br'));
573
    if (this.options.onFormCustomization)
574
      this.options.onFormCustomization(this, this._form);
575
    addText('Before', this.options.okControl || this.options.cancelControl);
576
    this.createControl('ok', this._boundSubmitHandler);
577
    addText('Between', this.options.okControl && this.options.cancelControl);
578
    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
579
    addText('After', this.options.okControl || this.options.cancelControl);
580
  },
581
  destroy: function() {
582
    if (this._oldInnerHTML)
583
      this.element.innerHTML = this._oldInnerHTML;
584
    this.leaveEditMode();
585
    this.unregisterListeners();
586
  },
587
  enterEditMode: function(e) {
588
    if (this._saving || this._editing) return;
589
    this._editing = true;
590
    this.triggerCallback('onEnterEditMode');
591
    if (this.options.externalControl)
592
      this.options.externalControl.hide();
593
    this.element.hide();
594
    this.createForm();
595
    this.element.parentNode.insertBefore(this._form, this.element);
596
    if (!this.options.loadTextURL)
597
      this.postProcessEditField();
598
    if (e) Event.stop(e);
599
  },
600
  enterHover: function(e) {
601
    if (this.options.hoverClassName)
602
      this.element.addClassName(this.options.hoverClassName);
603
    if (this._saving) return;
604
    this.triggerCallback('onEnterHover');
605
  },
606
  getText: function() {
607
    return this.element.innerHTML.unescapeHTML();
608
  },
609
  handleAJAXFailure: function(transport) {
610
    this.triggerCallback('onFailure', transport);
611
    if (this._oldInnerHTML) {
612
      this.element.innerHTML = this._oldInnerHTML;
613
      this._oldInnerHTML = null;
614
    }
615
  },
616
  handleFormCancellation: function(e) {
617
    this.wrapUp();
618
    if (e) Event.stop(e);
619
  },
620
  handleFormSubmission: function(e) {
621
    var form = this._form;
622
    var value = $F(this._controls.editor);
623
    this.prepareSubmission();
624
    var params = this.options.callback(form, value) || '';
625
    if (Object.isString(params))
626
      params = params.toQueryParams();
627
    params.editorId = this.element.id;
628
    if (this.options.htmlResponse) {
629
      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
630
      Object.extend(options, {
631
        parameters: params,
632
        onComplete: this._boundWrapperHandler,
633
        onFailure: this._boundFailureHandler
634
      });
635
      new Ajax.Updater({ success: this.element }, this.url, options);
636
    } else {
637
      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
638
      Object.extend(options, {
639
        parameters: params,
640
        onComplete: this._boundWrapperHandler,
641
        onFailure: this._boundFailureHandler
642
      });
643
      new Ajax.Request(this.url, options);
644
    }
645
    if (e) Event.stop(e);
646
  },
647
  leaveEditMode: function() {
648
    this.element.removeClassName(this.options.savingClassName);
649
    this.removeForm();
650
    this.leaveHover();
651
    this.element.style.backgroundColor = this._originalBackground;
652
    this.element.show();
653
    if (this.options.externalControl)
654
      this.options.externalControl.show();
655
    this._saving = false;
656
    this._editing = false;
657
    this._oldInnerHTML = null;
658
    this.triggerCallback('onLeaveEditMode');
659
  },
660
  leaveHover: function(e) {
661
    if (this.options.hoverClassName)
662
      this.element.removeClassName(this.options.hoverClassName);
663
    if (this._saving) return;
664
    this.triggerCallback('onLeaveHover');
665
  },
666
  loadExternalText: function() {
667
    this._form.addClassName(this.options.loadingClassName);
668
    this._controls.editor.disabled = true;
669
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
670
    Object.extend(options, {
671
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
672
      onComplete: Prototype.emptyFunction,
673
      onSuccess: function(transport) {
674
        this._form.removeClassName(this.options.loadingClassName);
675
        var text = transport.responseText;
676
        if (this.options.stripLoadedTextTags)
677
          text = text.stripTags();
678
        this._controls.editor.value = text;
679
        this._controls.editor.disabled = false;
680
        this.postProcessEditField();
681
      }.bind(this),
682
      onFailure: this._boundFailureHandler
683
    });
684
    new Ajax.Request(this.options.loadTextURL, options);
685
  },
686
  postProcessEditField: function() {
687
    var fpc = this.options.fieldPostCreation;
688
    if (fpc)
689
      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
690
  },
691
  prepareOptions: function() {
692
    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
693
    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
694
    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
695
      Object.extend(this.options, defs);
696
    }.bind(this));
697
  },
698
  prepareSubmission: function() {
699
    this._saving = true;
700
    this.removeForm();
701
    this.leaveHover();
702
    this.showSaving();
703
  },
704
  registerListeners: function() {
705
    this._listeners = { };
706
    var listener;
707
    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
708
      listener = this[pair.value].bind(this);
709
      this._listeners[pair.key] = listener;
710
      if (!this.options.externalControlOnly)
711
        this.element.observe(pair.key, listener);
712
      if (this.options.externalControl)
713
        this.options.externalControl.observe(pair.key, listener);
714
    }.bind(this));
715
  },
716
  removeForm: function() {
717
    if (!this._form) return;
718
    this._form.remove();
719
    this._form = null;
720
    this._controls = { };
721
  },
722
  showSaving: function() {
723
    this._oldInnerHTML = this.element.innerHTML;
724
    this.element.innerHTML = this.options.savingText;
725
    this.element.addClassName(this.options.savingClassName);
726
    this.element.style.backgroundColor = this._originalBackground;
727
    this.element.show();
728
  },
729
  triggerCallback: function(cbName, arg) {
730
    if ('function' == typeof this.options[cbName]) {
731
      this.options[cbName](this, arg);
732
    }
733
  },
734
  unregisterListeners: function() {
735
    $H(this._listeners).each(function(pair) {
736
      if (!this.options.externalControlOnly)
737
        this.element.stopObserving(pair.key, pair.value);
738
      if (this.options.externalControl)
739
        this.options.externalControl.stopObserving(pair.key, pair.value);
740
    }.bind(this));
741
  },
742
  wrapUp: function(transport) {
743
    this.leaveEditMode();
744
    // Can't use triggerCallback due to backward compatibility: requires
745
    // binding + direct element
746
    this._boundComplete(transport, this.element);
747
  }
748
});
749

    
750
Object.extend(Ajax.InPlaceEditor.prototype, {
751
  dispose: Ajax.InPlaceEditor.prototype.destroy
752
});
753

    
754
Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
755
  initialize: function($super, element, url, options) {
756
    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
757
    $super(element, url, options);
758
  },
759

    
760
  createEditField: function() {
761
    var list = document.createElement('select');
762
    list.name = this.options.paramName;
763
    list.size = 1;
764
    this._controls.editor = list;
765
    this._collection = this.options.collection || [];
766
    if (this.options.loadCollectionURL)
767
      this.loadCollection();
768
    else
769
      this.checkForExternalText();
770
    this._form.appendChild(this._controls.editor);
771
  },
772

    
773
  loadCollection: function() {
774
    this._form.addClassName(this.options.loadingClassName);
775
    this.showLoadingText(this.options.loadingCollectionText);
776
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
777
    Object.extend(options, {
778
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
779
      onComplete: Prototype.emptyFunction,
780
      onSuccess: function(transport) {
781
        var js = transport.responseText.strip();
782
        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
783
          throw('Server returned an invalid collection representation.');
784
        this._collection = eval(js);
785
        this.checkForExternalText();
786
      }.bind(this),
787
      onFailure: this.onFailure
788
    });
789
    new Ajax.Request(this.options.loadCollectionURL, options);
790
  },
791

    
792
  showLoadingText: function(text) {
793
    this._controls.editor.disabled = true;
794
    var tempOption = this._controls.editor.firstChild;
795
    if (!tempOption) {
796
      tempOption = document.createElement('option');
797
      tempOption.value = '';
798
      this._controls.editor.appendChild(tempOption);
799
      tempOption.selected = true;
800
    }
801
    tempOption.update((text || '').stripScripts().stripTags());
802
  },
803

    
804
  checkForExternalText: function() {
805
    this._text = this.getText();
806
    if (this.options.loadTextURL)
807
      this.loadExternalText();
808
    else
809
      this.buildOptionList();
810
  },
811

    
812
  loadExternalText: function() {
813
    this.showLoadingText(this.options.loadingText);
814
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
815
    Object.extend(options, {
816
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
817
      onComplete: Prototype.emptyFunction,
818
      onSuccess: function(transport) {
819
        this._text = transport.responseText.strip();
820
        this.buildOptionList();
821
      }.bind(this),
822
      onFailure: this.onFailure
823
    });
824
    new Ajax.Request(this.options.loadTextURL, options);
825
  },
826

    
827
  buildOptionList: function() {
828
    this._form.removeClassName(this.options.loadingClassName);
829
    this._collection = this._collection.map(function(entry) {
830
      return 2 === entry.length ? entry : [entry, entry].flatten();
831
    });
832
    var marker = ('value' in this.options) ? this.options.value : this._text;
833
    var textFound = this._collection.any(function(entry) {
834
      return entry[0] == marker;
835
    }.bind(this));
836
    this._controls.editor.update('');
837
    var option;
838
    this._collection.each(function(entry, index) {
839
      option = document.createElement('option');
840
      option.value = entry[0];
841
      option.selected = textFound ? entry[0] == marker : 0 == index;
842
      option.appendChild(document.createTextNode(entry[1]));
843
      this._controls.editor.appendChild(option);
844
    }.bind(this));
845
    this._controls.editor.disabled = false;
846
    Field.scrollFreeActivate(this._controls.editor);
847
  }
848
});
849

    
850
//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
851
//**** This only  exists for a while,  in order to  let ****
852
//**** users adapt to  the new API.  Read up on the new ****
853
//**** API and convert your code to it ASAP!            ****
854

    
855
Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
856
  if (!options) return;
857
  function fallback(name, expr) {
858
    if (name in options || expr === undefined) return;
859
    options[name] = expr;
860
  };
861
  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
862
    options.cancelLink == options.cancelButton == false ? false : undefined)));
863
  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
864
    options.okLink == options.okButton == false ? false : undefined)));
865
  fallback('highlightColor', options.highlightcolor);
866
  fallback('highlightEndColor', options.highlightendcolor);
867
};
868

    
869
Object.extend(Ajax.InPlaceEditor, {
870
  DefaultOptions: {
871
    ajaxOptions: { },
872
    autoRows: 3,                                // Use when multi-line w/ rows == 1
873
    cancelControl: 'link',                      // 'link'|'button'|false
874
    cancelText: 'cancel',
875
    clickToEditText: 'Click to edit',
876
    externalControl: null,                      // id|elt
877
    externalControlOnly: false,
878
    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
879
    formClassName: 'inplaceeditor-form',
880
    formId: null,                               // id|elt
881
    highlightColor: '#ffff99',
882
    highlightEndColor: '#ffffff',
883
    hoverClassName: '',
884
    htmlResponse: true,
885
    loadingClassName: 'inplaceeditor-loading',
886
    loadingText: 'Loading...',
887
    okControl: 'button',                        // 'link'|'button'|false
888
    okText: 'ok',
889
    paramName: 'value',
890
    rows: 1,                                    // If 1 and multi-line, uses autoRows
891
    savingClassName: 'inplaceeditor-saving',
892
    savingText: 'Saving...',
893
    size: 0,
894
    stripLoadedTextTags: false,
895
    submitOnBlur: false,
896
    textAfterControls: '',
897
    textBeforeControls: '',
898
    textBetweenControls: ''
899
  },
900
  DefaultCallbacks: {
901
    callback: function(form) {
902
      return Form.serialize(form);
903
    },
904
    onComplete: function(transport, element) {
905
      // For backward compatibility, this one is bound to the IPE, and passes
906
      // the element directly.  It was too often customized, so we don't break it.
907
      new Effect.Highlight(element, {
908
        startcolor: this.options.highlightColor, keepBackgroundImage: true });
909
    },
910
    onEnterEditMode: null,
911
    onEnterHover: function(ipe) {
912
      ipe.element.style.backgroundColor = ipe.options.highlightColor;
913
      if (ipe._effect)
914
        ipe._effect.cancel();
915
    },
916
    onFailure: function(transport, ipe) {
917
      alert('Error communication with the server: ' + transport.responseText.stripTags());
918
    },
919
    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
920
    onLeaveEditMode: null,
921
    onLeaveHover: function(ipe) {
922
      ipe._effect = new Effect.Highlight(ipe.element, {
923
        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
924
        restorecolor: ipe._originalBackground, keepBackgroundImage: true
925
      });
926
    }
927
  },
928
  Listeners: {
929
    click: 'enterEditMode',
930
    keydown: 'checkForEscapeOrReturn',
931
    mouseover: 'enterHover',
932
    mouseout: 'leaveHover'
933
  }
934
});
935

    
936
Ajax.InPlaceCollectionEditor.DefaultOptions = {
937
  loadingCollectionText: 'Loading options...'
938
};
939

    
940
// Delayed observer, like Form.Element.Observer,
941
// but waits for delay after last key input
942
// Ideal for live-search fields
943

    
944
Form.Element.DelayedObserver = Class.create({
945
  initialize: function(element, delay, callback) {
946
    this.delay     = delay || 0.5;
947
    this.element   = $(element);
948
    this.callback  = callback;
949
    this.timer     = null;
950
    this.lastValue = $F(this.element);
951
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
952
  },
953
  delayedListener: function(event) {
954
    if(this.lastValue == $F(this.element)) return;
955
    if(this.timer) clearTimeout(this.timer);
956
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
957
    this.lastValue = $F(this.element);
958
  },
959
  onTimerEvent: function() {
960
    this.timer = null;
961
    this.callback(this.element, $F(this.element));
962
  }
963
});