annotate public/javascripts/controls.js @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents 513646585e45
children
rev   line source
Chris@0 1 // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
Chris@0 2 // (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
Chris@0 3 // (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
Chris@0 4 // Contributors:
Chris@0 5 // Richard Livsey
Chris@0 6 // Rahul Bhargava
Chris@0 7 // Rob Wills
Chris@0 8 //
Chris@0 9 // script.aculo.us is freely distributable under the terms of an MIT-style license.
Chris@0 10 // For details, see the script.aculo.us web site: http://script.aculo.us/
Chris@0 11
Chris@0 12 // Autocompleter.Base handles all the autocompletion functionality
Chris@0 13 // that's independent of the data source for autocompletion. This
Chris@0 14 // includes drawing the autocompletion menu, observing keyboard
Chris@0 15 // and mouse events, and similar.
Chris@0 16 //
Chris@0 17 // Specific autocompleters need to provide, at the very least,
Chris@0 18 // a getUpdatedChoices function that will be invoked every time
Chris@0 19 // the text inside the monitored textbox changes. This method
Chris@0 20 // should get the text for which to provide autocompletion by
Chris@0 21 // invoking this.getToken(), NOT by directly accessing
Chris@0 22 // this.element.value. This is to allow incremental tokenized
Chris@0 23 // autocompletion. Specific auto-completion logic (AJAX, etc)
Chris@0 24 // belongs in getUpdatedChoices.
Chris@0 25 //
Chris@0 26 // Tokenized incremental autocompletion is enabled automatically
Chris@0 27 // when an autocompleter is instantiated with the 'tokens' option
Chris@0 28 // in the options parameter, e.g.:
Chris@0 29 // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
Chris@0 30 // will incrementally autocomplete with a comma as the token.
Chris@0 31 // Additionally, ',' in the above example can be replaced with
Chris@0 32 // a token array, e.g. { tokens: [',', '\n'] } which
Chris@0 33 // enables autocompletion on multiple tokens. This is most
Chris@0 34 // useful when one of the tokens is \n (a newline), as it
Chris@0 35 // allows smart autocompletion after linebreaks.
Chris@0 36
Chris@0 37 if(typeof Effect == 'undefined')
Chris@0 38 throw("controls.js requires including script.aculo.us' effects.js library");
Chris@0 39
Chris@0 40 var Autocompleter = { };
Chris@0 41 Autocompleter.Base = Class.create({
Chris@0 42 baseInitialize: function(element, update, options) {
Chris@0 43 element = $(element);
Chris@0 44 this.element = element;
Chris@0 45 this.update = $(update);
Chris@0 46 this.hasFocus = false;
Chris@0 47 this.changed = false;
Chris@0 48 this.active = false;
Chris@0 49 this.index = 0;
Chris@0 50 this.entryCount = 0;
Chris@0 51 this.oldElementValue = this.element.value;
Chris@0 52
Chris@0 53 if(this.setOptions)
Chris@0 54 this.setOptions(options);
Chris@0 55 else
Chris@0 56 this.options = options || { };
Chris@0 57
Chris@0 58 this.options.paramName = this.options.paramName || this.element.name;
Chris@0 59 this.options.tokens = this.options.tokens || [];
Chris@0 60 this.options.frequency = this.options.frequency || 0.4;
Chris@0 61 this.options.minChars = this.options.minChars || 1;
Chris@0 62 this.options.onShow = this.options.onShow ||
Chris@0 63 function(element, update){
Chris@0 64 if(!update.style.position || update.style.position=='absolute') {
Chris@0 65 update.style.position = 'absolute';
Chris@0 66 Position.clone(element, update, {
Chris@0 67 setHeight: false,
Chris@0 68 offsetTop: element.offsetHeight
Chris@0 69 });
Chris@0 70 }
Chris@0 71 Effect.Appear(update,{duration:0.15});
Chris@0 72 };
Chris@0 73 this.options.onHide = this.options.onHide ||
Chris@0 74 function(element, update){ new Effect.Fade(update,{duration:0.15}) };
Chris@0 75
Chris@0 76 if(typeof(this.options.tokens) == 'string')
Chris@0 77 this.options.tokens = new Array(this.options.tokens);
Chris@0 78 // Force carriage returns as token delimiters anyway
Chris@0 79 if (!this.options.tokens.include('\n'))
Chris@0 80 this.options.tokens.push('\n');
Chris@0 81
Chris@0 82 this.observer = null;
Chris@0 83
Chris@0 84 this.element.setAttribute('autocomplete','off');
Chris@0 85
Chris@0 86 Element.hide(this.update);
Chris@0 87
Chris@0 88 Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
Chris@0 89 Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
Chris@0 90 },
Chris@0 91
Chris@0 92 show: function() {
Chris@0 93 if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
Chris@0 94 if(!this.iefix &&
Chris@0 95 (Prototype.Browser.IE) &&
Chris@0 96 (Element.getStyle(this.update, 'position')=='absolute')) {
Chris@0 97 new Insertion.After(this.update,
Chris@0 98 '<iframe id="' + this.update.id + '_iefix" '+
Chris@0 99 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
Chris@0 100 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
Chris@0 101 this.iefix = $(this.update.id+'_iefix');
Chris@0 102 }
Chris@0 103 if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
Chris@0 104 },
Chris@0 105
Chris@0 106 fixIEOverlapping: function() {
Chris@0 107 Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
Chris@0 108 this.iefix.style.zIndex = 1;
Chris@0 109 this.update.style.zIndex = 2;
Chris@0 110 Element.show(this.iefix);
Chris@0 111 },
Chris@0 112
Chris@0 113 hide: function() {
Chris@0 114 this.stopIndicator();
Chris@0 115 if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
Chris@0 116 if(this.iefix) Element.hide(this.iefix);
Chris@0 117 },
Chris@0 118
Chris@0 119 startIndicator: function() {
Chris@0 120 if(this.options.indicator) Element.show(this.options.indicator);
Chris@0 121 },
Chris@0 122
Chris@0 123 stopIndicator: function() {
Chris@0 124 if(this.options.indicator) Element.hide(this.options.indicator);
Chris@0 125 },
Chris@0 126
Chris@0 127 onKeyPress: function(event) {
Chris@0 128 if(this.active)
Chris@0 129 switch(event.keyCode) {
Chris@0 130 case Event.KEY_TAB:
Chris@0 131 case Event.KEY_RETURN:
Chris@0 132 this.selectEntry();
Chris@0 133 Event.stop(event);
Chris@0 134 case Event.KEY_ESC:
Chris@0 135 this.hide();
Chris@0 136 this.active = false;
Chris@0 137 Event.stop(event);
Chris@0 138 return;
Chris@0 139 case Event.KEY_LEFT:
Chris@0 140 case Event.KEY_RIGHT:
Chris@0 141 return;
Chris@0 142 case Event.KEY_UP:
Chris@0 143 this.markPrevious();
Chris@0 144 this.render();
Chris@0 145 Event.stop(event);
Chris@0 146 return;
Chris@0 147 case Event.KEY_DOWN:
Chris@0 148 this.markNext();
Chris@0 149 this.render();
Chris@0 150 Event.stop(event);
Chris@0 151 return;
Chris@0 152 }
Chris@0 153 else
Chris@0 154 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
Chris@0 155 (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
Chris@0 156
Chris@0 157 this.changed = true;
Chris@0 158 this.hasFocus = true;
Chris@0 159
Chris@0 160 if(this.observer) clearTimeout(this.observer);
Chris@0 161 this.observer =
Chris@0 162 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
Chris@0 163 },
Chris@0 164
Chris@0 165 activate: function() {
Chris@0 166 this.changed = false;
Chris@0 167 this.hasFocus = true;
Chris@0 168 this.getUpdatedChoices();
Chris@0 169 },
Chris@0 170
Chris@0 171 onHover: function(event) {
Chris@0 172 var element = Event.findElement(event, 'LI');
Chris@0 173 if(this.index != element.autocompleteIndex)
Chris@0 174 {
Chris@0 175 this.index = element.autocompleteIndex;
Chris@0 176 this.render();
Chris@0 177 }
Chris@0 178 Event.stop(event);
Chris@0 179 },
Chris@0 180
Chris@0 181 onClick: function(event) {
Chris@0 182 var element = Event.findElement(event, 'LI');
Chris@0 183 this.index = element.autocompleteIndex;
Chris@0 184 this.selectEntry();
Chris@0 185 this.hide();
Chris@0 186 },
Chris@0 187
Chris@0 188 onBlur: function(event) {
Chris@0 189 // needed to make click events working
Chris@0 190 setTimeout(this.hide.bind(this), 250);
Chris@0 191 this.hasFocus = false;
Chris@0 192 this.active = false;
Chris@0 193 },
Chris@0 194
Chris@0 195 render: function() {
Chris@0 196 if(this.entryCount > 0) {
Chris@0 197 for (var i = 0; i < this.entryCount; i++)
Chris@0 198 this.index==i ?
Chris@0 199 Element.addClassName(this.getEntry(i),"selected") :
Chris@0 200 Element.removeClassName(this.getEntry(i),"selected");
Chris@0 201 if(this.hasFocus) {
Chris@0 202 this.show();
Chris@0 203 this.active = true;
Chris@0 204 }
Chris@0 205 } else {
Chris@0 206 this.active = false;
Chris@0 207 this.hide();
Chris@0 208 }
Chris@0 209 },
Chris@0 210
Chris@0 211 markPrevious: function() {
Chris@0 212 if(this.index > 0) this.index--;
Chris@0 213 else this.index = this.entryCount-1;
Chris@0 214 this.getEntry(this.index).scrollIntoView(true);
Chris@0 215 },
Chris@0 216
Chris@0 217 markNext: function() {
Chris@0 218 if(this.index < this.entryCount-1) this.index++;
Chris@0 219 else this.index = 0;
Chris@0 220 this.getEntry(this.index).scrollIntoView(false);
Chris@0 221 },
Chris@0 222
Chris@0 223 getEntry: function(index) {
Chris@0 224 return this.update.firstChild.childNodes[index];
Chris@0 225 },
Chris@0 226
Chris@0 227 getCurrentEntry: function() {
Chris@0 228 return this.getEntry(this.index);
Chris@0 229 },
Chris@0 230
Chris@0 231 selectEntry: function() {
Chris@0 232 this.active = false;
Chris@0 233 this.updateElement(this.getCurrentEntry());
Chris@0 234 },
Chris@0 235
Chris@0 236 updateElement: function(selectedElement) {
Chris@0 237 if (this.options.updateElement) {
Chris@0 238 this.options.updateElement(selectedElement);
Chris@0 239 return;
Chris@0 240 }
Chris@0 241 var value = '';
Chris@0 242 if (this.options.select) {
Chris@0 243 var nodes = $(selectedElement).select('.' + this.options.select) || [];
Chris@0 244 if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
Chris@0 245 } else
Chris@0 246 value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
Chris@0 247
Chris@0 248 var bounds = this.getTokenBounds();
Chris@0 249 if (bounds[0] != -1) {
Chris@0 250 var newValue = this.element.value.substr(0, bounds[0]);
Chris@0 251 var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
Chris@0 252 if (whitespace)
Chris@0 253 newValue += whitespace[0];
Chris@0 254 this.element.value = newValue + value + this.element.value.substr(bounds[1]);
Chris@0 255 } else {
Chris@0 256 this.element.value = value;
Chris@0 257 }
Chris@0 258 this.oldElementValue = this.element.value;
Chris@0 259 this.element.focus();
Chris@0 260
Chris@0 261 if (this.options.afterUpdateElement)
Chris@0 262 this.options.afterUpdateElement(this.element, selectedElement);
Chris@0 263 },
Chris@0 264
Chris@0 265 updateChoices: function(choices) {
Chris@0 266 if(!this.changed && this.hasFocus) {
Chris@0 267 this.update.innerHTML = choices;
Chris@0 268 Element.cleanWhitespace(this.update);
Chris@0 269 Element.cleanWhitespace(this.update.down());
Chris@0 270
Chris@0 271 if(this.update.firstChild && this.update.down().childNodes) {
Chris@0 272 this.entryCount =
Chris@0 273 this.update.down().childNodes.length;
Chris@0 274 for (var i = 0; i < this.entryCount; i++) {
Chris@0 275 var entry = this.getEntry(i);
Chris@0 276 entry.autocompleteIndex = i;
Chris@0 277 this.addObservers(entry);
Chris@0 278 }
Chris@0 279 } else {
Chris@0 280 this.entryCount = 0;
Chris@0 281 }
Chris@0 282
Chris@0 283 this.stopIndicator();
Chris@0 284 this.index = 0;
Chris@0 285
Chris@0 286 if(this.entryCount==1 && this.options.autoSelect) {
Chris@0 287 this.selectEntry();
Chris@0 288 this.hide();
Chris@0 289 } else {
Chris@0 290 this.render();
Chris@0 291 }
Chris@0 292 }
Chris@0 293 },
Chris@0 294
Chris@0 295 addObservers: function(element) {
Chris@0 296 Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Chris@0 297 Event.observe(element, "click", this.onClick.bindAsEventListener(this));
Chris@0 298 },
Chris@0 299
Chris@0 300 onObserverEvent: function() {
Chris@0 301 this.changed = false;
Chris@0 302 this.tokenBounds = null;
Chris@0 303 if(this.getToken().length>=this.options.minChars) {
Chris@0 304 this.getUpdatedChoices();
Chris@0 305 } else {
Chris@0 306 this.active = false;
Chris@0 307 this.hide();
Chris@0 308 }
Chris@0 309 this.oldElementValue = this.element.value;
Chris@0 310 },
Chris@0 311
Chris@0 312 getToken: function() {
Chris@0 313 var bounds = this.getTokenBounds();
Chris@0 314 return this.element.value.substring(bounds[0], bounds[1]).strip();
Chris@0 315 },
Chris@0 316
Chris@0 317 getTokenBounds: function() {
Chris@0 318 if (null != this.tokenBounds) return this.tokenBounds;
Chris@0 319 var value = this.element.value;
Chris@0 320 if (value.strip().empty()) return [-1, 0];
Chris@0 321 var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
Chris@0 322 var offset = (diff == this.oldElementValue.length ? 1 : 0);
Chris@0 323 var prevTokenPos = -1, nextTokenPos = value.length;
Chris@0 324 var tp;
Chris@0 325 for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
Chris@0 326 tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
Chris@0 327 if (tp > prevTokenPos) prevTokenPos = tp;
Chris@0 328 tp = value.indexOf(this.options.tokens[index], diff + offset);
Chris@0 329 if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
Chris@0 330 }
Chris@0 331 return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
Chris@0 332 }
Chris@0 333 });
Chris@0 334
Chris@0 335 Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
Chris@0 336 var boundary = Math.min(newS.length, oldS.length);
Chris@0 337 for (var index = 0; index < boundary; ++index)
Chris@0 338 if (newS[index] != oldS[index])
Chris@0 339 return index;
Chris@0 340 return boundary;
Chris@0 341 };
Chris@0 342
Chris@0 343 Ajax.Autocompleter = Class.create(Autocompleter.Base, {
Chris@0 344 initialize: function(element, update, url, options) {
Chris@0 345 this.baseInitialize(element, update, options);
Chris@0 346 this.options.asynchronous = true;
Chris@0 347 this.options.onComplete = this.onComplete.bind(this);
Chris@0 348 this.options.defaultParams = this.options.parameters || null;
Chris@0 349 this.url = url;
Chris@0 350 },
Chris@0 351
Chris@0 352 getUpdatedChoices: function() {
Chris@0 353 this.startIndicator();
Chris@0 354
Chris@0 355 var entry = encodeURIComponent(this.options.paramName) + '=' +
Chris@0 356 encodeURIComponent(this.getToken());
Chris@0 357
Chris@0 358 this.options.parameters = this.options.callback ?
Chris@0 359 this.options.callback(this.element, entry) : entry;
Chris@0 360
Chris@0 361 if(this.options.defaultParams)
Chris@0 362 this.options.parameters += '&' + this.options.defaultParams;
Chris@0 363
Chris@0 364 new Ajax.Request(this.url, this.options);
Chris@0 365 },
Chris@0 366
Chris@0 367 onComplete: function(request) {
Chris@0 368 this.updateChoices(request.responseText);
Chris@0 369 }
Chris@0 370 });
Chris@0 371
Chris@0 372 // The local array autocompleter. Used when you'd prefer to
Chris@0 373 // inject an array of autocompletion options into the page, rather
Chris@0 374 // than sending out Ajax queries, which can be quite slow sometimes.
Chris@0 375 //
Chris@0 376 // The constructor takes four parameters. The first two are, as usual,
Chris@0 377 // the id of the monitored textbox, and id of the autocompletion menu.
Chris@0 378 // The third is the array you want to autocomplete from, and the fourth
Chris@0 379 // is the options block.
Chris@0 380 //
Chris@0 381 // Extra local autocompletion options:
Chris@0 382 // - choices - How many autocompletion choices to offer
Chris@0 383 //
Chris@0 384 // - partialSearch - If false, the autocompleter will match entered
Chris@0 385 // text only at the beginning of strings in the
Chris@0 386 // autocomplete array. Defaults to true, which will
Chris@0 387 // match text at the beginning of any *word* in the
Chris@0 388 // strings in the autocomplete array. If you want to
Chris@0 389 // search anywhere in the string, additionally set
Chris@0 390 // the option fullSearch to true (default: off).
Chris@0 391 //
Chris@0 392 // - fullSsearch - Search anywhere in autocomplete array strings.
Chris@0 393 //
Chris@0 394 // - partialChars - How many characters to enter before triggering
Chris@0 395 // a partial match (unlike minChars, which defines
Chris@0 396 // how many characters are required to do any match
Chris@0 397 // at all). Defaults to 2.
Chris@0 398 //
Chris@0 399 // - ignoreCase - Whether to ignore case when autocompleting.
Chris@0 400 // Defaults to true.
Chris@0 401 //
Chris@0 402 // It's possible to pass in a custom function as the 'selector'
Chris@0 403 // option, if you prefer to write your own autocompletion logic.
Chris@0 404 // In that case, the other options above will not apply unless
Chris@0 405 // you support them.
Chris@0 406
Chris@0 407 Autocompleter.Local = Class.create(Autocompleter.Base, {
Chris@0 408 initialize: function(element, update, array, options) {
Chris@0 409 this.baseInitialize(element, update, options);
Chris@0 410 this.options.array = array;
Chris@0 411 },
Chris@0 412
Chris@0 413 getUpdatedChoices: function() {
Chris@0 414 this.updateChoices(this.options.selector(this));
Chris@0 415 },
Chris@0 416
Chris@0 417 setOptions: function(options) {
Chris@0 418 this.options = Object.extend({
Chris@0 419 choices: 10,
Chris@0 420 partialSearch: true,
Chris@0 421 partialChars: 2,
Chris@0 422 ignoreCase: true,
Chris@0 423 fullSearch: false,
Chris@0 424 selector: function(instance) {
Chris@0 425 var ret = []; // Beginning matches
Chris@0 426 var partial = []; // Inside matches
Chris@0 427 var entry = instance.getToken();
Chris@0 428 var count = 0;
Chris@0 429
Chris@0 430 for (var i = 0; i < instance.options.array.length &&
Chris@0 431 ret.length < instance.options.choices ; i++) {
Chris@0 432
Chris@0 433 var elem = instance.options.array[i];
Chris@0 434 var foundPos = instance.options.ignoreCase ?
Chris@0 435 elem.toLowerCase().indexOf(entry.toLowerCase()) :
Chris@0 436 elem.indexOf(entry);
Chris@0 437
Chris@0 438 while (foundPos != -1) {
Chris@0 439 if (foundPos == 0 && elem.length != entry.length) {
Chris@0 440 ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
Chris@0 441 elem.substr(entry.length) + "</li>");
Chris@0 442 break;
Chris@0 443 } else if (entry.length >= instance.options.partialChars &&
Chris@0 444 instance.options.partialSearch && foundPos != -1) {
Chris@0 445 if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
Chris@0 446 partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
Chris@0 447 elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
Chris@0 448 foundPos + entry.length) + "</li>");
Chris@0 449 break;
Chris@0 450 }
Chris@0 451 }
Chris@0 452
Chris@0 453 foundPos = instance.options.ignoreCase ?
Chris@0 454 elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
Chris@0 455 elem.indexOf(entry, foundPos + 1);
Chris@0 456
Chris@0 457 }
Chris@0 458 }
Chris@0 459 if (partial.length)
Chris@0 460 ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
Chris@0 461 return "<ul>" + ret.join('') + "</ul>";
Chris@0 462 }
Chris@0 463 }, options || { });
Chris@0 464 }
Chris@0 465 });
Chris@0 466
Chris@0 467 // AJAX in-place editor and collection editor
Chris@0 468 // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
Chris@0 469
Chris@0 470 // Use this if you notice weird scrolling problems on some browsers,
Chris@0 471 // the DOM might be a bit confused when this gets called so do this
Chris@0 472 // waits 1 ms (with setTimeout) until it does the activation
Chris@0 473 Field.scrollFreeActivate = function(field) {
Chris@0 474 setTimeout(function() {
Chris@0 475 Field.activate(field);
Chris@0 476 }, 1);
Chris@0 477 };
Chris@0 478
Chris@0 479 Ajax.InPlaceEditor = Class.create({
Chris@0 480 initialize: function(element, url, options) {
Chris@0 481 this.url = url;
Chris@0 482 this.element = element = $(element);
Chris@0 483 this.prepareOptions();
Chris@0 484 this._controls = { };
Chris@0 485 arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
Chris@0 486 Object.extend(this.options, options || { });
Chris@0 487 if (!this.options.formId && this.element.id) {
Chris@0 488 this.options.formId = this.element.id + '-inplaceeditor';
Chris@0 489 if ($(this.options.formId))
Chris@0 490 this.options.formId = '';
Chris@0 491 }
Chris@0 492 if (this.options.externalControl)
Chris@0 493 this.options.externalControl = $(this.options.externalControl);
Chris@0 494 if (!this.options.externalControl)
Chris@0 495 this.options.externalControlOnly = false;
Chris@0 496 this._originalBackground = this.element.getStyle('background-color') || 'transparent';
Chris@0 497 this.element.title = this.options.clickToEditText;
Chris@0 498 this._boundCancelHandler = this.handleFormCancellation.bind(this);
Chris@0 499 this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
Chris@0 500 this._boundFailureHandler = this.handleAJAXFailure.bind(this);
Chris@0 501 this._boundSubmitHandler = this.handleFormSubmission.bind(this);
Chris@0 502 this._boundWrapperHandler = this.wrapUp.bind(this);
Chris@0 503 this.registerListeners();
Chris@0 504 },
Chris@0 505 checkForEscapeOrReturn: function(e) {
Chris@0 506 if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
Chris@0 507 if (Event.KEY_ESC == e.keyCode)
Chris@0 508 this.handleFormCancellation(e);
Chris@0 509 else if (Event.KEY_RETURN == e.keyCode)
Chris@0 510 this.handleFormSubmission(e);
Chris@0 511 },
Chris@0 512 createControl: function(mode, handler, extraClasses) {
Chris@0 513 var control = this.options[mode + 'Control'];
Chris@0 514 var text = this.options[mode + 'Text'];
Chris@0 515 if ('button' == control) {
Chris@0 516 var btn = document.createElement('input');
Chris@0 517 btn.type = 'submit';
Chris@0 518 btn.value = text;
Chris@0 519 btn.className = 'editor_' + mode + '_button';
Chris@0 520 if ('cancel' == mode)
Chris@0 521 btn.onclick = this._boundCancelHandler;
Chris@0 522 this._form.appendChild(btn);
Chris@0 523 this._controls[mode] = btn;
Chris@0 524 } else if ('link' == control) {
Chris@0 525 var link = document.createElement('a');
Chris@0 526 link.href = '#';
Chris@0 527 link.appendChild(document.createTextNode(text));
Chris@0 528 link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
Chris@0 529 link.className = 'editor_' + mode + '_link';
Chris@0 530 if (extraClasses)
Chris@0 531 link.className += ' ' + extraClasses;
Chris@0 532 this._form.appendChild(link);
Chris@0 533 this._controls[mode] = link;
Chris@0 534 }
Chris@0 535 },
Chris@0 536 createEditField: function() {
Chris@0 537 var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
Chris@0 538 var fld;
Chris@0 539 if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
Chris@0 540 fld = document.createElement('input');
Chris@0 541 fld.type = 'text';
Chris@0 542 var size = this.options.size || this.options.cols || 0;
Chris@0 543 if (0 < size) fld.size = size;
Chris@0 544 } else {
Chris@0 545 fld = document.createElement('textarea');
Chris@0 546 fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
Chris@0 547 fld.cols = this.options.cols || 40;
Chris@0 548 }
Chris@0 549 fld.name = this.options.paramName;
Chris@0 550 fld.value = text; // No HTML breaks conversion anymore
Chris@0 551 fld.className = 'editor_field';
Chris@0 552 if (this.options.submitOnBlur)
Chris@0 553 fld.onblur = this._boundSubmitHandler;
Chris@0 554 this._controls.editor = fld;
Chris@0 555 if (this.options.loadTextURL)
Chris@0 556 this.loadExternalText();
Chris@0 557 this._form.appendChild(this._controls.editor);
Chris@0 558 },
Chris@0 559 createForm: function() {
Chris@0 560 var ipe = this;
Chris@0 561 function addText(mode, condition) {
Chris@0 562 var text = ipe.options['text' + mode + 'Controls'];
Chris@0 563 if (!text || condition === false) return;
Chris@0 564 ipe._form.appendChild(document.createTextNode(text));
Chris@0 565 };
Chris@0 566 this._form = $(document.createElement('form'));
Chris@0 567 this._form.id = this.options.formId;
Chris@0 568 this._form.addClassName(this.options.formClassName);
Chris@0 569 this._form.onsubmit = this._boundSubmitHandler;
Chris@0 570 this.createEditField();
Chris@0 571 if ('textarea' == this._controls.editor.tagName.toLowerCase())
Chris@0 572 this._form.appendChild(document.createElement('br'));
Chris@0 573 if (this.options.onFormCustomization)
Chris@0 574 this.options.onFormCustomization(this, this._form);
Chris@0 575 addText('Before', this.options.okControl || this.options.cancelControl);
Chris@0 576 this.createControl('ok', this._boundSubmitHandler);
Chris@0 577 addText('Between', this.options.okControl && this.options.cancelControl);
Chris@0 578 this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
Chris@0 579 addText('After', this.options.okControl || this.options.cancelControl);
Chris@0 580 },
Chris@0 581 destroy: function() {
Chris@0 582 if (this._oldInnerHTML)
Chris@0 583 this.element.innerHTML = this._oldInnerHTML;
Chris@0 584 this.leaveEditMode();
Chris@0 585 this.unregisterListeners();
Chris@0 586 },
Chris@0 587 enterEditMode: function(e) {
Chris@0 588 if (this._saving || this._editing) return;
Chris@0 589 this._editing = true;
Chris@0 590 this.triggerCallback('onEnterEditMode');
Chris@0 591 if (this.options.externalControl)
Chris@0 592 this.options.externalControl.hide();
Chris@0 593 this.element.hide();
Chris@0 594 this.createForm();
Chris@0 595 this.element.parentNode.insertBefore(this._form, this.element);
Chris@0 596 if (!this.options.loadTextURL)
Chris@0 597 this.postProcessEditField();
Chris@0 598 if (e) Event.stop(e);
Chris@0 599 },
Chris@0 600 enterHover: function(e) {
Chris@0 601 if (this.options.hoverClassName)
Chris@0 602 this.element.addClassName(this.options.hoverClassName);
Chris@0 603 if (this._saving) return;
Chris@0 604 this.triggerCallback('onEnterHover');
Chris@0 605 },
Chris@0 606 getText: function() {
Chris@0 607 return this.element.innerHTML.unescapeHTML();
Chris@0 608 },
Chris@0 609 handleAJAXFailure: function(transport) {
Chris@0 610 this.triggerCallback('onFailure', transport);
Chris@0 611 if (this._oldInnerHTML) {
Chris@0 612 this.element.innerHTML = this._oldInnerHTML;
Chris@0 613 this._oldInnerHTML = null;
Chris@0 614 }
Chris@0 615 },
Chris@0 616 handleFormCancellation: function(e) {
Chris@0 617 this.wrapUp();
Chris@0 618 if (e) Event.stop(e);
Chris@0 619 },
Chris@0 620 handleFormSubmission: function(e) {
Chris@0 621 var form = this._form;
Chris@0 622 var value = $F(this._controls.editor);
Chris@0 623 this.prepareSubmission();
Chris@0 624 var params = this.options.callback(form, value) || '';
Chris@0 625 if (Object.isString(params))
Chris@0 626 params = params.toQueryParams();
Chris@0 627 params.editorId = this.element.id;
Chris@0 628 if (this.options.htmlResponse) {
Chris@0 629 var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
Chris@0 630 Object.extend(options, {
Chris@0 631 parameters: params,
Chris@0 632 onComplete: this._boundWrapperHandler,
Chris@0 633 onFailure: this._boundFailureHandler
Chris@0 634 });
Chris@0 635 new Ajax.Updater({ success: this.element }, this.url, options);
Chris@0 636 } else {
Chris@0 637 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Chris@0 638 Object.extend(options, {
Chris@0 639 parameters: params,
Chris@0 640 onComplete: this._boundWrapperHandler,
Chris@0 641 onFailure: this._boundFailureHandler
Chris@0 642 });
Chris@0 643 new Ajax.Request(this.url, options);
Chris@0 644 }
Chris@0 645 if (e) Event.stop(e);
Chris@0 646 },
Chris@0 647 leaveEditMode: function() {
Chris@0 648 this.element.removeClassName(this.options.savingClassName);
Chris@0 649 this.removeForm();
Chris@0 650 this.leaveHover();
Chris@0 651 this.element.style.backgroundColor = this._originalBackground;
Chris@0 652 this.element.show();
Chris@0 653 if (this.options.externalControl)
Chris@0 654 this.options.externalControl.show();
Chris@0 655 this._saving = false;
Chris@0 656 this._editing = false;
Chris@0 657 this._oldInnerHTML = null;
Chris@0 658 this.triggerCallback('onLeaveEditMode');
Chris@0 659 },
Chris@0 660 leaveHover: function(e) {
Chris@0 661 if (this.options.hoverClassName)
Chris@0 662 this.element.removeClassName(this.options.hoverClassName);
Chris@0 663 if (this._saving) return;
Chris@0 664 this.triggerCallback('onLeaveHover');
Chris@0 665 },
Chris@0 666 loadExternalText: function() {
Chris@0 667 this._form.addClassName(this.options.loadingClassName);
Chris@0 668 this._controls.editor.disabled = true;
Chris@0 669 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Chris@0 670 Object.extend(options, {
Chris@0 671 parameters: 'editorId=' + encodeURIComponent(this.element.id),
Chris@0 672 onComplete: Prototype.emptyFunction,
Chris@0 673 onSuccess: function(transport) {
Chris@0 674 this._form.removeClassName(this.options.loadingClassName);
Chris@0 675 var text = transport.responseText;
Chris@0 676 if (this.options.stripLoadedTextTags)
Chris@0 677 text = text.stripTags();
Chris@0 678 this._controls.editor.value = text;
Chris@0 679 this._controls.editor.disabled = false;
Chris@0 680 this.postProcessEditField();
Chris@0 681 }.bind(this),
Chris@0 682 onFailure: this._boundFailureHandler
Chris@0 683 });
Chris@0 684 new Ajax.Request(this.options.loadTextURL, options);
Chris@0 685 },
Chris@0 686 postProcessEditField: function() {
Chris@0 687 var fpc = this.options.fieldPostCreation;
Chris@0 688 if (fpc)
Chris@0 689 $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
Chris@0 690 },
Chris@0 691 prepareOptions: function() {
Chris@0 692 this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
Chris@0 693 Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
Chris@0 694 [this._extraDefaultOptions].flatten().compact().each(function(defs) {
Chris@0 695 Object.extend(this.options, defs);
Chris@0 696 }.bind(this));
Chris@0 697 },
Chris@0 698 prepareSubmission: function() {
Chris@0 699 this._saving = true;
Chris@0 700 this.removeForm();
Chris@0 701 this.leaveHover();
Chris@0 702 this.showSaving();
Chris@0 703 },
Chris@0 704 registerListeners: function() {
Chris@0 705 this._listeners = { };
Chris@0 706 var listener;
Chris@0 707 $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
Chris@0 708 listener = this[pair.value].bind(this);
Chris@0 709 this._listeners[pair.key] = listener;
Chris@0 710 if (!this.options.externalControlOnly)
Chris@0 711 this.element.observe(pair.key, listener);
Chris@0 712 if (this.options.externalControl)
Chris@0 713 this.options.externalControl.observe(pair.key, listener);
Chris@0 714 }.bind(this));
Chris@0 715 },
Chris@0 716 removeForm: function() {
Chris@0 717 if (!this._form) return;
Chris@0 718 this._form.remove();
Chris@0 719 this._form = null;
Chris@0 720 this._controls = { };
Chris@0 721 },
Chris@0 722 showSaving: function() {
Chris@0 723 this._oldInnerHTML = this.element.innerHTML;
Chris@0 724 this.element.innerHTML = this.options.savingText;
Chris@0 725 this.element.addClassName(this.options.savingClassName);
Chris@0 726 this.element.style.backgroundColor = this._originalBackground;
Chris@0 727 this.element.show();
Chris@0 728 },
Chris@0 729 triggerCallback: function(cbName, arg) {
Chris@0 730 if ('function' == typeof this.options[cbName]) {
Chris@0 731 this.options[cbName](this, arg);
Chris@0 732 }
Chris@0 733 },
Chris@0 734 unregisterListeners: function() {
Chris@0 735 $H(this._listeners).each(function(pair) {
Chris@0 736 if (!this.options.externalControlOnly)
Chris@0 737 this.element.stopObserving(pair.key, pair.value);
Chris@0 738 if (this.options.externalControl)
Chris@0 739 this.options.externalControl.stopObserving(pair.key, pair.value);
Chris@0 740 }.bind(this));
Chris@0 741 },
Chris@0 742 wrapUp: function(transport) {
Chris@0 743 this.leaveEditMode();
Chris@0 744 // Can't use triggerCallback due to backward compatibility: requires
Chris@0 745 // binding + direct element
Chris@0 746 this._boundComplete(transport, this.element);
Chris@0 747 }
Chris@0 748 });
Chris@0 749
Chris@0 750 Object.extend(Ajax.InPlaceEditor.prototype, {
Chris@0 751 dispose: Ajax.InPlaceEditor.prototype.destroy
Chris@0 752 });
Chris@0 753
Chris@0 754 Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
Chris@0 755 initialize: function($super, element, url, options) {
Chris@0 756 this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
Chris@0 757 $super(element, url, options);
Chris@0 758 },
Chris@0 759
Chris@0 760 createEditField: function() {
Chris@0 761 var list = document.createElement('select');
Chris@0 762 list.name = this.options.paramName;
Chris@0 763 list.size = 1;
Chris@0 764 this._controls.editor = list;
Chris@0 765 this._collection = this.options.collection || [];
Chris@0 766 if (this.options.loadCollectionURL)
Chris@0 767 this.loadCollection();
Chris@0 768 else
Chris@0 769 this.checkForExternalText();
Chris@0 770 this._form.appendChild(this._controls.editor);
Chris@0 771 },
Chris@0 772
Chris@0 773 loadCollection: function() {
Chris@0 774 this._form.addClassName(this.options.loadingClassName);
Chris@0 775 this.showLoadingText(this.options.loadingCollectionText);
Chris@0 776 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Chris@0 777 Object.extend(options, {
Chris@0 778 parameters: 'editorId=' + encodeURIComponent(this.element.id),
Chris@0 779 onComplete: Prototype.emptyFunction,
Chris@0 780 onSuccess: function(transport) {
Chris@0 781 var js = transport.responseText.strip();
Chris@0 782 if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
Chris@0 783 throw('Server returned an invalid collection representation.');
Chris@0 784 this._collection = eval(js);
Chris@0 785 this.checkForExternalText();
Chris@0 786 }.bind(this),
Chris@0 787 onFailure: this.onFailure
Chris@0 788 });
Chris@0 789 new Ajax.Request(this.options.loadCollectionURL, options);
Chris@0 790 },
Chris@0 791
Chris@0 792 showLoadingText: function(text) {
Chris@0 793 this._controls.editor.disabled = true;
Chris@0 794 var tempOption = this._controls.editor.firstChild;
Chris@0 795 if (!tempOption) {
Chris@0 796 tempOption = document.createElement('option');
Chris@0 797 tempOption.value = '';
Chris@0 798 this._controls.editor.appendChild(tempOption);
Chris@0 799 tempOption.selected = true;
Chris@0 800 }
Chris@0 801 tempOption.update((text || '').stripScripts().stripTags());
Chris@0 802 },
Chris@0 803
Chris@0 804 checkForExternalText: function() {
Chris@0 805 this._text = this.getText();
Chris@0 806 if (this.options.loadTextURL)
Chris@0 807 this.loadExternalText();
Chris@0 808 else
Chris@0 809 this.buildOptionList();
Chris@0 810 },
Chris@0 811
Chris@0 812 loadExternalText: function() {
Chris@0 813 this.showLoadingText(this.options.loadingText);
Chris@0 814 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Chris@0 815 Object.extend(options, {
Chris@0 816 parameters: 'editorId=' + encodeURIComponent(this.element.id),
Chris@0 817 onComplete: Prototype.emptyFunction,
Chris@0 818 onSuccess: function(transport) {
Chris@0 819 this._text = transport.responseText.strip();
Chris@0 820 this.buildOptionList();
Chris@0 821 }.bind(this),
Chris@0 822 onFailure: this.onFailure
Chris@0 823 });
Chris@0 824 new Ajax.Request(this.options.loadTextURL, options);
Chris@0 825 },
Chris@0 826
Chris@0 827 buildOptionList: function() {
Chris@0 828 this._form.removeClassName(this.options.loadingClassName);
Chris@0 829 this._collection = this._collection.map(function(entry) {
Chris@0 830 return 2 === entry.length ? entry : [entry, entry].flatten();
Chris@0 831 });
Chris@0 832 var marker = ('value' in this.options) ? this.options.value : this._text;
Chris@0 833 var textFound = this._collection.any(function(entry) {
Chris@0 834 return entry[0] == marker;
Chris@0 835 }.bind(this));
Chris@0 836 this._controls.editor.update('');
Chris@0 837 var option;
Chris@0 838 this._collection.each(function(entry, index) {
Chris@0 839 option = document.createElement('option');
Chris@0 840 option.value = entry[0];
Chris@0 841 option.selected = textFound ? entry[0] == marker : 0 == index;
Chris@0 842 option.appendChild(document.createTextNode(entry[1]));
Chris@0 843 this._controls.editor.appendChild(option);
Chris@0 844 }.bind(this));
Chris@0 845 this._controls.editor.disabled = false;
Chris@0 846 Field.scrollFreeActivate(this._controls.editor);
Chris@0 847 }
Chris@0 848 });
Chris@0 849
Chris@0 850 //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
Chris@0 851 //**** This only exists for a while, in order to let ****
Chris@0 852 //**** users adapt to the new API. Read up on the new ****
Chris@0 853 //**** API and convert your code to it ASAP! ****
Chris@0 854
Chris@0 855 Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
Chris@0 856 if (!options) return;
Chris@0 857 function fallback(name, expr) {
Chris@0 858 if (name in options || expr === undefined) return;
Chris@0 859 options[name] = expr;
Chris@0 860 };
Chris@0 861 fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
Chris@0 862 options.cancelLink == options.cancelButton == false ? false : undefined)));
Chris@0 863 fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
Chris@0 864 options.okLink == options.okButton == false ? false : undefined)));
Chris@0 865 fallback('highlightColor', options.highlightcolor);
Chris@0 866 fallback('highlightEndColor', options.highlightendcolor);
Chris@0 867 };
Chris@0 868
Chris@0 869 Object.extend(Ajax.InPlaceEditor, {
Chris@0 870 DefaultOptions: {
Chris@0 871 ajaxOptions: { },
Chris@0 872 autoRows: 3, // Use when multi-line w/ rows == 1
Chris@0 873 cancelControl: 'link', // 'link'|'button'|false
Chris@0 874 cancelText: 'cancel',
Chris@0 875 clickToEditText: 'Click to edit',
Chris@0 876 externalControl: null, // id|elt
Chris@0 877 externalControlOnly: false,
Chris@0 878 fieldPostCreation: 'activate', // 'activate'|'focus'|false
Chris@0 879 formClassName: 'inplaceeditor-form',
Chris@0 880 formId: null, // id|elt
Chris@0 881 highlightColor: '#ffff99',
Chris@0 882 highlightEndColor: '#ffffff',
Chris@0 883 hoverClassName: '',
Chris@0 884 htmlResponse: true,
Chris@0 885 loadingClassName: 'inplaceeditor-loading',
Chris@0 886 loadingText: 'Loading...',
Chris@0 887 okControl: 'button', // 'link'|'button'|false
Chris@0 888 okText: 'ok',
Chris@0 889 paramName: 'value',
Chris@0 890 rows: 1, // If 1 and multi-line, uses autoRows
Chris@0 891 savingClassName: 'inplaceeditor-saving',
Chris@0 892 savingText: 'Saving...',
Chris@0 893 size: 0,
Chris@0 894 stripLoadedTextTags: false,
Chris@0 895 submitOnBlur: false,
Chris@0 896 textAfterControls: '',
Chris@0 897 textBeforeControls: '',
Chris@0 898 textBetweenControls: ''
Chris@0 899 },
Chris@0 900 DefaultCallbacks: {
Chris@0 901 callback: function(form) {
Chris@0 902 return Form.serialize(form);
Chris@0 903 },
Chris@0 904 onComplete: function(transport, element) {
Chris@0 905 // For backward compatibility, this one is bound to the IPE, and passes
Chris@0 906 // the element directly. It was too often customized, so we don't break it.
Chris@0 907 new Effect.Highlight(element, {
Chris@0 908 startcolor: this.options.highlightColor, keepBackgroundImage: true });
Chris@0 909 },
Chris@0 910 onEnterEditMode: null,
Chris@0 911 onEnterHover: function(ipe) {
Chris@0 912 ipe.element.style.backgroundColor = ipe.options.highlightColor;
Chris@0 913 if (ipe._effect)
Chris@0 914 ipe._effect.cancel();
Chris@0 915 },
Chris@0 916 onFailure: function(transport, ipe) {
Chris@0 917 alert('Error communication with the server: ' + transport.responseText.stripTags());
Chris@0 918 },
Chris@0 919 onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
Chris@0 920 onLeaveEditMode: null,
Chris@0 921 onLeaveHover: function(ipe) {
Chris@0 922 ipe._effect = new Effect.Highlight(ipe.element, {
Chris@0 923 startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
Chris@0 924 restorecolor: ipe._originalBackground, keepBackgroundImage: true
Chris@0 925 });
Chris@0 926 }
Chris@0 927 },
Chris@0 928 Listeners: {
Chris@0 929 click: 'enterEditMode',
Chris@0 930 keydown: 'checkForEscapeOrReturn',
Chris@0 931 mouseover: 'enterHover',
Chris@0 932 mouseout: 'leaveHover'
Chris@0 933 }
Chris@0 934 });
Chris@0 935
Chris@0 936 Ajax.InPlaceCollectionEditor.DefaultOptions = {
Chris@0 937 loadingCollectionText: 'Loading options...'
Chris@0 938 };
Chris@0 939
Chris@0 940 // Delayed observer, like Form.Element.Observer,
Chris@0 941 // but waits for delay after last key input
Chris@0 942 // Ideal for live-search fields
Chris@0 943
Chris@0 944 Form.Element.DelayedObserver = Class.create({
Chris@0 945 initialize: function(element, delay, callback) {
Chris@0 946 this.delay = delay || 0.5;
Chris@0 947 this.element = $(element);
Chris@0 948 this.callback = callback;
Chris@0 949 this.timer = null;
Chris@0 950 this.lastValue = $F(this.element);
Chris@0 951 Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
Chris@0 952 },
Chris@0 953 delayedListener: function(event) {
Chris@0 954 if(this.lastValue == $F(this.element)) return;
Chris@0 955 if(this.timer) clearTimeout(this.timer);
Chris@0 956 this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
Chris@0 957 this.lastValue = $F(this.element);
Chris@0 958 },
Chris@0 959 onTimerEvent: function() {
Chris@0 960 this.timer = null;
Chris@0 961 this.callback(this.element, $F(this.element));
Chris@0 962 }
Chris@0 963 });