tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Controls.js (47788B)


      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 };