/*
Core JavaScript Library
$Id: core.js 232 2007-10-01 20:32:42Z whitaker $

Copyright (c) 2005, Six Apart, Ltd.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.

    * Neither the name of "Six Apart" nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

/* stubs */

log = function() {};
log.error = log.warn = log.debug = log;


/* utility functions */

defined = function( x ) {
    return x === undefined ? false : true;
}


/**
 * Utility method.
 * @param x <code>any</code> Any JavaScript value, including <code>undefined</code>.
 * @return boolean <code>true</code> if the value is not <code>null</code> and is not <code>undefined</code>.
 */
exists = function( x ) {
   return (x === undefined || x === null) ? false : true;
}


finite = function( x ) {
    return isFinite( x ) ? x : 0;
}


finiteInt = function( x, base ) {
    return finite( parseInt( x, base ) );
}


finiteFloat = function( x ) {
    return finite( parseFloat( x ) );
}


max = function() {
    var a = arguments;
    var n = a[ 0 ];
    for( var i = 1; i < a.length; i++ )
        if( a[ i ] > n )
            n = a[ i ];
    return n;
}


min = function() {
    var a = arguments;
    var n = a[ 0 ];
    for( var i = 1; i < a.length; i++ )
        if( a[ i ] < n )
            n = a[ i ];
    return n;
}


/* try block */  
 
Try = {
    these: function() {
        for( var i = 0; i < arguments.length; i++ ) {
            try {
                return arguments[ i ]();
            } catch( e ) {}
        }
        return undefined;
    }
}


/* unique id generator */

Unique = {
    length: 0,
    
    id: function() {
        return ++this.length;
    }
}


/* event methods */

if( !defined( window.Event ) )
    Event = {};


Event.stop = function( event ) {
    event = event || this;
    if( event === Event )
        event = window.event;

    // w3c
    if( event.preventDefault )
        event.preventDefault();
    if( event.stopPropagation )
        event.stopPropagation();

    // ie
    try {
        event.cancelBubble = true;
        event.returnValue = false;
    } catch( e ) {}

    return false;
}


Event.prep = function( event ) {
    event = event || window.event;
    if( !defined( event.stop ) )
        event.stop = this.stop;
    if( !defined( event.target ) )
        event.target = event.srcElement;
    if( !defined( event.relatedTarget ) ) 
        event.relatedTarget = event.toElement;
    return event;
}


try { Event.prototype.stop = Event.stop; }
catch( e ) {}


/* object extensions */

Function.stub = function() {};


if( !Object.prototype.hasOwnProperty ) {
    Object.prototype.hasOwnProperty = function( p ) {
        if( !(p in this) )
            return false;
        try {
            var pr = this.constructor.prototype;
            while( pr ) {
                if( pr[ p ] === this[ p ] )
                    return false;
                if( pr === pr.constructor.prototype )
                    break;
                pr = pr.constructor.prototype;
            }
        } catch( e ) {}
        return true;
    }
}


Object.prototype.extend = function() {
    var a = arguments;
    for( var i = 0; i < a.length; i++ ) {
        var o = a[ i ];
        for( var p in o ) {
            try {
                if( !this[ p ] &&
                    (!o.hasOwnProperty || o.hasOwnProperty( p )) )
                    this[ p ] = o[ p ];
            } catch( e ) {}
        }
    }
    return this;
}


Object.prototype.override = function() {
    var a = arguments;
    for( var i = 0; i < a.length; i++ ) {
        var o = a[ i ];
        for( var p in o ) {
            try {
                if( !o.hasOwnProperty || o.hasOwnProperty( p ) )
                    this[ p ] = o[ p ];
            } catch( e ) {}
        }
    }
    return this;
}


Object.prototype.extend( {
    init: Function.stub,
    destroy: Function.stub
} );



/* function extensions */

Function.prototype.extend( {
    bind: function( object ) {
        var method = this;
        return function() {
            return method.apply( object, arguments );
        };
    },
    
    
    bindEventListener: function( object ) {
        var method = this; // Use double closure to work around IE 6 memory leak.
        return function( event ) {
            try {
                event = Event.prep( event );
            } catch( e ) {}
            return method.call( object, event );
        };
    }
} );


/* class helpers */

indirectObjects = [];


Class = function( superClass ) {

    // Set the constructor:
    var constructor = function() {
        if( arguments.length )
            this.init.apply( this, arguments );
    };    
    //   -- Accomplish static-inheritance:
    constructor.override( Class );  // inherit static methods from Class
    superClass = superClass || Object; 
    constructor.override( superClass ); // inherit static methods from the superClass 
    constructor.superClass = superClass.prototype;
    
    // Set the constructor's prototype (accomplish object-inheritance):
    constructor.prototype = new superClass();
    constructor.prototype.constructor = constructor; // rev. 0.7    
    //   -- extend prototype with Class instance methods
    constructor.prototype.extend( Class.prototype );    
    //   -- override prototype with interface methods
    for( var i = 1; i < arguments.length; i++ )
        constructor.prototype.override( arguments[ i ] );
    
    return constructor;
}


Class.extend( {
    initSingleton: function() {
        if( this.singleton )
            return this.singleton;
        this.singleton = this.singletonConstructor
            ? new this.singletonConstructor()
            : new this();
        this.singleton.init.apply( this.singleton, arguments );
        return this.singleton;
    }
} );


Class.prototype = {
    destroy: function() {
        try {
            if( this.indirectIndex )
                indirectObjects[ this.indirectIndex ] = undefined;
            delete this.indirectIndex;
        } catch( e ) {}
        
        for( var property in this ) {
            try {
                if( this.hasOwnProperty( property ) )
                    delete this[ property ];
            } catch( e ) {}
        }
    },
    
    
    getBoundMethod: function( methodName ) {
        return this[ name ].bind( this );
    },
    
    
    getEventListener: function( methodName ) {
        return this[ methodName ].bindEventListener( this );
    },
    
    
    getIndirectIndex: function() {
        if( !defined( this.indirectIndex ) ) {
            this.indirectIndex = indirectObjects.length;
            indirectObjects.push( this );
        }
        return this.indirectIndex;
    },
    
    
    getIndirectMethod: function( methodName ) {
        if( !this.indirectMethods )
            this.indirectMethods = {};
        var method = this[ methodName ];
        if( typeof method != "function" )
            return undefined;
        var indirectIndex = this.getIndirectIndex();
        if( !this.indirectMethods[ methodName ] ) {
            this.indirectMethods[ methodName ] = new Function(
                "var o = indirectObjects[" + indirectIndex + "];" +
                "return o." + methodName + ".apply( o, arguments );"
            );
        }
        return this.indirectMethods[ methodName ];
    },
    
    
    getIndirectEventListener: function( methodName ) {
        if( !this.indirectEventListeners )
            this.indirectEventListeners = {};
        var method = this[ methodName ];
        if( typeof method != "function" )
            return undefined;
        var indirectIndex = this.getIndirectIndex();
        if( !this.indirectEventListeners[ methodName ] ) {
            this.indirectEventListeners[ methodName ] = new Function( "event",
                "try { event = Event.prep( event ); } catch( e ) {}" +
                "var o = indirectObjects[" + indirectIndex + "];" +
                "return o." + methodName + ".call( o, event );"
            );
        }
        return this.indirectEventListeners[ methodName ];
    }
}


/* string extensions */

String.extend( {
    escapeJSChar: function( c ) {
        // try simple escaping
        switch( c ) {
            case "\\": return "\\\\";
            case "\"": return "\\\"";
            case "'":  return "\\'";
            case "\b": return "\\b";
            case "\f": return "\\f";
            case "\n": return "\\n";
            case "\r": return "\\r";
            case "\t": return "\\t";
        }
        
        // return raw bytes now ... should be UTF-8
        if( c >= " " )
            return c;
        
        // try \uXXXX escaping, but shouldn't make it for case 1, 2
        c = c.charCodeAt( 0 ).toString( 16 );
        switch( c.length ) {
            case 1: return "\\u000" + c;
            case 2: return "\\u00" + c;
            case 3: return "\\u0" + c;
            case 4: return "\\u" + c;
        }
        
        // should never make it here
        return "";
    },
    
    
    encodeEntity: function( c ) {
        switch( c ) {
            case "<": return "&lt;";
            case ">": return "&gt;";
            case "&": return "&amp;";
            case '"': return "&quot;";
            case "'": return "&apos;";
        }
        return c;
    },


    decodeEntity: function( c ) {
        switch( c ) {
            case "amp": return "&";
            case "quot": return '"';
            case "gt": return ">";
            case "lt": return "<";
        }
        var m = c.match( /^#(\d+)$/ );
        if( m && defined( m[ 1 ] ) )
            return String.fromCharCode( m[ 1 ] );
        m = c.match( /^#x([0-9a-f]+)$/i );
        if(  m && defined( m[ 1 ] ) )
            return String.fromCharCode( parseInt( hex, m[ 1 ] ) );
        return c;
    }
} );


String.prototype.extend( {
    escapeJS: function() {
        return this.replace( /([^ -!#-\[\]-~])/g, function( m, c ) { return String.escapeJSChar( c ); } )
    },
    
    
    escapeJS2: function() {
        return this.replace( /([\u0000-\u0031'"\\])/g, function( m, c ) { return String.escapeJSChar( c ); } )
    },
    
    
    escapeJS3: function() {
        return this.replace( /[\u0000-\u0031'"\\]/g, function( m ) { return String.escapeJSChar( m ); } )
    },
    
    
    escapeJS4: function() {
        return this.replace( /./g, function( m ) { return String.escapeJSChar( m ); } )
    },
    
    
    encodeHTML: function() {
        return this.replace( /([<>&"])/g, function( m, c ) { return String.encodeEntity( c ) } );
    },


    decodeHTML: function() {
        return this.replace( /&(.*?);/g, function( m, c ) { return String.decodeEntity( c ) } );
    },
    
    
    cssToJS: function() {
        return this.replace( /-([a-z])/g, function( m, c ) { return c.toUpperCase() } );
    },
    
    
    jsToCSS: function() {
        return this.replace( /([A-Z])/g, function( m, c ) { return "-" + c.toLowerCase() } );
    },
    
    
    firstToLowerCase: function() {
        return this.replace( /^(.)/, function( m, c ) { return c.toLowerCase() } );
    },
    
        
    rgbToHex: function() {
        var c = this.match( /(\d+)\D+(\d+)\D+(\d+)/ );
        if( !c )
            return undefined;
        return "#" +
            finiteInt( c[ 1 ] ).toString( 16 ).pad( 2, "0" ) +
            finiteInt( c[ 2 ] ).toString( 16 ).pad( 2, "0" ) +
            finiteInt( c[ 3 ] ).toString( 16 ).pad( 2, "0" );
    },
    
    
    pad: function( length, padChar ) {
        var padding = length - this.length;
        if( padding <= 0 )
            return this;
        if( !defined( padChar ) )
            padChar = " ";
        var out = [];
        for( var i = 0; i < padding; i++ )
            out.push( padChar );
        out.push( this );
        return out.join( "" );
    },


    trim: function() {
        return this.replace( /^\s+|\s+$/g, "" );
    }

} );


/* extend array object */

Array.extend( { 
    fromPseudo: function ( args ) {
        var out = [];
        for ( var i = 0; i < args.length; i++ )
            out.push( args[ i ] );
        return out;
    }
});


/* extend array object */

Array.prototype.extend( {
    copy: function() {
        var out = [];
        for( var i = 0; i < this.length; i++ )
            out[ i ] = this[ i ];
        return out;
    },


    first: function( callback, object ) {
        var length = this.length;
        for( var i = 0; i < length; i++ ) {
            var result = object
                ? callback.call( object, this[ i ], i, this )
                : callback( this[ i ], i, this );
            if( result )
                return this[ i ];
        }
        return null;
    },


    fitIndex: function( fromIndex, defaultIndex ) {
        if( !defined( fromIndex ) || fromIndex == null )
            fromIndex = defaultIndex;
        else if( fromIndex < 0 ) {
            fromIndex = this.length + fromIndex;
            if( fromIndex < 0 )
                fromIndex = 0;
        } else if( fromIndex >= this.length )
            fromIndex = this.length - 1;
        return fromIndex;
    },


    scramble: function() {
        for( var i = 0; i < this.length; i++ ) {
            var j = Math.floor( Math.random() * this.length );
            var temp = this[ i ];
            this[ i ] = this[ j ];
            this[ j ] = temp;
        }
    },
    
    
    add: function() {
        var a = arguments;
        for( var i = 0; i < a.length; i++ ) {
            var index = this.indexOf( a[ i ] );
            if( index < 0 ) 
                this.push( arguments[ i ] );
        }
        return this.length;
    },
        
    
    remove: function() {
        var a = arguments;
        for( var i = 0; i < a.length; i++ ) {
            var j = this.indexOf( a[ i ] );
            if( j >= 0 )
                this.splice( j, 1 );
        }
        return this.length;
    },


    /* javascript 1.5 array methods */
    /* http://developer-test.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array#Methods */

    every: function( callback, object ) {
        var length = this.length;
        for( var i = 0; i < length; i++ ) {
            var result = object
                ? callback.call( object, this[ i ], i, this )
                : callback( this[ i ], i, this );
            if( !result )
                return false;
        }
        return true;
    },


    filter: function( callback, object ) {
        var out = [];
        var length = this.length;
        for( var i = 0; i < length; i++ ) {
            var result = object
                ? callback.call( object, this[ i ], i, this )
                : callback( this[ i ], i, this );
            if( result )
                out.push( this[ i ] );
        }
        return out;
    },
    
    
    forEach: function( callback, object ) {
        var length = this.length;
        for( var i = 0; i < length; i++ ) {
            object
                ? callback.call( object, this[ i ], i, this )
                : callback( this[ i ], i, this );
        }
    },
    
    
    indexOf: function( value, fromIndex ) {
        fromIndex = this.fitIndex( fromIndex, 0 );
        for( var i = 0; i < this.length; i++ ) {
            if( this[ i ] === value )
                return i; 
        }
        return -1;
    },


    lastIndexOf: function( value, fromIndex ) {
        fromIndex = this.fitIndex( fromIndex, this.length - 1 );
        for( var i = fromIndex; i >= 0; i-- ) {
            if( this[ i ] == value )
                return i;
        }
        return -1;
    },


    some: function( callback, object ) {
        var length = this.length;
        for( var i = 0; i < length; i++ ) {
            var result = object
                ? callback.call( object, this[ i ], i, this )
                : callback( this[ i ], i, this );
            if( result )
                return true;
        }
        return false;
    },


    /* javascript 1.2 array methods */

    concat: function() {
        var a = arguments;
        var out = this.copy();
        for( i = 0; i < a.length; i++ ) {
            var b = a[ i ];
            for( j = 0; j < b.length; j++ )
                out.push( b[ j ] );
        }
        return out;
    },
    

    push: function() {
        var a = arguments;
        for( var i = 0; i < a.length; i++ )
            this[ this.length ] = a[ i ];
        return this.length;     
    },


    pop: function() {
        if( this.length == 0 )
            return undefined;
        var out = this[ this.length - 1 ];
        this.length--;
        return out;
    },
    
    
    unshift: function() {
        var a = arguments;
        for( var i = 0; i < a.length; i++ ) {
            this[ i + a.length ] = this[ i ];
            this[ i ] = a[ i ];
        }
        return this.length;     
    },
    
    
    shift: function() {
        if( this.length == 0 )
            return undefined;
        var out = this[ 0 ];
        for( var i = 1; i < this.length; i++ )
            this[ i - 1 ] = this[ i ];
        this.length--;
        return out;
    }
} );


/* date extensions */

Date.extend( {
    /*  iso 8601 date format parser
        this was fun to write...
        thanks to: http://www.cl.cam.ac.uk/~mgk25/iso-time.html */

    matchISOString: new RegExp(
        "^([0-9]{4})" +                                                     // year
        "(?:-(?=0[1-9]|1[0-2])|$)(..)?" +                                   // month
        "(?:-(?=0[1-9]|[12][0-9]|3[01])|$)([0-9]{2})?" +                    // day of the month
        "(?:T(?=[01][0-9]|2[0-4])|$)T?([0-9]{2})?" +                        // hours
        "(?::(?=[0-5][0-9])|\\+|-|Z|$)([0-9]{2})?" +                        // minutes
        "(?::(?=[0-5][0-9]|60$|60[+|-|Z]|60.0+)|\\+|-|Z|$):?([0-9]{2})?" +  // seconds
        "(\.[0-9]+)?" +                                                     // fractional seconds
        "(Z|\\+[01][0-9]|\\+2[0-4]|-[01][0-9]|-2[0-4])?" +                  // timezone hours
        ":?([0-5][0-9]|60)?$"                                               // timezone minutes
    ),
    
    
    fromISOString: function( string ) {
        var t = this.matchISOString.exec( string );
        if( !t )
            return undefined;

        var year = finiteInt( t[ 1 ], 10 );
        var month = finiteInt( t[ 2 ], 10 ) - 1;
        var day = finiteInt( t[ 3 ], 10 );
        var hours = finiteInt( t[ 4 ], 10 );
        var minutes = finiteInt( t[ 5 ], 10 );
        var seconds = finiteInt( t[ 6 ], 10 );
        var milliseconds = finiteInt( Math.round( parseFloat( t[ 7 ] ) * 1000 ) );
        var tzHours = finiteInt( t[ 8 ], 10 );
        var tzMinutes = finiteInt( t[ 9 ], 10 );

        var date = new this( 0 );
        if( defined( t[ 8 ] ) ) {
            date.setUTCFullYear( year, month, day );
            date.setUTCHours( hours, minutes, seconds, milliseconds );
            var offset = (tzHours * 60 + tzMinutes) * 60000;
            if( offset )
                date = new this( date - offset );
        } else {
            date.setFullYear( year, month, day );
            date.setHours( hours, minutes, seconds, milliseconds );
        }

        return date;
    }
} );


Date.prototype.extend( {
    getISOTimezoneOffset: function() {
        var offset = -this.getTimezoneOffset();
        var negative = false;
        if( offset < 0 ) {
            negative = true;
            offset *= -1;
        }
        var offsetHours = Math.floor( offset / 60 ).toString().pad( 2, "0" );
        var offsetMinutes = Math.floor( offset % 60 ).toString().pad( 2, "0" );
        return (negative ? "-" : "+") + offsetHours + ":" + offsetMinutes;
    },


    toISODateString: function() {
        var year = this.getFullYear();
        var month = (this.getMonth() + 1).toString().pad( 2, "0" );
        var day = this.getDate().toString().pad( 2, "0" );
        return year + "-" + month + "-" + day;
    },


    toUTCISODateString: function() {
        var year = this.getUTCFullYear();
        var month = (this.getUTCMonth() + 1).toString().pad( 2, "0" );
        var day = this.getUTCDate().toString().pad( 2, "0" );
        return year + "-" + month + "-" + day;
    },


    toISOTimeString: function() {
        var hours = this.getHours().toString().pad( 2, "0" );
        var minutes = this.getMinutes().toString().pad( 2, "0" );
        var seconds = this.getSeconds().toString().pad( 2, "0" );
        var milliseconds = this.getMilliseconds().toString().pad( 3, "0" );
        var timezone = this.getISOTimezoneOffset();
        return hours + ":" + minutes + ":" + seconds + "." + milliseconds + timezone;
    },


    toUTCISOTimeString: function() {
        var hours = this.getUTCHours().toString().pad( 2, "0" );
        var minutes = this.getUTCMinutes().toString().pad( 2, "0" );
        var seconds = this.getUTCSeconds().toString().pad( 2, "0" );
        var milliseconds = this.getUTCMilliseconds().toString().pad( 3, "0" );
        return hours + ":" + minutes + ":" + seconds + "." + milliseconds + "Z";
    },


    toISOString: function() {
        return this.toISODateString() + "T" + this.toISOTimeString();
    },


    toUTCISOString: function() {
        return this.toUTCISODateString() + "T" + this.toUTCISOTimeString();
    }
} );


/* ajax */

if( !defined( window.XMLHttpRequest ) ) {
    window.XMLHttpRequest = function() {
        var types = [
            "Microsoft.XMLHTTP",
            "MSXML2.XMLHTTP.5.0",
            "MSXML2.XMLHTTP.4.0",
            "MSXML2.XMLHTTP.3.0",
            "MSXML2.XMLHTTP"
        ];
        
        for( var i = 0; i < types.length; i++ ) {
            try {
                return new ActiveXObject( types[ i ] );
            } catch( e ) {}
        }
        
        return undefined;
    }
}
/*
DOM Library - Copyright 2005 Six Apart
$Id: dom.js 261 2008-02-26 23:40:50Z janine $

Copyright (c) 2005, Six Apart, Ltd.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.

    * Neither the name of "Six Apart" nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/


/* Node class */

if( !defined( window.Node ) )
    Node = {};

try {
    Node.extend( {
        ELEMENT_NODE: 1,
        ATTRIBUTE_NODE: 2,
        TEXT_NODE: 3,
        CDATA_SECTION_NODE: 4,  
        COMMENT_NODE: 8,    
        DOCUMENT_NODE: 9,
        DOCUMENT_FRAGMENT_NODE: 11
    } );
} catch( e ) {}


/* DOM class */

if( !defined( window.DOM ) )
    DOM = {};


DOM.extend( {
    getElement: function( e ) {
        return (typeof e == "string" || typeof e == "number") ? document.getElementById( e ) : e;
    },


    addEventListener: function( e, eventName, func, useCapture ) {
        try {
            if( e.addEventListener )
                e.addEventListener( eventName, func, useCapture );
            else if( e.attachEvent )
                e.attachEvent( "on" + eventName, func );
            else
                e[ "on" + eventName ] = func;
        } catch( e ) {}
    },


    removeEventListener: function( e, eventName, func, useCapture ) {
        try {
            if( e.removeEventListener )
                e.removeEventListener( eventName, func, useCapture );
            else if( e.detachEvent )
                e.detachEvent( "on" + eventName, func );
            else
                e[ "on" + eventName ] = undefined;
        } catch( e ) {}
    },
    
    
    focus: function( e ) {
        try {
            e = DOM.getElement( e );
            e.focus();
        } catch( e ) {}
    },


    blur: function( e ) {
        try {
            e = DOM.getElement( e );
            e.blur();
        } catch( e ) {}
    },
    

    /* style */
    
    getComputedStyle: function( e ) {
        if( e.currentStyle )
            return e.currentStyle;
        var style = {};
        var owner = DOM.getOwnerDocument( e );
        if( owner && owner.defaultView && owner.defaultView.getComputedStyle ) {            
            try {
                style = owner.defaultView.getComputedStyle( e, null );
            } catch( e ) {}
        }
        return style;
    },


    getStyle: function( e, p ) {
        var s = DOM.getComputedStyle( e );
        return s[ p ];
    },


    // given a window (or defaulting to current window), returns
    // object with .x and .y of client's usable area
    getClientDimensions: function( w ) {
        if( !w )
            w = window;

        var d = {};

        // most browsers
        if( w.innerHeight ) {
            d.x = w.innerWidth;
            d.y = w.innerHeight;
            return d;
        }

        // IE6, strict
        var de = w.document.documentElement;
        if( de && de.clientHeight ) {
            d.x = de.clientWidth;
            d.y = de.clientHeight;
            return d;
        }

        // IE, misc
        if( document.body ) {
            d.x = document.body.clientWidth;
            d.y = document.body.clientHeight;
            return d;
        }
        
        return undefined;
    },


    getDimensions: function( e ) {
        if( !e )
            return undefined;

        var style = DOM.getComputedStyle( e );

        return {
            offsetLeft: e.offsetLeft,
            offsetTop: e.offsetTop,
            offsetWidth: e.offsetWidth,
            offsetHeight: e.offsetHeight,
            clientWidth: e.clientWidth,
            clientHeight: e.clientHeight,
            
            offsetRight: e.offsetLeft + e.offsetWidth,
            offsetBottom: e.offsetTop + e.offsetHeight,
            clientLeft: finiteInt( style.borderLeftWidth ) + finiteInt( style.paddingLeft ),
            clientTop: finiteInt( style.borderTopWidth ) + finiteInt( style.paddingTop ),
            clientRight: e.clientLeft + e.clientWidth,
            clientBottom: e.clientTop + e.clientHeight
        };
    },


    getAbsoluteDimensions: function( e ) {
        var d = DOM.getDimensions( e );
        if( !d )
            return d;
        d.absoluteLeft = d.offsetLeft;
        d.absoluteTop = d.offsetTop;
        d.absoluteRight = d.offsetRight;
        d.absoluteBottom = d.offsetBottom;
        var bork = 0;
        while( e ) {
            try { // IE 6 sometimes gives an unwarranted error ("htmlfile: Unspecified error").
                e = e.offsetParent;
            } catch ( err ) {
                log( "In DOM.getAbsoluteDimensions: " + err.message ); 
                if ( ++bork > 25 )
                    return null;
            }
            if( !e )
                return d;
            d.absoluteLeft += e.offsetLeft;
            d.absoluteTop += e.offsetTop;
            d.absoluteRight += e.offsetLeft;
            d.absoluteBottom += e.offsetTop;
        }
        return d;
    },
    
    
    getIframeAbsoluteDimensions: function( e ) {
        var d = DOM.getAbsoluteDimensions( e );
        if( !d )
            return d;
        var iframe = DOM.getOwnerIframe( e );
        if( !defined( iframe ) )
            return d;
        
        var d2 = DOM.getIframeAbsoluteDimensions( iframe );
        var scroll = DOM.getWindowScroll( iframe.contentWindow );
        var left = d2.absoluteLeft - scroll.left;
        var top = d2.absoluteTop - scroll.top;
        
        d.absoluteLeft += left;
        d.absoluteTop += top;
        d.absoluteRight += left;
        d.absoluteBottom += top;
        
        return d;
    },
    
    
    setLeft: function( e, v ) { e.style.left = finiteInt( v ) + "px"; },
    setTop: function( e, v ) { e.style.top = finiteInt( v ) + "px"; },
    setRight: function( e, v ) { e.style.right = finiteInt( v ) + "px"; },
    setBottom: function( e, v ) { e.style.bottom = finiteInt( v ) + "px"; },
    setWidth: function( e, v ) { e.style.width = max( 0, finiteInt( v ) ) + "px"; },
    setHeight: function( e, v ) { e.style.height = max( 0, finiteInt( v ) ) + "px"; },
    setZIndex: function( e, v ) { e.style.zIndex = finiteInt( v ); },


    getWindowScroll: function( w ) {
        var s = {
            left: 0,
            top: 0
        };

        if (!w) w = window;
        var d = w.document;
        var de = d.documentElement;

        // most browsers
        if ( defined( w.pageXOffset ) ) {
            s.left = w.pageXOffset;
            s.top = w.pageYOffset;
        }

        // ie
        else if( de && defined( de.scrollLeft ) ) {
            s.left = de.scrollLeft;
            s.top = de.scrollTop;
        }

        // safari
        else if( defined( w.scrollX ) ) {
            s.left = w.scrollX;
            s.top = w.scrollY;
        }

        // opera
        else if( d.body && defined( d.body.scrollLeft ) ) {
            s.left = d.body.scrollLeft;
            s.top = d.body.scrollTop;
        }


        return s;
    },


    getAbsoluteCursorPosition: function( event ) {
        event = event || window.event;
        var s = DOM.getWindowScroll( window );
        return {
            x: s.left + event.clientX,
            y: s.top + event.clientY
        };
    },
    
    
    invisibleStyle: {
        display: "block",
        position: "absolute",
        left: 0,
        top: 0,
        width: 0,
        height: 0,
        margin: 0,
        border: 0,
        padding: 0,
        fontSize: "0.1px",
        lineHeight: 0,
        opacity: 0,
        MozOpacity: 0,
        filter: "alpha(opacity=0)"
    },
    
    
    makeInvisible: function( e ) {
        for( var p in this.invisibleStyle ) {
            if( this.invisibleStyle.hasOwnProperty( p ) )
                e.style[ p ] = this.invisibleStyle[ p ];
        }
    },


    /* text and selection related methods */

    mergeTextNodes: function( n ) {
        var c = 0;
        while( n ) {
            if( n.nodeType == Node.TEXT_NODE && n.nextSibling && n.nextSibling.nodeType == Node.TEXT_NODE ) {
                n.nodeValue += n.nextSibling.nodeValue;
                n.parentNode.removeChild( n.nextSibling );
                c++;
            } else {
                if( n.firstChild )
                    c += DOM.mergeTextNodes( n.firstChild );
                n = n.nextSibling;
            }
        }
        return c;
    },
    
    
    selectElement: function( e ) {  
        var d = e.ownerDocument;  
        
        // internet explorer  
        if( d.body.createControlRange ) {  
            var r = d.body.createControlRange();  
            r.addElement( e );  
            r.select();  
        }  
    }, 
    
    
    /* dom methods */
    
    isImmutable: function( n ) {
        try {
            if( n.getAttribute( "contenteditable" ) == "false" )
                return true;
        } catch( e ) {}
        return false;
    },
    
    
    getImmutable: function( n ) {
        var immutable = null;
        while( n ) {
            if( DOM.isImmutable( n ) )
                immutable = n;
            n = n.parentNode;
        }
        return immutable;
    },


    getOwnerDocument: function( n ) {
        if( !n )
            return document;
        if( n.ownerDocument )
            return n.ownerDocument;
        if( n.getElementById )
            return n;
        return document;
    },


    getOwnerWindow: function( n ) {
        if( !n )
            return window;
        if( n.parentWindow )
            return n.parentWindow;
        var doc = DOM.getOwnerDocument( n );
        if( doc && doc.defaultView )
            return doc.defaultView;
        return window;
    },
    
    
    getOwnerIframe: function( n ) {
        if( !n )
            return undefined;
        var nw = DOM.getOwnerWindow( n );
        var nd = DOM.getOwnerDocument( n );
        var pw = nw.parent || nw.parentWindow;
        if( !pw )
            return undefined;
        var parentDocument = pw.document;
        var es = parentDocument.getElementsByTagName( "iframe" );
        for( var i = 0; i < es.length; i++ ) {
            var e = es[ i ];
            try {
                var d = e.contentDocument || e.contentWindow.document;
                if( d === nd )
                    return e;
            }catch(err) {};
        }
        return undefined;
    },


    filterElementsByClassName: function( es, className ) {
        var filtered = [];
        for( var i = 0; i < es.length; i++ ) {
            var e = es[ i ];
            if( DOM.hasClassName( e, className ) )
                filtered[ filtered.length ] = e;
        }
        return filtered;
    },
    
    
    filterElementsByAttribute: function( es, attr ) {
        if( !es )
            return [];
        if( !defined( attr ) || attr == null || attr == "" )
            return es;
        var filtered = [];
        for( var i = 0; i < es.length; i++ ) {
            var element = es[ i ];
            if( !element )
                continue;
            if( element.getAttribute && ( element.getAttribute( attr ) ) )
                filtered[ filtered.length ] = element;
        }
        return filtered;
    },


    filterElementsByTagName: function( es, tagName ) {
        if( tagName == "*" )
            return es;
        var filtered = [];
        tagName = tagName.toLowerCase();
        for( var i = 0; i < es.length; i++ ) {
            var e = es[ i ];
            if( e.tagName && e.tagName.toLowerCase() == tagName )
                filtered[ filtered.length ] = e;
        }
        return filtered;
    },


    getElementsByTagAndAttribute: function( root, tagName, attr ) {
        if( !root )
            root = document;
        var es = root.getElementsByTagName( tagName );
        return DOM.filterElementsByAttribute( es, attr );
    },
    
    
    getElementsByAttribute: function( root, attr ) {
        return DOM.getElementsByTagAndAttribute( root, "*", attr );
    },


    getElementsByAttributeAndValue: function( root, attr, value ) {
        var es = DOM.getElementsByTagAndAttribute( root, "*", attr );
        var filtered = [];
        for ( var i = 0; i < es.length; i++ )
            if ( es[ i ].getAttribute( attr ) == value )
                filtered.push( es[ i ] );
        return filtered;
    },
    

    getElementsByTagAndClassName: function( root, tagName, className ) {
        if( !root )
            root = document;
        var elements = root.getElementsByTagName( tagName );
        return DOM.filterElementsByClassName( elements, className );
    },


    getElementsByClassName: function( root, className ) {
        return DOM.getElementsByTagAndClassName( root, "*", className );
    },


    getAncestors: function( n, includeSelf ) {
        if( !n )
            return [];
        var as = includeSelf ? [ n ] : [];
        n = n.parentNode;
        while( n ) {
            as.push( n );
            n = n.parentNode;
        }
        return as;
    },
    
    
    getAncestorsByTagName: function( n, tagName, includeSelf ) {
        var es = DOM.getAncestors( n, includeSelf );
        return DOM.filterElementsByTagName( es, tagName );
    },
    
    
    getFirstAncestorByTagName: function( n, tagName, includeSelf ) {
        return DOM.getAncestorsByTagName( n, tagName, includeSelf )[ 0 ];
    },


    getAncestorsByClassName: function( n, className, includeSelf ) {
        var es = DOM.getAncestors( n, includeSelf );
        return DOM.filterElementsByClassName( es, className );
    },


    getFirstAncestorByClassName: function( n, className, includeSelf ) {
        return DOM.getAncestorsByClassName( n, className, includeSelf )[ 0 ];
    },


    getAncestorsByTagAndClassName: function( n, tagName, className, includeSelf ) {
        var es = DOM.getAncestorsByTagName( n, tagName, includeSelf );
        return DOM.filterElementsByClassName( es, className );
    },


    getFirstAncestorByTagAndClassName: function( n, tagName, className, includeSelf ) {
        return DOM.getAncestorsByTagAndClassName( n, tagName, className, includeSelf )[ 0 ];
    },


    getPreviousElement: function( n ) {
        n = n.previousSibling;
        while( n ) {
            if( n.nodeType == Node.ELEMENT_NODE )
                return n;
            n = n.previousSibling;
        }
        return null;
    },


    getNextElement: function( n ) {
        n = n.nextSibling;
        while( n ) {
            if( n.nodeType == Node.ELEMENT_NODE )
                return n;
            n = n.nextSibling;
        }
        return null;
    },


    isInlineNode: function( n ) {
        // text nodes are inline
        if( n.nodeType == Node.TEXT_NODE )
            return n;

        // document nodes are non-inline
        if( n.nodeType == Node.DOCUMENT_NODE )
            return false;

        // all nonelement nodes are inline
        if( n.nodeType != Node.ELEMENT_NODE )
            return n;

        // br elements are not inline
        if( n.tagName && n.tagName.toLowerCase() == "br" )
            return false;

        // examine the style property of the inline n
        var display = DOM.getStyle( n, "display" ); 
        if( display && display.indexOf( "inline" ) >= 0 ) 
            return n;
    },
    
    
    isTextNode: function( n ) {
        if( n.nodeType == Node.TEXT_NODE )
            return n;
    },
    
    
    isInlineTextNode: function( n ) {
        if( n.nodeType == Node.TEXT_NODE )
            return n;
        if( !DOM.isInlineNode( n ) )
            return null;
    },


    /* this and the following classname functions honor w3c case-sensitive classnames */

    getClassNames: function( e ) {
        if( !e || !e.className )
            return [];
        return e.className.split( /\s+/g );
    },


    hasClassName: function( e, className ) {
        if( !e || !e.className )
            return false;
        var cs = DOM.getClassNames( e );
        for( var i = 0; i < cs.length; i++ ) {
            if( cs[ i ] == className )
                return true;
        }
        return false;
    },


    addClassName: function( e, className ) {
        if( !e || !className )
            return false;
        var cs = DOM.getClassNames( e );
        for( var i = 0; i < cs.length; i++ ) {
            if( cs[ i ] == className )
                return true;
        }
        cs.push( className );
        e.className = cs.join( " " );
        return false;
    },


    removeClassName: function( e, className ) {
        var r = false;
        if( !e || !e.className || !className )
            return r;
        var cs = (e.className && e.className.length)
            ? e.className.split( /\s+/g )
            : [];
        var ncs = [];
        for( var i = 0; i < cs.length; i++ ) {
            if( cs[ i ] == className ) {
                r = true;
                continue;
            }
            ncs.push( cs[ i ] );
        }
        if( r )
            e.className = ncs.join( " " );
        return r;
    },
    
    
    /* tree manipulation methods */
    
    replaceWithChildNodes: function( n ) {
        var firstChild = n.firstChild;
        var parentNode = n.parentNode;
        while( n.firstChild )
            parentNode.insertBefore( n.removeChild( n.firstChild ), n );
        parentNode.removeChild( n );
        return firstChild;
    },
    
    
    /* factory methods */
    
    createInvisibleInput: function( d ) {
        if( !d )
            d = window.document;
        var e = document.createElement( "input" );
        e.setAttribute( "autocomplete", "off" );
        e.autocomplete = "off";
        DOM.makeInvisible( e );
        return e;
    },


    getMouseEventAttribute: function( event, a ) {
        if( !a )
            return;
        var es = DOM.getAncestors( event.target, true );
        for( var i = 0; i < es.length; i++ ) {
            try {
                var e = es[ i ]
                var v = e.getAttribute ? e.getAttribute( a ) : null;
                if( v ) {
                    event.attributeElement = e;
                    event.attribute = v;
                    return v;
                }
            } catch( e ) {}
        }
    },
    

    setElementAttribute: function( e, a, v ) {
        /* safari workaround
         * safari's setAttribute assumes you want to use a namespace
         * when you have a colon in your attribute
         */
        if ( navigator.userAgent.toLowerCase().match(/webkit/) ) {
            var at = e.attributes;
            for ( var i = 0; i < at.length; i++ )
                if ( at[ i ].name == a )
                    return at[ i ].nodeValue = v;
        } else
            e.setAttribute( a, v );
    },


    swapAttributes: function( e, tg, at ) {
        var ar = e.getAttribute( tg );
        if( !ar )
            return false;
        
        /* clone the node with all children */
        if ( e.tagName.toLowerCase() == 'script' ) {
            /* only clone and replace script tags */
            var cl = e.cloneNode( true );
            if ( !cl )
                return false;

            DOM.setElementAttribute( cl, at, ar );
            cl.removeAttribute( tg );
        
            /* replace new, old */
            return e.parentNode.replaceChild( cl, e );
        } else {
            DOM.setElementAttribute( e, at, ar );
            e.removeAttribute( tg );
        }
    },


    findPosX: function( e ) {
        var curleft = 0;

        if (e.offsetParent) {
            while (1) {
                curleft += e.offsetLeft;
                if (!e.offsetParent) {
                    break;
                }
                e = e.offsetParent;
            }
        } else if (e.x) {
            curleft += e.x;
        }

        return curleft;
    },


    findPosY: function( e ) {
        var curtop = 0;

        if (e.offsetParent) {
            while (1) {
                curtop += e.offsetTop;
                if (!e.offsetParent) {
                    break;
                }
                e = e.offsetParent;
            }
        } else if (e.y) {
            curtop += e.y;
        }

        return curtop;
    }
    
    
} );


$ = DOM.getElement;
var HTTPReq = new Object;

HTTPReq.create = function () {
    var xtr;
    var ex;

    if (typeof(XMLHttpRequest) != "undefined") {
        xtr = new XMLHttpRequest();
    } else {
        try {
            xtr = new ActiveXObject("Msxml2.XMLHTTP.4.0");
        } catch (ex) {
            try {
                xtr = new ActiveXObject("Msxml2.XMLHTTP");
            } catch (ex) {
            }
        }
    }

    // let me explain this.  Opera 8 does XMLHttpRequest, but not setRequestHeader.
    // no problem, we thought:  we'll test for setRequestHeader and if it's not present
    // then fall back to the old behavior (treat it as not working).  BUT --- IE6 won't
    // let you even test for setRequestHeader without throwing an exception (you need
    // to call .open on the .xtr first or something)
    try {
        if (xtr && ! xtr.setRequestHeader)
            xtr = null;
    } catch (ex) { }

    return xtr;
};

// opts:
// url, onError, onData, method (GET or POST), data
// url: where to get/post to
// onError: callback on error
// onData: callback on data received
// method: HTTP method, GET by default
// data: what to send to the server (urlencoded)
HTTPReq.getJSON = function (opts) {
    var req = HTTPReq.create();
    if (! req) {
        if (opts.onError) opts.onError("noxmlhttprequest");
        return;
    }

    var state_callback = function () {
        if (req.readyState != 4) return;

        if (req.status != 200) {
            if (opts.onError) opts.onError(req.status ? "status: " + req.status : "no data");
            return;
        }

        var resObj;
        var e;
        try {
            eval("resObj = " + req.responseText + ";");
        } catch (e) {
        }

        if (e || ! resObj) {
            if (opts.onError)
                opts.onError("Error parsing response: \"" + req.responseText + "\"");

            return;
        }

        if (opts.onData)
            opts.onData(resObj);
    };

    req.onreadystatechange = state_callback;

    var method = opts.method || "GET";
    var data = opts.data || null;

    var url = opts.url;
    if (opts.method == "GET" && opts.data) {
        url += url.match(/\?/) ? "&" : "?";
        url += opts.data
    }

    url += url.match(/\?/) ? "&" : "?";
    url += "_rand=" + Math.random();

    req.open(method, url, true);

    // we should send null unless we're in a POST
    var to_send = null;

    if (method.toUpperCase() == "POST") {
        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        to_send = data;
    }

    req.send(to_send);
};

HTTPReq.formEncoded = function (vars) {
    var enc = [];
    var e;
    for (var key in vars) {
        try {
            if (!vars.hasOwnProperty(key))
                continue;
            enc.push(encodeURIComponent(key) + "=" + encodeURIComponent(vars[key]));
        } catch( e ) {}
    }
    return enc.join("&");

};

// This file contains general-purpose LJ code

var LiveJournal = new Object;

// The hook mappings
LiveJournal.hooks = {};

LiveJournal.register_hook = function (hook, func) {
    if (! LiveJournal.hooks[hook])
        LiveJournal.hooks[hook] = [];

    LiveJournal.hooks[hook].push(func);
};

// args: hook, params to pass to hook
LiveJournal.run_hook = function () {
    var a = arguments;

    var hookfuncs = LiveJournal.hooks[a[0]];
    if (!hookfuncs || !hookfuncs.length) return;

    var hookargs = [];

    for (var i = 1; i < a.length; i++) {
        hookargs.push(a[i]);
    }

    var rv = null;

    hookfuncs.forEach(function (hookfunc) {
        rv = hookfunc.apply(null, hookargs);
    });

    return rv;
};

LiveJournal.pageLoaded = false;

LiveJournal.initPage = function () {
    // only run once
    if (LiveJournal.pageLoaded)
        return;
    LiveJournal.pageLoaded = 1;

    // set up various handlers for every page
    LiveJournal.initPlaceholders();
    LiveJournal.initLabels();
    LiveJournal.initInboxUpdate();
    LiveJournal.initAds();
    LiveJournal.initPolls();

    // run other hooks
    LiveJournal.run_hook("page_load");
};

// Set up two different ways to test if the page is loaded yet.
// The proper way is using DOMContentLoaded, but only Mozilla supports it.
{
    // Others
    DOM.addEventListener(window, "load", LiveJournal.initPage);

    // Mozilla
    DOM.addEventListener(window, "DOMContentLoaded", LiveJournal.initPage);
}

// Set up a timer to keep the inbox count updated
LiveJournal.initInboxUpdate = function () {
    // Don't run if not logged in or this is disabled
    if (! Site || ! Site.has_remote || ! Site.inbox_update_poll) return;

    // Don't run if no inbox count
    var unread = $("LJ_Inbox_Unread_Count");
    if (! unread) return;

    // Update every five minutes
    window.setInterval(LiveJournal.updateInbox, 1000 * 60 * 5);
};

// Do AJAX request to find the number of unread items in the inbox
LiveJournal.updateInbox = function () {
    var postData = {
        "action": "get_unread_items"
    };

    var opts = {
        "data": HTTPReq.formEncoded(postData),
        "method": "POST",
        "onData": LiveJournal.gotInboxUpdate
    };

    opts.url = Site.currentJournal ? "/" + Site.currentJournal + "/__rpc_esn_inbox" : "/__rpc_esn_inbox";

    HTTPReq.getJSON(opts);
};

// We received the number of unread inbox items from the server
LiveJournal.gotInboxUpdate = function (resp) {
    if (! resp || resp.error) return;

    var unread = $("LJ_Inbox_Unread_Count");
    if (! unread) return;

    unread.innerHTML = resp.unread_count ? "  (" + resp.unread_count + ")" : "";
};

// Search for placeholders and initialize them
LiveJournal.initPlaceholders = function () {
    var placeholders = DOM.getElementsByTagAndClassName(document, "img", "LJ_Placeholder") || [];

    Array.prototype.forEach.call(placeholders, function (placeholder) {
        var parent = DOM.getFirstAncestorByClassName(placeholder, "LJ_Placeholder_Container", false);
        if (!parent) return;

        var container = DOM.filterElementsByClassName(parent.getElementsByTagName("div"), "LJ_Container")[0];
        if (!container) return;

        var html = DOM.filterElementsByClassName(parent.getElementsByTagName("div"), "LJ_Placeholder_HTML")[0];
        if (!html) return;

        var placeholder_html = unescape(html.innerHTML);

        var placeholderClickHandler = function (e) {
            Event.stop(e);
            // have to wrap placeholder_html in another block, IE is weird
            container.innerHTML = "<span>" + placeholder_html + "</span>";
            DOM.makeInvisible(placeholder);
        };

        DOM.addEventListener(placeholder, "click", placeholderClickHandler);

        return false;
    });
};

// set up labels for Safari
LiveJournal.initLabels = function () {
    // disabled because new webkit has labels that work
    return;

    // safari doesn't know what <label> tags are, lets fix them
    if (navigator.userAgent.indexOf('Safari') == -1) return;

    // get all labels
    var labels = document.getElementsByTagName("label");

    for (var i = 0; i < labels.length; i++) {
        DOM.addEventListener(labels[i], "click", LiveJournal.labelClickHandler);
    }
};

LiveJournal.labelClickHandler = function (evt) {
    Event.prep(evt);

    var label = DOM.getAncestorsByTagName(evt.target, "label", true)[0];
    if (! label) return;

    var targetId = label.getAttribute("for");
    if (! targetId) return;

    var target = $(targetId);
    if (! target) return;

    target.click();

    return false;
};

// change drsc to src for ads
LiveJournal.initAds = function () {
    AdEngine.init();
};

// handy utilities to create elements with just text in them
function _textSpan () { return _textElements("span", arguments); }
function _textDiv  () { return _textElements("div", arguments);  }

function _textElements (eleType, txts) {
    var ele = [];
    for (var i = 0; i < txts.length; i++) {
        var node = document.createElement(eleType);
        node.innerHTML = txts[i];
        ele.push(node);
    }

    return ele.length == 1 ? ele[0] : ele;
};

LiveJournal.initPolls = function () {
    var pollLinks = DOM.getElementsByTagAndClassName(document, 'a', "LJ_PollAnswerLink") || [];  

    // attach click handlers to each answer link
    Array.prototype.forEach.call(pollLinks, function (pollLink) {
        DOM.addEventListener(pollLink, "click", LiveJournal.pollAnswerLinkClicked.bindEventListener(pollLink));
    });
};

// invocant is the pollLink from above
LiveJournal.pollAnswerLinkClicked = function (e) {
    Event.stop(e);

    if (! this || ! this.tagName || this.tagName.toLowerCase() != "a")
    return true;

    var pollid = this.getAttribute("lj_pollid");
    if (! pollid) return true;

    var pollqid = this.getAttribute("lj_qid");
    if (! pollqid) return true;

    var action = "get_answers";

    // Do ajax request to replace the link with the answers
    var params = {
        "pollid" : pollid,
        "pollqid": pollqid,
        "action" : action
    };

    var opts = {
        "url"    : LiveJournal.getAjaxUrl("poll"),
        "method" : "POST",
        "data"   : HTTPReq.formEncoded(params),
        "onData" : LiveJournal.pollAnswersReceived,
        "onError": LiveJournal.ajaxError
    };

    HTTPReq.getJSON(opts);
    this.innerHTML = "<div class='lj_pollanswer_loading'>Loading...</div>";

    return false;
};

LiveJournal.pollAnswersReceived = function (answers) {
    if (! answers) return false;
    if (answers.error) return LiveJournal.ajaxError(answers.error);

    var pollid = answers.pollid;
    var pollqid = answers.pollqid;
    if (! pollid || ! pollqid) return false;

    var linkEle = $("LJ_PollAnswerLink_" + pollid + "_" + pollqid);
    if (! linkEle) return false;

    var answerEle = document.createElement("div");
    DOM.addClassName(answerEle, "lj_pollanswer");
    answerEle.innerHTML = answers.answer_html ? answers.answer_html : "(No answers)";

    linkEle.parentNode.insertBefore(answerEle, linkEle);
    linkEle.parentNode.removeChild(linkEle);
};


// gets a url for doing ajax requests
LiveJournal.getAjaxUrl = function (action) {
    // if we are on a journal subdomain then our url will be
    // /journalname/__rpc_action instead of /__rpc_action
    return Site.currentJournal
        ? "/" + Site.currentJournal + "/__rpc_" + action
        : "/__rpc_" + action;
};

// generic handler for ajax errors
LiveJournal.ajaxError = function (err) {
    if (LJ_IPPU) {
        LJ_IPPU.showNote("Error: " + err);
    } else {
        alert("Error: " + err);
    }
};

// utility method to get all items on the page with a certain class name
LiveJournal.getDocumentElementsByClassName = function (className) {
  var domObjects = document.getElementsByTagName("*");
  var items = DOM.filterElementsByClassName(domObjects, className) || [];

  return items;
};

// utility method to add an onclick callback on all items with a classname
LiveJournal.addClickHandlerToElementsWithClassName = function (callback, className) {
  var items = LiveJournal.getDocumentElementsByClassName(className);

  items.forEach(function (item) {
    DOM.addEventListener(item, "click", callback);
  })
};

LiveJournal.insertAdsMulti = function (params) {
  var i = 0;
  var containers = [];

  for (i = 0; i < params.length; i++) {
    if (! params[i].html || params[i].html == "<ul>\n</ul>") continue;
    AdEngine.insertAdResponse( params[i] );
    containers.push(document.getElementById(params[i].id));
  }

    // add the ad box style to the containers
    containers.forEach(function (container) {
      if (! container) return;

      DOM.addClassName(container.parentNode, "lj_content_ad");
      DOM.removeClassName(container.parentNode, "lj_inactive_ad");
    });
};

// given a URL, parse out the GET args and return them in a hash
LiveJournal.parseGetArgs = function (url) {
    var getArgsHash = {};

    var urlParts = url.split("?");
    if (!urlParts[1]) return getArgsHash;
    var getArgs = urlParts[1].split("&");
    for (var arg in getArgs) {
        if (!getArgs.hasOwnProperty(arg)) continue;
        var pair = getArgs[arg].split("=");
        getArgsHash[pair[0]] = pair[1];
    }

    return getArgsHash;
};
/* 
AdEngine
$Id: AdEngine.js 226 2007-09-18 18:20:57Z janine $

Copyright (c) 2006, Six Apart, Ltd.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.

    * Neither the name of "Six Apart" nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

AdEngine = {

    /* called from window.onload ONLY on the logged out blog side (view) */
    init: function() {
        var es = document.getElementsByTagName( "script" );
        for ( var i = 0; i < es.length; i++ ) {
            var ar = es[ i ].getAttribute( "defersrc" );
            if( !ar )
                continue;
        
            var cl = es[ i ].cloneNode( true );
            if ( !cl )
                continue;

            cl.setAttribute( "src", ar );
            cl.removeAttribute( "defersrc" );
        
            /* replace new, old */
            try {
                es[ i ].parentNode.replaceChild( cl, es[ i ] );
            } catch (e) {}
        }
    },
    
    
    insertAdResponse: function( params ) {
        var e = document.getElementById( params.id );
        if( !e )
            return;
        if( params.html ) {
            var e2 = document.createElement( "div" );
            e2.innerHTML = params.html;
            e.innerHTML = ""; // clear old content
            e.appendChild( e2 );
        }
        if( params.js )
            return eval( "(" + params.js + ")" );
    },

    insertAdsMulti: function( params ) {
        var i = 0;
        for (i = 0; i < params.length; i++) {
            AdEngine.insertAdResponse( params[i] );
        }
    }

};
var ESN = new Object();

LiveJournal.register_hook("page_load", function () {
  ESN.initCheckAllBtns();
  ESN.initEventCheckBtns();
  ESN.initTrackBtns();
});

// When page loads, set up "check all" checkboxes
ESN.initCheckAllBtns = function () {
  var ntids  = $("ntypeids");
  var catids = $("catids");

  if (!ntids || !catids)
    return;

  ntidList  = ntids.value;
  catidList = catids.value;

  if (!ntidList || !catidList)
    return;

  ntids  = ntidList.split(",");
  catids = catidList.split(",");

  catids.forEach( function (catid) {
    ntids.forEach( function (ntypeid) {
      var className = "SubscribeCheckbox-" + catid + "-" + ntypeid;

      var cab = new CheckallButton();
      cab.init({
        "class": className,
          "button": $("CheckAll-" + catid + "-" + ntypeid),
          "parent": $("CategoryRow-" + catid)
          });
    });
  });
}

// set up auto show/hiding of notification methods
ESN.initEventCheckBtns = function () {
  var viewObjects = document.getElementsByTagName("*");
  var boxes = DOM.filterElementsByClassName(viewObjects, "SubscriptionInboxCheck") || [];

  boxes.forEach( function (box) {
    DOM.addEventListener(box, "click", ESN.eventChecked.bindEventListener());
  });
}

ESN.eventChecked = function (evt) {
  var target = evt.target;
  if (!target)
    return;

  var parentRow = DOM.getFirstAncestorByTagName(target, "tr", false);

  var viewObjects = parentRow.getElementsByTagName("*");
  var boxes = DOM.filterElementsByClassName(viewObjects, "NotificationOptions") || [];

  boxes.forEach( function (box) {
    box.style.visibility = target.checked ? "visible" : "hidden";
  });
}

// attach event handlers to all track buttons
ESN.initTrackBtns = function () {
    // don't do anything if no remote
    if (!Site || !Site.has_remote) return;

    // attach to all ljuser head icons
    var trackBtns = DOM.getElementsByTagAndClassName(document, "img", "TrackButton");

    Array.prototype.forEach.call(trackBtns, function (trackBtn) {
        if (!trackBtn || !trackBtn.getAttribute) return;

        if (!trackBtn.getAttribute("lj_subid") && !trackBtn.getAttribute("lj_journalid")) return;

        DOM.addEventListener(trackBtn, "click",
                             ESN.trackBtnClickHandler.bindEventListener(trackBtn));
    });
};

ESN.trackBtnClickHandler = function (evt) {
    var trackBtn = evt.target;
    if (! trackBtn || trackBtn.tagName.toLowerCase() != "img") return true;

    Event.stop(evt);

    var btnInfo = {};

    ['arg1', 'arg2', 'etypeid', 'newentry_etypeid', 'newentry_token', 'newentry_subid',
     'journalid', 'subid', 'auth_token'].forEach(function (arg) {
        btnInfo[arg] = trackBtn.getAttribute("lj_" + arg);
    });

    // pop up little dialog to either track by inbox/email or go to more options
    var dlg = document.createElement("div");
    var title = _textDiv("Email me when");
    DOM.addClassName(title, "track_title");
    dlg.appendChild(title);

    var TrackCheckbox = function (title, checked) {
        var checkContainer = document.createElement("div");

        var newCheckbox = document.createElement("input");
        newCheckbox.type = "checkbox";
        newCheckbox.id = "newentrytrack" + Unique.id();
        var newCheckboxLabel = document.createElement("label");
        newCheckboxLabel.setAttribute("for", newCheckbox.id);
        newCheckboxLabel.innerHTML = title;

        checkContainer.appendChild(newCheckbox);
        checkContainer.appendChild(newCheckboxLabel);
        dlg.appendChild(checkContainer);

        newCheckbox.checked = checked ? true : false;

        return newCheckbox;
    };

    // global trackPopup so we can only have one
    if (ESN.trackPopup) {
        ESN.trackPopup.hide();
        ESN.trackPopup = null;
    }

    var saveChangesBtn = document.createElement("input");
    saveChangesBtn.type = "button";
    saveChangesBtn.value = "Save Changes";
    DOM.addClassName(saveChangesBtn, "track_savechanges");

    var trackingNewEntries  = Number(btnInfo['newentry_subid']) ? 1 : 0;
    var trackingNewComments = Number(btnInfo['subid']) ? 1 : 0;

    var newEntryTrackBtn;
    var commentsTrackBtn;

    if (Number(trackBtn.getAttribute("lj_dtalkid"))) {
        // this is a thread tracking button
        // always checked: either because they're subscribed, or because
        // they're going to subscribe.
        commentsTrackBtn = TrackCheckbox("someone replies in this comment thread", 1);
    } else {
        // entry tracking button
        newEntryTrackBtn = TrackCheckbox(LJ_cmtinfo["journal"] + " posts a new entry", trackingNewEntries);
        commentsTrackBtn = TrackCheckbox("someone comments on this post", trackingNewComments);
    }

    DOM.addEventListener(saveChangesBtn, "click", function () {
        ESN.toggleSubscriptions(btnInfo, evt, trackBtn, {
            newEntry: newEntryTrackBtn ? newEntryTrackBtn.checked : false,
            newComments: commentsTrackBtn.checked
        });
        if (ESN.trackPopup) ESN.trackPopup.hide();
    });

    var btnsContainer = document.createElement("div");
    DOM.addClassName(btnsContainer, "track_btncontainer");
    dlg.appendChild(btnsContainer);

    btnsContainer.appendChild(saveChangesBtn);

    var custTrackLink = document.createElement("a");
    custTrackLink.href = trackBtn.parentNode.href;
    btnsContainer.appendChild(custTrackLink);
    custTrackLink.innerHTML = "More Options";
    DOM.addClassName(custTrackLink, "track_moreopts");

    ESN.trackPopup = new LJ_IPPU.showNoteElement(dlg, trackBtn, 0);

    DOM.addEventListener(custTrackLink, "click", function (evt) {
        Event.stop(evt);
        document.location.href = trackBtn.parentNode.href;
        if (ESN.trackPopup) ESN.trackPopup.hide();
        return false;
    });

    return false;
}

// toggles subscriptions
ESN.toggleSubscriptions = function (subInfo, evt, btn, subs) {
    subInfo["subid"] = Number(subInfo["subid"]);
    if ((subInfo["subid"] && ! subs["newComments"])
        || (! subInfo["subid"] && subs["newComments"])) {
        ESN.toggleSubscription(subInfo, evt, btn, "newComments");
    }

    subInfo["newentry_subid"] = Number(subInfo["newentry_subid"]);
    if ((subInfo["newentry_subid"] && ! subs["newEntry"])
        || (! subInfo["newentry_subid"] && subs["newEntry"])) {
            var newentrySubInfo = new Object(subInfo);
            newentrySubInfo["subid"] = Number(btn.getAttribute("lj_newentry_subid"));
            ESN.toggleSubscription(newentrySubInfo, evt, btn, "newEntry");
    }
};

// (Un)subscribes to an event
ESN.toggleSubscription = function (subInfo, evt, btn, sub) {
    var action = "";
    var params = {
        auth_token: sub == "newEntry" ? subInfo.newentry_token : subInfo.auth_token
    };

    if (Number(subInfo.subid)) {
        // subscription exists
        action = "delsub";
        params.subid = subInfo.subid;
    } else {
        // create a new subscription
        action = "addsub";

        var param_keys;
        if (sub == "newEntry") {
            params.etypeid = subInfo.newentry_etypeid;
            param_keys = ["journalid"];
        } else {
            param_keys = ["journalid", "arg1", "arg2", "etypeid"];
        }

        param_keys.forEach(function (param) {
            if (Number(subInfo[param]))
                params[param] = parseInt(subInfo[param]);
        });
    }

    params.action = action;

    var reqInfo = {
        "method": "POST",
        "url":    LiveJournal.getAjaxUrl('esn_subs'),
        "data":   HTTPReq.formEncoded(params)
    };

    var gotInfoCallback = function (info) {
        if (! info) return LJ_IPPU.showNote("Error changing subscription", btn);

        if (info.error) return LJ_IPPU.showNote(info.error, btn);

        if (info.success) {
            if (info.msg)
                LJ_IPPU.showNote(info.msg, btn);

            if (info.subscribed) {
                if (info.subid)
                    DOM.setElementAttribute(btn, "lj_subid", info.subid);
                if (info.newentry_subid)
                    DOM.setElementAttribute(btn, "lj_newentry_subid", info.newentry_subid);

                ["journalid", "arg1", "arg2", "etypeid"].forEach(function (param) {
                    DOM.setElementAttribute(btn, "lj_" + param, 0);
                });

                DOM.setElementAttribute(btn, "title", 'Untrack This');

                // update subthread tracking icons
                var dtalkid = btn.getAttribute("lj_dtalkid");
                if (dtalkid)
                    ESN.updateThreadIcons(dtalkid, "on");
                else // not thread tracking button
                    btn.src = Site.imgprefix + "/btn_tracking.gif";
            } else {
                if (info["event_class"] == "LJ::Event::JournalNewComment")
                    DOM.setElementAttribute(btn, "lj_subid", 0);
                else if (info["event_class"] == "LJ::Event::JournalNewEntry")
                    DOM.setElementAttribute(btn, "lj_newentry_subid", 0);

                ["journalid", "arg1", "arg2", "etypeid"].forEach(function (param) {
                    DOM.setElementAttribute(btn, "lj_" + param, info[param]);
                });

                DOM.setElementAttribute(btn, "title", 'Track This');

                // update subthread tracking icons
                var dtalkid = btn.getAttribute("lj_dtalkid");
                if (dtalkid) {
                    // set state to "off" if no parents tracking this,
                    // otherwise set state to "parent"
                    var state = "off";
                    var parentBtn;
                    var parent_dtalkid = dtalkid;
                    while (parentBtn = ESN.getThreadParentBtn(parent_dtalkid)) {
                        parent_dtalkid = parentBtn.getAttribute("lj_dtalkid");
                        if (! parent_dtalkid) {
                            log("could not find parent_dtalkid");
                            break;
                        }

                        if (! Number(parentBtn.getAttribute("lj_subid")))
                            continue;
                        state = "parent";
                        break;
                    }

                    ESN.updateThreadIcons(dtalkid, state);
                } else {
                    // not thread tracking button
                    btn.src = Site.imgprefix + "/btn_track.gif";
                }
            }

            if (info.auth_token)
                DOM.setElementAttribute(btn, "lj_auth_token", info.auth_token);
            if (info.newentry_token)
                DOM.setElementAttribute(btn, "lj_newentry_token", info.newentry_token);
        }
    };

    reqInfo.onData = gotInfoCallback;
    reqInfo.onError = function (err) { LJ_IPPU.showNote("Error: " + err) };

    HTTPReq.getJSON(reqInfo);
};

// given a dtalkid, find the track button for its parent comment (if any)
ESN.getThreadParentBtn = function (dtalkid) {
    var cmtInfo = LJ_cmtinfo[dtalkid + ""];
    if (! cmtInfo) {
        log("no comment info");
        return null;
    }

    var parent_dtalkid = cmtInfo.parent;
    if (! parent_dtalkid)
        return null;

    return $("lj_track_btn_" + parent_dtalkid);
};

// update all the tracking icons under a parent comment
ESN.updateThreadIcons = function (dtalkid, tracking) {
    var btn = $("lj_track_btn_" + dtalkid);
    if (! btn) {
        log("no button");
        return;
    }

    var cmtInfo = LJ_cmtinfo[dtalkid + ""];
    if (! cmtInfo) {
        log("no comment info");
        return;
    }

    if (Number(btn.getAttribute("lj_subid")) && tracking != "on") {
        // subscription already exists on this button, don't mess with it
        return;
    }

    if (cmtInfo.rc && cmtInfo.rc.length) {
        // update children
        cmtInfo.rc.forEach(function (child_dtalkid) {
            window.setTimeout(function () {
                var state;
                switch (tracking) {
                case "on":
                    state = "parent";
                    break;
                case "off":
                    state = "off";
                    break;
                case "parent":
                    state = "parent";
                    break;
                default:
                    alert("Unknown tracking state " + tracking);
                    break;
                }
                ESN.updateThreadIcons(child_dtalkid, state);
            }, 300);
        });
    }

    // update icon
    var uri;
    switch (tracking) {
        case "on":
            uri = "/btn_tracking.gif";
            break;
        case "off":
            uri = "/btn_track.gif";
            break;
        case "parent":
            uri = "/btn_tracking_thread.gif";
            break;
        default:
            alert("Unknown tracking state " + tracking);
            break;
    }

    btn.src = Site.imgprefix + uri;
};
/*
  IPPU methods:
     init([innerHTML]) -- takes innerHTML as optional argument
     show() -- shows the popup
     hide() -- hides popup
     cancel() -- hides and calls cancel callback

  Content setters:
     setContent(innerHTML) -- set innerHTML
     setContentElement(element) -- adds element as a child of the popup

   Accessors:
     getElement() -- returns popup DIV element
     visible() -- returns whether the popup is visible or not

   Titlebar:
     setTitlebar(show) -- true: show titlebar / false: no titlebar
     setTitle(title) -- sets the titlebar text
     getTitlebarElement() -- returns the titlebar element
     setTitlebarClass(className) -- set the class of the titlebar

   Styling:
     setOverflow(overflow) -- sets ele.style.overflow to overflow
     addClass(className) -- adds class to popup
     removeClass(className) -- removes class to popup

   Browser Hacks:
     setAutoHideSelects(autohide) -- when the popup is shown should it find all the selects
                                on the page and hide them (and show them again) (for IE)

   Positioning/Sizing:
     setLocation(left, top) -- set popup location: will be pixels if units not specified
     setLeft(left) -- set left location
     setTop(top)   -- set top location
     setDimensions(width, height) -- set popup dimensions: will be pixels if units not specified
     setAutoCenter(x, y) -- what dimensions to auto-center
     center() -- centers popup on screen
     centerX() -- centers popup horizontally
     centerY() -- centers popup vertically
     setFixedPosition(fixed) -- should the popup stay fixed on the page when it scrolls?
     centerOnWidget(widget) -- center popup on this widget
     setAutoCenterCallback(callback) -- calls callback with this IPPU instance as a parameter
                                        for auto-centering. Some common built-in class methods
                                        you can use as callbacks are:
                                        IPPU.center
                                        IPPU.centerX
                                        IPPU.centerY

     moveForward(amount) -- increases the zIndex by one or amount if specified
     moveBackward(amount) -- decreases the zIndex by one or amount if specified

   Modality:
     setClickToClose(clickToClose) -- if clickToClose is true, clicking outside of the popup
                                      will close it
     setModal(modality) -- If modality is true, then popup will capture all mouse events
                     and optionally gray out the rest of the page. (overrides clickToClose)
     setOverlayVisible(visible) -- If visible is true, when this popup is on the page it
                                   will gray out the rest of the page if this is modal

   Callbacks:
     setCancelledCallback(callback) -- call this when the dialog is closed through clicking
                                       outside, titlebar close button, etc...
     setHiddenCallback(callback) -- called when the dialog is closed in any fashion

   Fading:
     setFadeIn(fadeIn) -- set whether or not to automatically fade the ippu in
     setFadeOut(fadeOut) -- set whether or not to automatically fade the ippu out
     setFadeSpeed(secs) -- sets fade speed

  Class Methods:
   Handy callbacks:
     IPPU.center
     IPPU.centerX
     IPPU.centerY
   Browser testing:
     IPPU.isIE() -- is the browser internet exploder?
     IPPU.ieSafari() -- is this safari?

////////////////////


ippu.setModalDenialCallback(IPPU.cssBorderFlash);


   private:
    Properties:
     ele -- DOM node of div
     shown -- boolean; if element is in DOM
     autoCenterX -- boolean; auto-center horiz
     autoCenterY -- boolean; auto-center vertical
     fixedPosition -- boolean; stay in fixed position when browser scrolls?
     titlebar -- titlebar element
     title -- string; text to go in titlebar
     showTitlebar -- boolean; whether or not to show titlebar
     content -- DIV containing user's specified content
     clickToClose -- boolean; clicking outside popup will close it
     clickHandlerSetup -- boolean; have we set up the click handlers?
     docOverlay -- DIV that overlays the document for capturing clicks
     modal -- boolean; capture all events and prevent user from doing anything
                       until dialog is dismissed
     visibleOverlay -- boolean; make overlay slightly opaque
     clickHandlerFunc -- function; function to handle document clicks
     resizeHandlerFunc -- function; function to handle document resizing
     autoCenterCallback -- function; what callback to call for auto-centering
     cancelledCallback -- function; called when dialog is cancelled
     setAutoHideSelects -- boolean; autohide all SELECT elements on the page when popup is visible
     hiddenSelects -- array; SELECT elements that have been hidden
     hiddenCallback -- funciton; called when dialog is hidden
     fadeIn, fadeOut, fadeSpeed -- fading settings
     fadeMode -- current fading mode (in, out) if there is fading going on

    Methods:
     updateTitlebar() -- create titlebar if it doesn't exist,
                         hide it if titlebar == false,
                         update titlebar text
     updateContent() -- makes sure all currently specified properties are applied
     setupClickCapture() -- if modal, create document-sized div overlay to capture click events
                            otherwise install document onclick handler
     removeClickHandlers() -- remove overlay, event handlers
     clickHandler() -- event handler for clicks
     updateOverlay() -- if we have an overlay, make sure it's where it should be and (in)visible
                        if it should be
     autoCenter() -- centers popup on screen according to autoCenterX and autoCenterY
     hideSelects() -- hide all select element on page
     showSelects() -- show all selects
     _hide () -- actually hides everything, called by hide(), which does fading if necessary
*/

// this belongs somewhere else:
function changeOpac(id, opacity) {
    var e =  $(id);
    if (e && e.style) {
        var object = e.style;
        if (object) {
            //reduce flicker
            if (IPPU.isSafari() && opacity >= 100)
                opacity = 99.99;

            // IE
            if (object.filters)
                object.filters.alpha.opacity = opacity * 100;

            object.opacity = opacity;
        }
    }
}

IPPU = new Class( Object, {
  setFixedPosition: function (fixed) {
    // no fixed position for IE
    if (IPPU.isIE())
      return;

    this.fixedPosition = fixed;
    this.updateContent();
  },

  clickHandler : function (evt) {
    if (!this.clickToClose) return;
    if (!this.visible()) return;

    evt = Event.prep(evt);
    var target = evt.target;
    // don't do anything if inside the popup
    if (DOM.getAncestorsByClassName(target, "ippu", true).length > 0) return;

    this.cancel();
  },

  setCancelledCallback : function (callback) {
    this.cancelledCallback = callback;
  },

  cancel : function () {
    if (this.cancelledCallback)
      this.cancelledCallback();

    this.hide();
  },

  setHiddenCallback: function (callback) {
    this.hiddenCallback = callback;
  },

  setupClickCapture : function () {
    if (!this.visible() || this.clickHandlerSetup) return;
    if (!this.clickToClose && !this.modal) return;

    this.clickHandlerFunc = this.clickHandler.bindEventListener(this);

    if (this.modal) {
      // create document-sized div to capture events
      if (this.overlay) return; // wtf? shouldn't exist yet

      this.overlay = document.createElement("div");
      this.overlay.style.position = "fixed";
      this.overlay.style.left = "0px";
      this.overlay.style.top = "0px";
      this.overlay.style.margin = "0px";
      this.overlay.style.padding = "0px";
      this.overlay.style.backgroundColor = "#000000";

      this.ele.parentNode.insertBefore(this.overlay, this.ele);
      this.updateOverlay();

      DOM.addEventListener(this.overlay, "click", this.clickHandlerFunc);
    } else {
      // simple document onclick handler
      DOM.addEventListener(document, "click", this.clickHandlerFunc);
    }

    this.clickHandlerSetup = true;
  },

  updateOverlay : function () {
    if (this.overlay) {
      var cd = DOM.getClientDimensions();
      this.overlay.style.width = (cd.x - 1) + "px";
      this.overlay.style.height = (cd.y - 1) + "px";

      if (this.visibleOverlay) {
        this.overlay.backgroundColor = "#000000";
        changeOpac(this.overlay, 0.50);
      } else {
        this.overlay.backgroundColor = "#FFFFFF";
        changeOpac(this.overlay, 0.0);
      }
    }
  },

  resizeHandler : function (evt) {
    this.updateContent();
  },

  removeClickHandlers : function () {
    if (!this.clickHandlerSetup) return;

    var myself = this;
    var handlerFunc = function (evt) {
      myself.clickHandler(evt);
    };

    DOM.removeEventListener(document, "click", this.clickHandlerFunc, false);

    if (this.overlay) {
      DOM.removeEventListener(this.overlay, "click", this.clickHandlerFunc, true);
      this.overlay.parentNode.removeChild(this.overlay);
      this.overlay = undefined;
    }

    this.clickHandlerFunc = undefined;
    this.clickHandlerSetup = false;
  },

  setClickToClose : function (clickToClose) {
    this.clickToClose = clickToClose;

    if (!this.clickHandlerSetup && clickToClose && this.visible()) {
      // popup is already visible, need to set up click handler
      var setupClickCaptureCallback = this.setupClickCapture.bind(this);
      window.setTimeout(setupClickCaptureCallback, 100);
    } else if (!clickToClose && this.clickHandlerSetup) {
      this.removeClickHandlers();
    }

    this.updateContent();
  },

  setModal : function (modal) {
    var changed = (this.modal == modal);

    // if it's modal, we don't want click-to-close
    if (modal)
      this.setClickToClose(false);

    this.modal = modal;
    if (changed) {
      this.removeClickHandlers();
      this.updateContent();
    }
  },

  setOverlayVisible : function (vis) {
    this.visibleOverlay = vis;
    this.updateContent();
  },

  updateContent : function () {
    this.autoCenter();
    this.updateTitlebar();
    this.updateOverlay();
    if (this.titlebar)
      this.setTitlebarClass(this.titlebar.className);

    var setupClickCaptureCallback = this.setupClickCapture.bind(this);
    window.setTimeout(setupClickCaptureCallback, 100);

    if (this.fixedPosition && this.ele.style.position != "fixed")
      this.ele.style.position = "fixed";
    else if (!this.fixedPosition && this.ele.style.position == "fixed")
      this.ele.style.position = "absolute";
  },

  getTitlebarElement : function () {
    return this.titlebar;
  },

  setTitlebarClass : function (className) {
    if (this.titlebar)
      this.titlebar.className = className;
  },

  setOverflow : function (overflow) {
    if (this.ele)
      this.ele.style.overflow = overflow;
  },

  visible : function () {
    return this.shown;
  },

  setTitlebar : function (show) {
    this.showTitlebar = show;

    if (show) {
      if (!this.titlebar) {
        // titlebar hasn't been created. Create it.
        var tbar = document.createElement("div");
        if (!tbar) return;
        tbar.style.width = "100%";

        if (this.title) tbar.innerHTML = this.title;
        this.ele.insertBefore(tbar, this.content);
        this.titlebar = tbar;

      }
    } else if (this.titlebar) {
      this.ele.removeChild(this.titlebar);
      this.titlebar = false;
    }
  },

  setTitle : function (title) {
    this.title = title;
    this.updateTitlebar();
  },

  updateTitlebar : function() {
    if (this.showTitlebar && this.titlebar && this.title != this.titlebar.innerHTML) {
      this.titlebar.innerHTML = this.title;
    }
  },

  addClass : function (className) {
    DOM.addClassName(this.ele, className);
  },

  removeClass : function (className) {
    DOM.removeClassName(this.ele, className);
  },

  setAutoCenterCallback : function (callback) {
    this.autoCenterCallback = callback;
  },

  autoCenter : function () {
    if (!this.visible || !this.visible()) return;

    if (this.autoCenterCallback) {
      this.autoCenterCallback(this);
      return;
    }

    if (this.autoCenterX)
      this.centerX();

    if (this.autoCenterY)
      this.centerY();
  },

  center : function () {
    this.centerX();
    this.centerY();
  },

  centerOnWidget : function (widget, offsetTop, offsetLeft) {
        offsetTop = offsetTop || 0;
        offsetLeft = offsetLeft || 0;
        this.setAutoCenter(false, false);
        this.setAutoCenterCallback(null);
  var wd = DOM.getAbsoluteDimensions(widget);
    var ed = DOM.getAbsoluteDimensions(this.ele);
    var newleft = (wd.absoluteRight - wd.offsetWidth / 2 - ed.offsetWidth / 2) + offsetLeft;
    var newtop = (wd.absoluteBottom - wd.offsetHeight / 2 - ed.offsetHeight / 2) + offsetTop;

        newleft = newleft < 0 ? 0 : newleft;
        newtop  = newtop  < 0 ? 0 : newtop;
    DOM.setLeft(this.ele, newleft);
    DOM.setTop(this.ele, newtop);
  },

  centerX : function () {
    if (!this.visible || !this.visible()) return;

    var cd = DOM.getClientDimensions();
    var newleft = cd.x / 2 - DOM.getDimensions(this.ele).offsetWidth / 2;

    // If not fixed position, center relative to the left of the page
    if (!this.fixedPosition) {
        var wd = DOM.getWindowScroll();
        newleft += wd.left;
    }

   DOM.setLeft(this.ele, newleft);
  },

  centerY : function () {
    if (!this.visible || !this.visible()) return;

    var cd = DOM.getClientDimensions();
    var newtop = cd.y / 2 - DOM.getDimensions(this.ele).offsetHeight / 2;

    // If not fixed position, center relative to the top of the page
    if (!this.fixedPosition) {
        var wd = DOM.getWindowScroll();
        newtop += wd.top;
    }

    DOM.setTop(this.ele, newtop);
  },

  setAutoCenter : function (autoCenterX, autoCenterY) {
    this.autoCenterX = autoCenterX || false;
    this.autoCenterY = autoCenterY || false;

    if (!autoCenterX && !autoCenterY) {
        this.setAutoCenterCallback(null);
        return;
    }

    this.autoCenter();
  },

  setDimensions : function (width, height) {
    width = width + "";
    height = height + "";
    if (width.match(/^\d+$/)) width += "px";
    if (height.match(/^\d+$/)) height += "px";

    this.ele.style.width  = width;
    this.ele.style.height = height;
  },

  moveForward : function (howMuch) {
      if (!howMuch) howMuch = 1;
      if (! this.ele) return;

      this.ele.style.zIndex += howMuch;
  },

  moveBackward : function (howMuch) {
      if (!howMuch) howMuch = 1;
      if (! this.ele) return;

      this.ele.style.zIndex -= howMuch;
  },

  setLocation : function (left, top) {
      this.setLeft(left);
      this.setTop(top);
  },

  setTop : function (top) {
    top = top + "";
    if (top.match(/^\d+$/)) top += "px";
    this.ele.style.top = top;
  },

  setLeft : function (left) {
    left = left + "";
    if (left.match(/^\d+$/)) left += "px";
    this.ele.style.left = left;
  },

  getElement : function () {
    return this.ele;
  },

  setContent : function (html) {
    this.content.innerHTML = html;
  },

  setContentElement : function (element) {
      // remove child nodes
      while (this.content.firstChild) {
          this.content.removeChild(this.content.firstChild);
      };

    this.content.appendChild(element);
  },

  setFadeIn : function (fadeIn) {
      this.fadeIn = fadeIn;
  },

  setFadeOut : function (fadeOut) {
      this.fadeOut = fadeOut;
  },

  setFadeSpeed : function (fadeSpeed) {
      this.fadeSpeed = fadeSpeed;
  },

  show : function () {
    this.shown = true;

    if (this.fadeIn) {
        var opp = 0.01;

        changeOpac(this.ele, opp);
    }

    document.body.appendChild(this.ele);
    this.ele.style.position = "absolute";
    if (this.autoCenterX || this.autoCenterY) this.center();

    this.updateContent();

    if (!this.resizeHandlerFunc) {
      this.resizeHandlerFunc = this.resizeHandler.bindEventListener(this);
      DOM.addEventListener(window, "resize", this.resizeHandlerFunc, false);
    }

    if (this.fadeIn)
        this.fade("in");

    this.hideSelects();
  },

  fade : function (mode, callback) {
      var opp;
      var delta;

      var steps = 10.0;

      if (mode == "in") {
          delta = 1 / steps;
          opp = 0.1;
      } else {
          if (this.ele.style.opacity)
          opp = finiteFloat(this.ele.style.opacity);
          else
          opp = 0.99;

          delta = -1 / steps;
      }

      var fadeSpeed = this.fadeSpeed;
      if (!fadeSpeed) fadeSpeed = 1;

      var fadeInterval = steps / fadeSpeed * 5;

      this.fadeMode = mode;

      var self = this;
      var fade = function () {
          opp += delta;

          // did someone start a fade in the other direction? if so,
          // cancel this fade
          if (self.fadeMode && self.fadeMode != mode) {
              if (callback)
                  callback.call(self, []);

              return;
          }

          if (opp <= 0.1) {
              if (callback)
                  callback.call(self, []);

              self.fadeMode = null;

              return;
          } else if (opp >= 1.0) {
              if (callback)
                  callback.call(self, []);

              self.fadeMode = null;

              return;
          } else {
              changeOpac(self.ele, opp);
              window.setTimeout(fade, fadeInterval);
          }
      };

      fade();
  },

  hide : function () {
    if (! this.visible()) return;

    if (this.fadeOut && this.ele) {
        this.fade("out", this._hide.bind(this));
    } else {
        this._hide();
    }
  },

  _hide : function () {
    if (this.hiddenCallback)
      this.hiddenCallback();

    this.shown = false;
    this.removeClickHandlers();

    if (this.ele)
    document.body.removeChild(this.ele);

    if (this.resizeHandlerFunc)
      DOM.removeEventListener(window, "resize", this.resizeHandlerFunc);

    this.showSelects();
  },

  // you probably want this for IE being dumb
  // (IE thinks select elements are cool and puts them in front of every element on the page)
  setAutoHideSelects : function (autohide) {
    this.autoHideSelects = autohide;
    this.updateContent();
  },

  hideSelects : function () {
    if (!this.autoHideSelects || !IPPU.isIE()) return;
    var sels = document.getElementsByTagName("select");
    var ele;
    for (var i = 0; i < sels.length; i++) {
      ele = sels[i];
      if (!ele) continue;

      // if this element is inside the ippu, skip it
      if (DOM.getAncestorsByClassName(ele, "ippu", true).length > 0) continue;

      if (ele.style.visibility != 'hidden') {
        ele.style.visibility = 'hidden';
        this.hiddenSelects.push(ele);
      }
    }
  },

  showSelects : function () {
    if (! this.autoHideSelects) return;
    var ele;
    while (ele = this.hiddenSelects.pop())
      ele.style.visibility = '';
  },

  init: function (html) {
    var ele = document.createElement("div");
    this.ele = ele;
    this.shown = false;
    this.autoCenterX = false;
    this.autoCenterY = false;
    this.titlebar = null;
    this.title = "";
    this.showTitlebar = false;
    this.clickToClose = false;
    this.modal = false;
    this.clickHandlerSetup = false;
    this.docOverlay = false;
    this.visibleOverlay = false;
    this.clickHandlerFunc = false;
    this.resizeHandlerFunc = false;
    this.fixedPosition = false;
    this.autoCenterCallback = null;
    this.cancelledCallback = null;
    this.autoHideSelects = false;
    this.hiddenCallback = null;
    this.fadeOut = false;
    this.fadeIn = false;
    this.hiddenSelects = [];
    this.fadeMode = null;

    ele.style.position = "absolute";
    ele.style.zIndex   = "1000";

    // plz don't remove thx
    DOM.addClassName(ele, "ippu");

    // create DIV to hold user's content
    this.content = document.createElement("div");

    this.content.innerHTML = html;

    this.ele.appendChild(this.content);
  }
});

// class methods
IPPU.center = function (obj) {
  obj.centerX();
  obj.centerY();
};

IPPU.centerX = function (obj) {
  obj.centerX();
};

IPPU.centerY = function (obj) {
  obj.centerY();
};

IPPU.isIE = function () {
    var UA = navigator.userAgent.toLowerCase();
    if (UA.indexOf('msie') != -1) return true;
    return false;
};

IPPU.isSafari = function () {
    var UA = navigator.userAgent.toLowerCase();
    if (UA.indexOf('safari') != -1) return true;
    return false;
};
LJ_IPPU = new Class ( IPPU, {
  init: function(title) {
    if (!title)
      title = "";

    LJ_IPPU.superClass.init.apply(this, []);

    this.uniqId = this.generateUniqId();
    this.cancelThisFunc = this.cancel.bind(this);

    this.setTitle(title);
    this.setTitlebar(true);
    this.setTitlebarClass("lj_ippu_titlebar");

    this.addClass("lj_ippu");

    this.setAutoCenterCallback(IPPU.center);
    this.setDimensions(400, "auto");
    this.setOverflow("hidden");

    this.setFixedPosition(true);
    this.setClickToClose(true);
    this.setAutoHideSelects(true);
  },

  setTitle: function (title) {
    var titlebarContent = "\
      <div style='float:right; padding-right: 8px'>" +
      "<img src='" + Site.imgprefix + "/CloseButton.gif' width='15' height='15' id='" + this.uniqId + "_cancel' /></div>" + title;

    LJ_IPPU.superClass.setTitle.apply(this, [titlebarContent]);
  },

  generateUniqId: function() {
    var theDate = new Date();
    return "lj_ippu_" + theDate.getHours() + theDate.getMinutes() + theDate.getMilliseconds();
  },

  show: function() {
    LJ_IPPU.superClass.show.apply(this);
    var setupCallback = this.setup_lj_ippu.bind(this);
    window.setTimeout(setupCallback, 300);
  },

  setup_lj_ippu: function (evt) {
    var cancelCallback = this.cancelThisFunc;
    DOM.addEventListener($(this.uniqId + "_cancel"), "click", cancelCallback, true);
  },

  hide: function() {
    DOM.removeEventListener($(this.uniqId + "_cancel"), "click", this.cancelThisFunc, true);
    LJ_IPPU.superClass.hide.apply(this);
  }
} );

// Class method to show a popup to show a note to the user
// note = message to show
// underele = element to display the note underneath
LJ_IPPU.showNote = function (note, underele, timeout, style) {
    var noteElement = document.createElement("div");
    noteElement.innerHTML = note;

    return LJ_IPPU.showNoteElement(noteElement, underele, timeout, style);
};

LJ_IPPU.showErrorNote = function (note, underele, timeout) {
    return LJ_IPPU.showNote(note, underele, timeout, "ErrorNote");
};

LJ_IPPU.showNoteElement = function (noteEle, underele, timeout, style) {
    var notePopup = new IPPU();
    notePopup.init();

    var inner = document.createElement("div");
    DOM.addClassName(inner, "Inner");
    inner.appendChild(noteEle);
    notePopup.setContentElement(inner);

    notePopup.setTitlebar(false);
    notePopup.setFadeIn(true);
    notePopup.setFadeOut(true);
    notePopup.setFadeSpeed(4);
    notePopup.setDimensions("auto", "auto");
    if (!style) style = "Note";
    notePopup.addClass(style);

    var dim;
    if (underele) {
        // pop up the box right under the element
        dim = DOM.getAbsoluteDimensions(underele);
        if (!dim) return;
    }

    var bounds = DOM.getClientDimensions();
    if (!bounds) return;

    if (!dim) {
        // no element specified to pop up on, show in the middle
        // notePopup.setModal(true);
        // notePopup.setOverlayVisible(true);
        notePopup.setAutoCenter(true, true);
        notePopup.show();
    } else {
        // default is to auto-center, don't want that
        notePopup.setAutoCenter(false, false);
        notePopup.setLocation(dim.absoluteLeft, dim.absoluteBottom + 4);
        notePopup.show();

        var popupBounds = DOM.getAbsoluteDimensions(notePopup.getElement());
        if (popupBounds.absoluteRight > bounds.x) {
            notePopup.setLocation(bounds.x - popupBounds.offsetWidth - 30, dim.absoluteBottom + 4);
        }
    }

    notePopup.setClickToClose(true);
    notePopup.moveForward();

    if (! defined(timeout)) {
        timeout = 5000;
    }

    if (timeout) {
        window.setTimeout(function () {
            if (notePopup)
                notePopup.hide();
        }, timeout);
    }

    return notePopup;
};

LJ_IPPU.textPrompt = function (title, prompt, callback) {
    title += '';
    var notePopup = new LJ_IPPU(title);

    var inner = document.createElement("div");
    DOM.addClassName(inner, "ljippu_textprompt");

    // label
    if (prompt)
        inner.appendChild(_textDiv(prompt));

    // text field
    var field = document.createElement("textarea");
    DOM.addClassName(field, "htmlfield");
    field.cols = 40;
    field.rows = 5;
    inner.appendChild(field);

    // submit btn
    var btncont = document.createElement("div");
    DOM.addClassName(btncont, "submitbtncontainer");
    var btn = document.createElement("input");
    DOM.addClassName(btn, "submitbtn");
    btn.type = "button";
    btn.value = "Insert";
    btncont.appendChild(btn);
    inner.appendChild(btncont);

    notePopup.setContentElement(inner);

    notePopup.setAutoCenter(true, true);
    notePopup.setDimensions("60%", "auto");
    notePopup.show();
    field.focus();

    DOM.addEventListener(btn, "click", function (e) {
        notePopup.hide();
        if (callback)
            callback.apply(null, [field.value]);
    });
}
// LiveJournal javascript standard interface routines

// create a little animated hourglass at (x,y) with a unique-ish ID
// returns the element created
Hourglass = new Class( Object, {
  init: function(widget, classname) {
    this.ele = document.createElement("img");
    if (!this.ele) return;

    var imgprefix = Site ? Site.imgprefix : '';

    this.ele.src = imgprefix ? imgprefix + "/hourglass.gif" : "/img/hourglass.gif";
    this.ele.style.position = "absolute";

    DOM.addClassName(this.ele, classname);

    if (widget)
      this.hourglass_at_widget(widget);
  },

  hourglass_at: function (x, y) {
    this.ele.width = 17;
    this.ele.height = 17;
    this.ele.style.top = (y - 8) + "px";
    this.ele.style.left = (x - 8) + "px";

    // unique ID
    this.ele.id = "lj_hourglass" + x + "." + y;

    document.body.appendChild(this.ele);
  },

  add_class_name: function (classname) {
      if (this.ele)
      DOM.addClassName(this.ele, classname);
  },

  hourglass_at_widget: function (widget) {
    var dim = DOM.getAbsoluteDimensions(widget);
    var x = dim.absoluteLeft;
    var y = dim.absoluteTop;
    var w = dim.absoluteRight - x;
    var h = dim.absoluteBottom - y;
    if (w && h) {
      x += w/2;
      y += h/2;
    }
    this.hourglass_at(x, y);
  },

  hide: function () {
    if (this.ele) {
      try {
        document.body.removeChild(this.ele);
      } catch (e) {}
    }
  }

} );
var ContextualPopup = new Object;

ContextualPopup.popupDelay  = 500;
ContextualPopup.hideDelay   = 250;
ContextualPopup.disableAJAX = false;
ContextualPopup.debug       = false;

ContextualPopup.cachedResults   = {};
ContextualPopup.currentRequests = {};
ContextualPopup.mouseInTimer    = null;
ContextualPopup.mouseOutTimer   = null;
ContextualPopup.currentId       = null;
ContextualPopup.hourglass       = null;
ContextualPopup.elements        = {};

ContextualPopup.setup = function () {
    // don't do anything if no remote
    if (!Site || !Site.ctx_popup) return;

    // attach to all ljuser head icons
    var ljusers = DOM.getElementsByTagAndClassName(document, 'span', "ljuser");

    var userElements = [];
    ljusers.forEach(function (ljuser) {
        var nodes = ljuser.getElementsByTagName("img");
        for (var i=0; i < nodes.length; i++) {
            var node = nodes.item(i);

            // if the parent (a tag with link to userinfo) has userid in its URL, then
            // this is an openid user icon and we should use the userid
            var parent = node.parentNode;
            var userid;
            if (parent && parent.href && (userid = parent.href.match(/\?userid=(\d+)/i)))
                node.userid = userid[1];
            else
                node.username = ljuser.getAttribute("lj:user");

            if (!node.username && !node.userid) continue;

            userElements.push(node);
            DOM.addClassName(node, "ContextualPopup");

        }
    });

    // attach to all userpics
    var images = document.getElementsByTagName("img") || [];
    Array.prototype.forEach.call(images, function (image) {
        // if the image url matches a regex for userpic urls then attach to it
        if (image.src.match(/userpic\..+\/\d+\/\d+/) ||
            image.src.match(/\/userpic\/\d+\/\d+/)) {
            image.up_url = image.src;
            DOM.addClassName(image, "ContextualPopup");
            userElements.push(image);

        }
    });

    var ctxPopupId = 1;
    userElements.forEach(function (userElement) {
        ContextualPopup.elements[ctxPopupId + ""] = userElement;
        userElement.ctxPopupId = ctxPopupId++;
    });

    DOM.addEventListener(document.body, "mousemove", ContextualPopup.mouseOver.bindEventListener());
}

ContextualPopup.isCtxPopElement = function (ele) {
    return (ele && DOM.getAncestorsByClassName(ele, "ContextualPopup", true).length);
}

ContextualPopup.mouseOver = function (e) {
    var target = e.target;
    var ctxPopupId = target.ctxPopupId;

    // if the ctxpopup class isn't fully loaded and set up yet,
    // skip the event handling for now
    if (!eval("ContextualPopup") || !ContextualPopup.isCtxPopElement) return;

    // did the mouse move out?
    if (!target || !ContextualPopup.isCtxPopElement(target)) {
        if (ContextualPopup.mouseInTimer) {
            window.clearTimeout(ContextualPopup.mouseInTimer);
            ContextualPopup.mouseInTimer = null;
        };

        if (ContextualPopup.ippu) {
            if (ContextualPopup.mouseInTimer || ContextualPopup.mouseOutTimer) return;

            ContextualPopup.mouseOutTimer = window.setTimeout(function () {
                ContextualPopup.mouseOut(e);
            }, ContextualPopup.hideDelay);
            return;
        }
    }

    // we're inside a ctxPopElement, cancel the mouseout timer
    if (ContextualPopup.mouseOutTimer) {
        window.clearTimeout(ContextualPopup.mouseOutTimer);
        ContextualPopup.mouseOutTimer = null;
    }

    if (!ctxPopupId)
    return;

    var cached = ContextualPopup.cachedResults[ctxPopupId + ""];

    // if we don't have cached data background request it
    if (!cached) {
        ContextualPopup.getInfo(target);
    }

    // start timer if it's not running
    if (! ContextualPopup.mouseInTimer && (! ContextualPopup.ippu || (
                                                                      ContextualPopup.currentId &&
                                                                      ContextualPopup.currentId != ctxPopupId))) {
        ContextualPopup.mouseInTimer = window.setTimeout(function () {
            ContextualPopup.showPopup(ctxPopupId);
        }, ContextualPopup.popupDelay);
    }
}

// if the popup was not closed by us catch it and handle it
ContextualPopup.popupClosed = function () {
    ContextualPopup.mouseOut();
}

ContextualPopup.mouseOut = function (e) {
    if (ContextualPopup.mouseInTimer)
        window.clearTimeout(ContextualPopup.mouseInTimer);
    if (ContextualPopup.mouseOutTimer)
        window.clearTimeout(ContextualPopup.mouseOutTimer);

    ContextualPopup.mouseInTimer = null;
    ContextualPopup.mouseOutTimer = null;
    ContextualPopup.currentId = null;

    ContextualPopup.hidePopup();
}

ContextualPopup.showPopup = function (ctxPopupId) {
    if (ContextualPopup.mouseInTimer) {
        window.clearTimeout(ContextualPopup.mouseInTimer);
    }
    ContextualPopup.mouseInTimer = null;

    if (ContextualPopup.ippu && (ContextualPopup.currentId && ContextualPopup.currentId == ctxPopupId)) {
        return;
    }

    ContextualPopup.currentId = ctxPopupId;

    ContextualPopup.constructIPPU(ctxPopupId);

    var ele = ContextualPopup.elements[ctxPopupId + ""];
    var data = ContextualPopup.cachedResults[ctxPopupId + ""];

    if (! ele || (data && data.noshow)) {
        return;
    }

    if (ContextualPopup.ippu) {
        var ippu = ContextualPopup.ippu;
        // default is to auto-center, don't want that
        ippu.setAutoCenter(false, false);

        // pop up the box right under the element
        var dim = DOM.getAbsoluteDimensions(ele);
        if (!dim) return;

        var bounds = DOM.getClientDimensions();
        if (!bounds) return;

        // hide the ippu content element, put it on the page,
        // get its bounds and make sure it's not going beyond the client
        // viewport. if the element is beyond the right bounds scoot it to the left.

        var popEle = ippu.getElement();
        popEle.style.visibility = "hidden";
        ContextualPopup.ippu.setLocation(dim.absoluteLeft, dim.absoluteBottom);

        // put the content element on the page so its dimensions can be found
        ContextualPopup.ippu.show();

        var ippuBounds = DOM.getAbsoluteDimensions(popEle);
        if (ippuBounds.absoluteRight > bounds.x) {
            ContextualPopup.ippu.setLocation(bounds.x - ippuBounds.offsetWidth - 30, dim.absoluteBottom);
        }

        // finally make the content visible
        popEle.style.visibility = "visible";
    }
}

ContextualPopup.constructIPPU = function (ctxPopupId) {
    if (ContextualPopup.ippu) {
        ContextualPopup.ippu.hide();
        ContextualPopup.ippu = null;
    }

    var ippu = new IPPU();
    ippu.init();
    ippu.setTitlebar(false);
    ippu.setFadeOut(true);
    ippu.setFadeIn(true);
    ippu.setFadeSpeed(15);
    ippu.setDimensions("auto", "auto");
    ippu.addClass("ContextualPopup");
    ippu.setCancelledCallback(ContextualPopup.popupClosed);
    ContextualPopup.ippu = ippu;

    ContextualPopup.renderPopup(ctxPopupId);
}

ContextualPopup.renderPopup = function (ctxPopupId) {
    var ippu = ContextualPopup.ippu;

    if (!ippu)
    return;

    if (ctxPopupId) {
        var data = ContextualPopup.cachedResults[ctxPopupId];

        if (!data) {
            ippu.setContent("<div class='Inner'>Loading...</div>");
            return;
        } else if (!data.username || !data.success || data.noshow) {
            ippu.hide();
            return;
        }

        var username = data.display_username;

        var inner = document.createElement("div");
        DOM.addClassName(inner, "Inner");

        var content = document.createElement("div");
        DOM.addClassName(content, "Content");

        var bar = document.createElement("span");
        bar.innerHTML = "&nbsp;| ";

        // userpic
        if (data.url_userpic && data.url_userpic != ContextualPopup.elements[ctxPopupId].src) {
            var userpicContainer = document.createElement("div");
            var userpicLink = document.createElement("a");
            userpicLink.href = data.url_allpics;
            var userpic = document.createElement("img");
            userpic.src = data.url_userpic;
            userpic.width = data.userpic_w;
            userpic.height = data.userpic_h;

            userpicContainer.appendChild(userpicLink);
            userpicLink.appendChild(userpic);
            DOM.addClassName(userpicContainer, "Userpic");

            inner.appendChild(userpicContainer);
        }

        inner.appendChild(content);

        // relation
        var relation = document.createElement("div");
        if (data.is_comm) {
            if (data.is_member)
                relation.innerHTML = "You are a member of " + username;
            else if (data.is_friend)
                relation.innerHTML = "You are watching " + username;
            else
                relation.innerHTML = username;
        } else if (data.is_syndicated) {
            if (data.is_friend)
                relation.innerHTML = "You are subscribed to " + username;
            else
                relation.innerHTML = username;
        } else {
            if (data.is_requester) {
                relation.innerHTML = "This is you";
            } else {
                var label = username + " ";

                if (data.is_friend_of) {
                    if (data.is_friend)
                        label += "is your mutual friend";
                    else
                        label += "lists you as a friend";
                } else {
                    if (data.is_friend)
                        label += "is your friend";
                }

                relation.innerHTML = label;
            }
        }
        DOM.addClassName(relation, "Relation");
        content.appendChild(relation);

        // add site-specific content here
        var extraContent = LiveJournal.run_hook("ctxpopup_extrainfo", data);
        if (extraContent) {
            content.appendChild(extraContent);
        }

        // member of community
        if (data.is_logged_in && data.is_comm) {
            var membership      = document.createElement("span");
            var membershipLink  = document.createElement("a");

            var membership_action = data.is_member ? "leave" : "join";

            if (data.is_member) {
                membershipLink.href = data.url_leavecomm;
                membershipLink.innerHTML = "Leave";
            } else {
                membershipLink.href = data.url_joincomm;
                membershipLink.innerHTML = "Join community";
            }

            if (!ContextualPopup.disableAJAX) {
                DOM.addEventListener(membershipLink, "click", function (e) {
                    Event.prep(e);
                    Event.stop(e);
                    return ContextualPopup.changeRelation(data, ctxPopupId, membership_action, e); });
            }

            membership.appendChild(membershipLink);
            content.appendChild(membership);
        }

        // send message
        var message;
        if (data.is_logged_in && data.is_person && ! data.is_requester && data.url_message) {
            message = document.createElement("span");

            var sendmessage = document.createElement("a");
            sendmessage.href = data.url_message;
            sendmessage.innerHTML = "Send message";

            message.appendChild(sendmessage);
            content.appendChild(message);
        }

        // friend
        var friend;
        if (data.is_logged_in && ! data.is_requester) {
            friend = document.createElement("span");

            if (! data.is_friend) {
                // add friend link
                var addFriend = document.createElement("span");
                var addFriendLink = document.createElement("a");
                addFriendLink.href = data.url_addfriend;

                if (data.is_comm)
                    addFriendLink.innerHTML = "Watch community";
                else if (data.is_syndicated)
                    addFriendLink.innerHTML = "Subscribe to feed";
                else
                    addFriendLink.innerHTML = "Add friend";

                addFriend.appendChild(addFriendLink);
                DOM.addClassName(addFriend, "AddFriend");

                if (!ContextualPopup.disableAJAX) {
                    DOM.addEventListener(addFriendLink, "click", function (e) {
                        Event.prep(e);
                        Event.stop(e);
                        return ContextualPopup.changeRelation(data, ctxPopupId, "addFriend", e); });
                }

                friend.appendChild(addFriend);
            } else {
                // remove friend link (omg!)
                var removeFriend = document.createElement("span");
                var removeFriendLink = document.createElement("a");
                removeFriendLink.href = data.url_addfriend;

                if (data.is_comm)
                    removeFriendLink.innerHTML = "Stop watching";
                else if (data.is_syndicated)
                    removeFriendLink.innerHTML = "Unsubscribe";
                else
                    removeFriendLink.innerHTML = "Remove friend";

                removeFriend.appendChild(removeFriendLink);
                DOM.addClassName(removeFriend, "RemoveFriend");

                if (!ContextualPopup.disableAJAX) {
                    DOM.addEventListener(removeFriendLink, "click", function (e) {
                        Event.stop(e);
                        return ContextualPopup.changeRelation(data, ctxPopupId, "removeFriend", e); });
                }

                friend.appendChild(removeFriend);
            }

            DOM.addClassName(relation, "FriendStatus");
        }

        // add a bar between stuff if we have community actions
        if ((data.is_logged_in && data.is_comm) || (message && friend))
            content.appendChild(document.createElement("br"));

        if (friend)
            content.appendChild(friend);

        if ((data.is_person || data.is_comm) && !data.is_requester && data.can_receive_vgifts) {
            var vgift = document.createElement("span");

            var sendvgift = document.createElement("a");
            sendvgift.href = window.Site.siteroot + "/shop/vgift.bml?to=" + data.username;
            sendvgift.innerHTML = "Send a virtual gift";

            vgift.appendChild(sendvgift);

            if (friend)
                content.appendChild(document.createElement("br"));

            content.appendChild(vgift);
        }

        // break
        if ((data.is_logged_in && !data.is_requester) || vgift) content.appendChild(document.createElement("br"));

        // view label
        var viewLabel = document.createElement("span");
        viewLabel.innerHTML = "View: ";
        content.appendChild(viewLabel);

        // journal
        if (data.is_person || data.is_comm || data.is_syndicated) {
            var journalLink = document.createElement("a");
            journalLink.href = data.url_journal;

            if (data.is_person)
                journalLink.innerHTML = "Journal";
            else if (data.is_comm)
                journalLink.innerHTML = "Community";
            else if (data.is_syndicated)
                journalLink.innerHTML = "Feed";

            content.appendChild(journalLink);
            content.appendChild(bar.cloneNode(true));
        }

        // profile
        var profileLink = document.createElement("a");
        profileLink.href = data.url_profile;
        profileLink.innerHTML = "Profile";
        content.appendChild(profileLink);

        // clearing div
        var clearingDiv = document.createElement("div");
        DOM.addClassName(clearingDiv, "ljclear");
        clearingDiv.innerHTML = "&nbsp;";
        content.appendChild(clearingDiv);

        ippu.setContentElement(inner);
    }
}

// ajax request to change relation
ContextualPopup.changeRelation = function (info, ctxPopupId, action, evt) {
    if (!info) return true;

    var postData = {
        "target": info.username,
        "action": action
    };

    // get the authtoken
    var authtoken = info[action + "_authtoken"];
    if (!authtoken) log("no auth token for action" + action);
    postData.auth_token = authtoken;

    // needed on journal subdomains
    var url = LiveJournal.getAjaxUrl("changerelation");

    // callback from changing relation request
    var changedRelation = function (data) {
        if (ContextualPopup.hourglass) ContextualPopup.hideHourglass();

        if (data.error) {
            ContextualPopup.showNote(data.error, ctxPopupId);
            return;
        }

        if (data.note)
        ContextualPopup.showNote(data.note, ctxPopupId);

        if (!data.success) return;

        if (ContextualPopup.cachedResults[ctxPopupId + ""]) {
            var updatedProps = ["is_friend", "is_member"];
            updatedProps.forEach(function (prop) {
                ContextualPopup.cachedResults[ctxPopupId + ""][prop] = data[prop];
            });
        }

        // if the popup is up, reload it
        ContextualPopup.renderPopup(ctxPopupId);
    };

    var opts = {
        "data": HTTPReq.formEncoded(postData),
        "method": "POST",
        "url": url,
        "onError": ContextualPopup.gotError,
        "onData": changedRelation
    };

    // do hourglass at mouse coords
    var mouseCoords = DOM.getAbsoluteCursorPosition(evt);
    if (!ContextualPopup.hourglass && mouseCoords) {
        ContextualPopup.hourglass = new Hourglass();
        ContextualPopup.hourglass.init(null, "lj_hourglass");
        ContextualPopup.hourglass.add_class_name("ContextualPopup"); // so mousing over hourglass doesn't make ctxpopup think mouse is outside
        ContextualPopup.hourglass.hourglass_at(mouseCoords.x, mouseCoords.y);
    }

    HTTPReq.getJSON(opts);

    return false;
}

// create a little popup to notify the user of something
ContextualPopup.showNote = function (note, ctxPopupId) {
    var ele;

    if (ContextualPopup.ippu) {
        // pop up the box right under the element
        ele = ContextualPopup.ippu.getElement();
    } else {
        if (ctxPopupId) {
            var ele = ContextualPopup.elements[ctxPopupId + ""];
        }
    }

    LJ_IPPU.showNote(note, ele);
}

ContextualPopup.hidePopup = function (ctxPopupId) {
    if (ContextualPopup.hourglass) ContextualPopup.hideHourglass();

    // destroy popup for now
    if (ContextualPopup.ippu) {
        ContextualPopup.ippu.hide();
        ContextualPopup.ippu = null;
    }
}

// do ajax request of user info
ContextualPopup.getInfo = function (target) {
    var ctxPopupId = target.ctxPopupId;
    var username = target.username;
    var userid = target.userid;
    var up_url = target.up_url;

    if (!ctxPopupId)
    return;

    if (ContextualPopup.currentRequests[ctxPopupId + ""]) {
        return;
    }

    ContextualPopup.currentRequests[ctxPopupId] = 1;

    if (!username) username = "";
    if (!userid) userid = 0;
    if (!up_url) up_url = "";

    var params = HTTPReq.formEncoded ({
        "user": username,
            "userid": userid,
            "userpic_url": up_url,
            "mode": "getinfo"
    });

    // needed on journal subdomains
    var url = LiveJournal.getAjaxUrl("ctxpopup");
    var url = Site.currentJournal ? "/" + Site.currentJournal + "/__rpc_ctxpopup" : "/__rpc_ctxpopup";

    // got data callback
    var gotInfo = function (data) {
        if (ContextualPopup && ContextualPopup.hourglass) ContextualPopup.hideHourglass();

        ContextualPopup.cachedResults[ctxPopupId] = data;

        if (data.error) {
            if (data.noshow) return;

            ContextualPopup.showNote(data.error, ctxPopupId);
            return;
        }

        if (data.note)
        ContextualPopup.showNote(data.note, data.ctxPopupId);

        ContextualPopup.currentRequests[ctxPopupId] = null;

        ContextualPopup.renderPopup(ctxPopupId);

        // expire cache after 5 minutes
        setTimeout(function () {
            ContextualPopup.cachedResults[ctxPopupId] = null;
        }, 60 * 1000);
    };

    HTTPReq.getJSON({
        "url": url,
            "method" : "GET",
            "data": params,
            "onData": gotInfo,
            "onError": ContextualPopup.gotError
            });
}

ContextualPopup.hideHourglass = function () {
    if (ContextualPopup.hourglass) {
        ContextualPopup.hourglass.hide();
        ContextualPopup.hourglass = null;
    }
}

ContextualPopup.gotError = function (err) {
    if (ContextualPopup.hourglass) ContextualPopup.hideHourglass();

    if (ContextualPopup.debug)
        ContextualPopup.showNote("Error: " + err);
}

// when page loads, set up contextual popups
LiveJournal.register_hook("page_load", ContextualPopup.setup);


Expander = function(){
    this.__caller__;    // <a> HTML element from where Expander was called
    this.url;           // full url of thread to be expanded
    this.id;            // id of the thread
    this.onclick;   
    this.stored_caller;
    this.iframe;        // iframe, where the thread will be loaded
    this.is_S1;         // bool flag, true == journal is in S1, false == in S2
}
Expander.Collection={};
Expander.make = function(el,url,id,is_S1){
    var local = (new Expander).set({__caller__:el,url:url.replace(/#.*$/,''),id:id,is_S1:!!is_S1});
    local.get();
}

Expander.prototype.set = function(options){
    for(var opt in options){
        this[opt] = options[opt];
    }
    return this;
}

Expander.prototype.getCanvas = function(id,context){
    return context.document.getElementById('ljcmt'+id); 
}

Expander.prototype.parseLJ_cmtinfo = function(context,callback){
    var map={}, node, j;
    var LJ = context.LJ_cmtinfo;
    if(!LJ)return false;
    for(j in LJ){
        if(/^\d*$/.test(j) && (node = this.getCanvas(j,context))){
            map[j] = {info:LJ[j],canvas:node};
            if(typeof callback == 'function'){
                callback(j,map[j]);     
            }
        }
    }
    return map; 
}

Expander.prototype.loadingStateOn = function(){
    this.stored_caller = this.__caller__.cloneNode(true); 
    this.__caller__.setAttribute('already_clicked','already_clicked');
    this.onclick = this.__caller__.onclick;
    this.__caller__.onclick = function(){return false;}
    this.__caller__.style.color = '#ccc';
}

Expander.prototype.loadingStateOff = function(){
    if(this.__caller__){
        // actually, the <a> element is removed from main window by
        // copying comment from ifame, so this code is not executed (?)
        this.__caller__.removeAttribute('already_clicked','already_clicked');
        if(this.__caller__.parentNode) this.__caller__.parentNode.replaceChild(this.stored_caller,this.__caller__);
    }
    var obj = this;
    // When frame is removed immediately, IE raises an error sometimes
    window.setTimeout(function(){obj.killFrame()},100);
}

Expander.prototype.killFrame = function(){
    document.body.removeChild(this.iframe); 
}

Expander.prototype.isFullComment = function(comment){
    return !!Number(comment.info.full);
}

Expander.prototype.killDuplicate = function(comments){
    var comment;
    var id,id_,el,el_;
    for(var j in comments){
        if(!/^\d*$/.test(j))continue;
        el_ = comments[j].canvas;
        id_ = el_.id;
        id = id_.replace(/_$/,'');
        el = document.getElementById(id);
        if(el!=null){
            //in case we have a duplicate;
            el_.parentNode.removeChild(el_);
        }else{
            el_.id = id;
        }
    }
}

Expander.prototype.getS1width = function(canvas){
  var w;
  //TODO:  may be we should should add somie ID to the spacer img instead of searching it
  //yet, this works until we have not changed the spacers url = 'dot.gif');
  var img, imgs, found;
  imgs = canvas.getElementsByTagName('img');
  if(!imgs)return false;    
  for(var j=0;j<imgs.length;j++){
    img=imgs[j];
    if(/dot\.gif$/.test(img.src)){
        found = true;
        break;  
    }
  }
  if(found&&img.width)return Number(img.width);   
  else return false;     
}

Expander.prototype.setS1width = function(canvas,w){
  var img, imgs, found;
  imgs = canvas.getElementsByTagName('img');
  if(!imgs)return false;    
  for(var j=0;j<imgs.length;j++){
    img=imgs[j];
    if(/dot\.gif$/.test(img.src)){
        found = true;
        break;  
    }
  }
  if(found)img.setAttribute('width',w);         
}

Expander.prototype.onLoadHandler = function(iframe){
        var doc = iframe.contentDocument || iframe.contentWindow;
        doc = doc.document||doc;
        var obj = this;
        var win = doc.defaultView||doc.parentWindow;
        var comments_intersection={};
        var comments_page = this.parseLJ_cmtinfo(window);
        var comments_iframe = this.parseLJ_cmtinfo(win,function(id,new_comment){
                                    if(id in comments_page){
                                        comments_page[id].canvas.id = comments_page[id].canvas.id+'_';
                                        comments_intersection[id] = comments_page[id];
                                        // copy comment from iframe to main window if
                                        // 1) the comment is collapsed in main window and is full in iframe
                                        // 2) or this is the root comment of this thread (it may be full in 
                                        //     main window too, it's copied so that to remove "expand" link from it)
                                        if((!obj.isFullComment(comments_page[id]) && obj.isFullComment(new_comment)) || (id===obj.id)){
                                            var w;
                                            if(obj.is_S1){
                                                w =obj.getS1width(comments_page[id].canvas);        
                                            }
                                            comments_page[id].canvas.innerHTML = new_comment.canvas.innerHTML;
                                            if(obj.is_S1 && w!==null){
                                                    obj.setS1width(comments_page[id].canvas,w);
                                            }
                                            //TODO: may be this should be uncommented
                                            //comments_page[id].canvas.className = new_comment.canvas.className;
                                            LJ_cmtinfo[id].full=1;
                                        }
                                    }//if(id in comments_page){
                                });
       this.killDuplicate(comments_intersection);   
       this.loadingStateOff();
       return true;
}


//just for debugging
Expander.prototype.toString = function(){
  return '__'+this.id+'__'; 
}


Expander.prototype.get = function(){
    if(this.__caller__.getAttribute('already_clicked')){
        return false;
    }
    this.loadingStateOn();
    
    var iframe;
    if(/*@cc_on !@*/0){
        // branch for IE
        Expander.Collection[this.id] = this;
        iframe = document.createElement('<iframe onload="Expander.Collection['+this.id+'].onLoadHandler(this)">');
    }else{
        // branch for all other browsers
        iframe = document.createElement('iframe');
        iframe.onload = function(obj){return function(){
                            obj.onLoadHandler(iframe);
                        }}(this);
    }
    iframe.style.height='1px';
    iframe.style.width='1px';
    iframe.style.display = 'none';
    iframe.src = this.url;
    document.body.appendChild(iframe);
    this.iframe=iframe;
    return true;
}
/*
JavaSript Object Notation (JSON)
$Id$

Copyright (c) 2005-2006, Six Apart, Ltd.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.

    * Neither the name of "Six Apart" nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/


JSON = {
    parse: function( string ) {
        string = string.replace( /=/g, "\\u3D" );
        string = string.replace( /\(/g, "\\u28" );
        try {
            return eval( "(" + string + ")" );
        } catch( e ) {}
        return undefined;
    }
}


Boolean.prototype.toJSON = function() {
    return this.toString();
}


Number.prototype.toJSON = function() {
    return isFinite( this ) ? this.toString() : "0";
}


Date.prototype.toJSON = function() {
    return this.toUTCISOString
        ? this.toUTCISOString().toJSON()
        : this.toString().toJSON();
}


String.prototype.toJSON = function() {
    return '"' + this.escapeJS() + '"';
}


RegExp.prototype.toJSON = function() {
    return this.toString().toJSON();
}


Function.prototype.toJSON = function() {
    return this.toString().toJSON();
}


Array.prototype.toJSON = function( root ) {
    // crude recursion detection
    if( !root )
        root = this;
    else if( root == this )
        return "[]";
    
    var out = [ "[" ];
    for( var i = 0; i < this.length; i++ ) {
        if( out.length > 1 )
            out.push( "," );
        if( typeof this[ i ] == "undefined" || this[ i ] == null )
            out.push( "null" );
        else if( !this[ i ].toJSON )
            out.push( "{}" );
        else
            out.push( this[ i ].toJSON( root ) );
    }
    out.push( "]" );
    return out.join( "" );
}


Object.prototype.toJSON = function( root ) {
    // crude recursion detection
    if( !root )
        root = this;
    else if( root == this )
        return "{}";
    
    var out = [ "{" ];
    for( var i in this ) {
        if( typeof this[ i ] == "undefined" ||
            (this.hasOwnProperty && !this.hasOwnProperty( i )) )
            continue;
        if( out.length > 1 )
            out.push( "," );
        out.push( i.toJSON() );
        if( this[ i ] == null )
            out.push( ":null" );
        else if( typeof this[ i ] == "function" )
            continue;
        else if( !this[ i ].toJSON )
            out.push( ":{}" );
        else
            out.push( ":", this[ i ].toJSON( root ) );
    }
    out.push( "}" );
    return out.join( "" );
}
/*
Template - Copyright 2005 Six Apart
$Id: template.js 35 2006-02-18 00:46:55Z mischa $

Copyright (c) 2005, Six Apart, Ltd.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.

    * Neither the name of "Six Apart" nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/


/* core template object */

Template = new Class( Object, {
    beginToken: "[#",
    endToken: "#]",
    
    
    init: function( source ) {
        if( source )
            this.compile( source );
    },
    
    
    compile: function( source ) {
        var statements = [
            "context.open();",
            "with( context.vars ) {"
        ];
        
        var start = 0, end = -this.endToken.length;
        while( start < source.length ) {
            end += this.endToken.length;
            
            // plaintext
            start = source.indexOf( this.beginToken, end );
            if( start < 0 )
                start = source.length;
            if( start > end )
                statements.push( "context.write( ", source.substring( end, start ).toJSON(), " );" );
            start += this.beginToken.length;
            
            // code
            if( start >= source.length )
                break;
            end = source.indexOf( this.endToken, start );
            if( end < 0 )
                throw "Template parsing error: Unable to find matching end token (" + this.endToken + ").";
            var length = (end - start);
            
            // empty tag
            if( length <= 0 )
                continue;
            
            // comment
            else if( length >= 4 &&
                source.charAt( start ) == "-" && source.charAt( start + 1 ) == "-" &&
                source.charAt( end - 1 ) == "-" && source.charAt( end - 2 ) == "-" )
                continue;
            
            // write
            else if( source.charAt( start ) == "=" )
                statements.push( "context.write( ", source.substring( start + 1, end ), " );" );
            
            // filters
            else if( source.charAt( start ) == "|" ) {
                start += 1;

                // find the first whitespace
                var afterfilters = source.substring(start,end).search(/\s/);
                
                var filters;
                if (afterfilters > 0) {
                    // allow pipes or commas to seperate filters
                    // split the string, reverse and rejoin to reverse it
                    filters = source.substring(start,start + afterfilters).replace(/,|\|/g,"").split('');
                    afterfilters += 1; // data starts after whitespace and filter list
                } else {
                    // default to escapeHTML
                    filters = ["h"];
                }
                // we have to do them in reverse order
                filters = filters.reverse();
               
                // start with our original filter number
                var numfilters = filters.length;
                
                // add the text between [#|  #]
                filters.push(source.substring( start + afterfilters, end ));
                
                // adjust each filter into a function call
                // eg. u ( h ( H ( blah ) ) )
                for (i=0; i<numfilters; i++) {
                    filters[i] = "context.f."+filters[i]+"( ";
                    filters.push(" )");
                }
                
                statements.push( "context.write( " + filters.join('') + " );");
            }
            
            // evaluate
            else
                statements.push( source.substring( start, end ) );
        }
        
        statements.push( "} return context.close();" );

        this.exec = new Function( "context", statements.join( "\n" ) );
    },
    
    
    exec: function( context ) {
        return "";
    }
} );


/* static members */

Template.templates = {};


/* context object */

Template.Context = new Class( Object, {
    init: function( vars, templates ) {
        this.vars = vars || {};
        this.templates = templates || Template.templates;
        this.stack = [];
        this.out = [];
        this.f = Template.Filter;
    },
    
    
    include: function( name ) {
        return this.templates[ name ].exec( this );
    },


    write: function() {
        this.out.push.apply( this.out, arguments );
    },


    writeln: function() {
        this.write.apply( this, arguments );
        this.write( "\n" );
    },

    
    clear: function() {
        this.out.length = 0;
    },


    getOutput: function() {
        return this.out.join( "" );
    },
    
    
    open: function() {
        this.stack.push( this.out );
        this.out = [];
    },
    
    
    close: function() {
        var result = this.getOutput();
        this.out = this.stack.pop() || [];
        return result;
    }
   
} );

/* filters */

Template.Filter = {

    // escapeHTML
    h: function(obj) {
        var div = document.createElement('div');
        var textNode = document.createTextNode(obj);
        div.appendChild(textNode);
        return (div.innerHTML);
    },

    // unescapeHTML
    H: function(obj) {
        return (unescape(obj));
    },

    // encodeURL
    u: function(obj) {
        return (escape(obj).replace(/\//g,"%2F"));
    }

};

UserpicSelect = new Class (LJ_IPPU, {
  init: function () {
    UserpicSelect.superClass.init.apply(this, ["Choose Userpic"]);

    this.setDimensions("550px", "441px");

    this.selectedPicid = null;
    this.displayPics = null;
    this.dataLoaded = false;
    this.imgScale = 1;

    this.picSelectedCallback = null;

    var template = new Template( UserpicSelect.top );
    var templates = { body: template };
    this.setContent(template.exec( new Template.Context( {}, templates ) ));
    this.setHiddenCallback(this.hidden.bind(this));
  },

  show: function() {
    UserpicSelect.superClass.show.apply(this, []);

    if (!this.dataLoaded) {
      this.setStatus("Loading...");
      this.loadPics();
      this.dataLoaded = true;
    } else {
      this.redraw();
    }
  },

  // hide the hourglass when window is closed
  hidden: function () {
    if (this.hourglass)
      this.hourglass.hide();
  },

  // set a callback to be called when the "select" button is clicked
  setPicSelectedCallback: function (callback) {
    this.picSelectedCallback = callback;
  },

  // called when the "select" button is clicked
  closeButtonClicked: function (evt) {
      if (this.picSelectedCallback) {
          var selectedKws = [];
          if (this.selectedPicid) {
              var kws = this.pics.pics[this.selectedPicid+""].keywords;
              if (kws && kws.length) selectedKws = kws;
          }

          this.picSelectedCallback(this.selectedPicid, selectedKws);
      }

    this.hide();
  },

  setStatus: function(status) {
      this.setField({'status': status});
  },

  setField: function(vars) {
    var template = new Template( UserpicSelect.dynamic );
    var userpics_template = new Template( UserpicSelect.userpics );

    var templates = {
      body: template,
      userpics: userpics_template
    };

    if (!vars.pics)
      vars.pics = this.pics || {};

    if (!vars.status)
      vars.status = "";

    vars.imgScale = this.imgScale;

    $("ups_dynamic").innerHTML = (template.exec( new Template.Context( vars, templates ) ));

    if (!vars.pics.ids)
      return;

    // we redrew the window so reselect the current selection, if any
    if (this.selectedPicid)
      this.selectPic(this.selectedPicid);

    var ST = new SelectableTable();
    ST.init({
        "table": $("ups_userpics_t"),
            "selectedClass": "ups_selected_cell",
            "selectableClass": "ups_cell",
            "multiple": false,
            "selectableItem": "cell"
            });

    var self = this;

    ST.addWatcher(function (data) {
        var selectedCell = data[0];

        if (!selectedCell) {
            // clear selection
            self.selectPic(null);
        } else {
            // find picid and select it
            var parentCell = DOM.getFirstAncestorByClassName(selectedCell, "ups_cell", true);
            if (!parentCell) return;

            var picid = parentCell.getAttribute("lj_ups:picid");
            if (!picid) return;

            self.selectPic(picid);
        }
    });

    DOM.addEventListener($("ups_closebutton"), "click", this.closeButtonClicked.bindEventListener(this));

    // set up image scaling buttons
    var scalingSizes = [3,2,1];
    var baseSize = 25;
    var scalingBtns = $("ups_scaling_buttons");
    this.scalingBtns = [];

    if (scalingBtns) {
        scalingSizes.forEach(function (scaleSize) {
            var scaleBtn = document.createElement("img");

            scaleBtn.style.width = scaleBtn.width = scaleBtn.style.height = scaleBtn.height = baseSize - scaleSize * 5;

            scaleBtn.src = Site.imgprefix + "/imgscale.png";
            DOM.addClassName(scaleBtn, "ups_scalebtn");

            self.scalingBtns.push(scaleBtn);

            DOM.addEventListener(scaleBtn, "click", function (evt) {
                Event.stop(evt);

                self.imgScale = scaleSize;
                self.scalingBtns.forEach(function (otherBtn) {
                    DOM.removeClassName(otherBtn, "ups_scalebtn_selected");
                });

                DOM.addClassName(scaleBtn, "ups_scalebtn_selected");

                self.redraw();
            });

            scalingBtns.appendChild(scaleBtn);

            if (self.imgScale == scaleSize)
                DOM.addClassName(scaleBtn, "ups_scalebtn_selected");
        });
    }
  },

  kwmenuChange: function(evt) {
    this.selectPic($("ups_kwmenu").value);
  },

  selectPic: function(picid) {
    if (this.selectedPicid) {
        DOM.removeClassName($("ups_upicimg" + this.selectedPicid), "ups_selected");
        DOM.removeClassName($("ups_cell" + this.selectedPicid), "ups_selected_cell");
    }

    this.selectedPicid = picid;

    if (picid) {
        // find the current pic and cell
        var picimg =  $("ups_upicimg" + picid);
        var cell   =  $("ups_cell" + picid);

        if (!picimg || !cell)
            return;

        // hilight the userpic
        DOM.addClassName(picimg, "ups_selected");

        // hilight the cell
        DOM.addClassName(cell, "ups_selected_cell");

        // enable the select button
        $("ups_closebutton").disabled = false;

        // select the current selectedPicid in the dropdown
        this.setDropdown();
    } else {
        $("ups_closebutton").disabled = true;
    }
  },

  // filter by keyword/comment
  filterPics: function(evt) {
    var searchbox = $("ups_search");

    if (!searchbox)
      return;

    var filter = searchbox.value.toLocaleUpperCase();
    var pics = this.pics;

    if (!filter) {
      this.setPics(pics);
      return;
    }

    // if there is a filter and there is selected text in the field assume that it's
    // inputcomplete text and ignore the rest of the selection.
    if (searchbox.selectionStart && searchbox.selectionStart > 0)
      filter = searchbox.value.substr(0, searchbox.selectionStart).toLocaleUpperCase();

    var newpics = {
      "pics": [],
      "ids": []
    };

    for (var i=0; i<pics.ids.length; i++) {
      var picid = pics.ids[i];
      var pic = pics.pics[picid];

      if (!pic)
        continue;

      for (var j=0; j < pic.keywords.length; j++) {
        var kw = pic.keywords[j];

        var piccomment = "";
        if (pic.comment)
          piccomment = pic.comment.toLocaleUpperCase();

        if(kw.toLocaleUpperCase().indexOf(filter) != -1 || // matches a keyword
           (piccomment && piccomment.indexOf(filter) != -1) || // matches comment
           (pic.keywords.join(", ").toLocaleUpperCase().indexOf(filter) != -1)) { // matches comma-seperated list of keywords

          newpics.pics[picid] = pic;
          newpics.ids.push(picid);
          break;
        }
      }
    }

    if (this.pics != newpics)
      this.setPics(newpics);

    // if we've filtered down to one pic and we don't currently have a selected pic, select it
    if (newpics.ids.length == 1 && !this.selectedPicid)
      this.selectPic(newpics.ids[0]);
  },

  setDropdown: function(pics) {
    var menu = $("ups_kwmenu");

    for (var i=0; i < menu.length; i++)
      menu.remove(i);

    menu.length = 0;

    if (!pics)
      pics = this.pics;

    if (!pics || !pics.ids)
      return;

    for (var i=0; i < pics.ids.length; i++) {
      var picid = pics.ids[i];
      var pic = pics.pics[picid];

      if (!pic)
        continue;

      var sel = false;
      var self = this;

      pic.keywords.forEach(function (kw) {
          // add to dropdown
          var picopt = document.createElement("option");
          picopt.text = kw;
          picopt.value = picid;

          if (! sel) {
              picopt.selected = self.selectedPicid ? self.selectedPicid == picid : false;
              sel = picopt.selected;
          }

          Try.these(
                    function () { menu.add(picopt, 0); },    // everything else
                    function () { menu.add(picopt, null); }  // IE
                    );
      });
    }
  },

  picsReceived: function(picinfo) {
    if (picinfo && picinfo.alert) { // got an error
      this.handleError(picinfo.alert);
      return;
    }

    if (!picinfo || !picinfo.ids || !picinfo.pics || !picinfo.ids.length)
      return;

    var piccount = picinfo.ids.length;

    // force convert integers to strings
    for (var i=0; i < piccount; i++) {
      var picid = picinfo.ids[i];

      var pic = picinfo.pics[picid];

      if (!pic)
        continue;

      if (pic.comment)
        pic.comment += "";

      for (var j=0; j < pic.keywords.length; j++)
        pic.keywords[j] += "";
    }

    // set default scaling size based on how many pics there are
    if (piccount < 30) {
        this.imgScale = 1;
    } else if (piccount < 60) {
        this.imgScale = 2;
    } else {
        this.imgScale = 3;
    }

    this.pics = picinfo;

    this.setPics(picinfo);
    this.redraw();

    if (this.hourglass)
      this.hourglass.hide();
  },

  redraw: function () {
    this.setStatus();

    if (!this.pics)
      return;

    this.setPics(this.pics);

    if (this.hourglass)
      this.hourglass.hide();

    var keywords = [], comments = [];
    for (var i=0; i < this.pics.ids.length; i++) {
      var picid = this.pics.ids[i];
      var pic = this.pics.pics[picid];

      for (var j=0; j < pic.keywords.length; j++)
        keywords.push(pic.keywords[j]);

      comments.push(pic.comment);
    }

    var searchbox = $("ups_search");
    var compdata = new InputCompleteData(keywords.concat(comments));
    var whut = new InputComplete(searchbox, compdata);

    DOM.addEventListener(searchbox, "keydown",  this.filterPics.bind(this));
    DOM.addEventListener(searchbox, "keyup",    this.filterPics.bind(this));
    DOM.addEventListener(searchbox, "focus",    this.filterPics.bind(this));

    try {
      searchbox.focus();
    } catch(e) {}

    DOM.addEventListener($("ups_kwmenu"), "change", this.kwmenuChange.bindEventListener(this));
  },

  setPics: function(pics) {
    if (this.displayPics == pics)
      return;

    this.displayPics = pics;

    this.setField({'pics': pics});
    this.setDropdown(pics);
  },

  handleError: function(err) {
    log("Error: " + err);
    this.hourglass.hide();
  },

  loadPics: function() {
    this.hourglass = new Hourglass($("ups_userpics"));
    var reqOpts = {};
    reqOpts.url = Site.currentJournal ? "/" + Site.currentJournal + "/__rpc_userpicselect" : "/__rpc_userpicselect";
    reqOpts.onData = this.picsReceived.bind(this);
    reqOpts.onError = this.handleError.bind(this);
    HTTPReq.getJSON(reqOpts);
  }
});

// Templates
UserpicSelect.top = "\
      <div class='ups_search'>\
       <span class='ups_searchbox'>\
         Search: <input type='text' id='ups_search'>\
         Select: <select id='ups_kwmenu'><option value=''></option></select>\
         </span>\
      </div>\
      <div id='ups_dynamic'></div>";

UserpicSelect.dynamic = "\
       [# if (status) { #] <div class='ups_status'>[#| status #]</div> [# } #]\
         <div class='ups_userpics' id='ups_userpics'>\
           [#= context.include( 'userpics' ) #]\
           &nbsp;\
         </div>\
      <div class='ups_closebuttonarea'>\
       <input type='button' id='ups_closebutton' value='Select' disabled='true'  />\
       <span id='ups_scaling_buttons'>\
       </span>\
      </div>";

UserpicSelect.userpics = "\
[# if(pics && pics.ids) { #] \
     <table class='ups_table' cellpadding='0' cellspacing='0' id='ups_userpics_t'> [# \
       var rownum = 0; \
       for (var i=0; i<pics.ids.length; i++) { \
          var picid = pics.ids[i]; \
          var pic = pics.pics[picid]; \
\
          if (!pic) \
            continue; \
\
          var pickws = pic.keywords; \
          if (i%2 == 0) { #] \
            <tr class='ups_row ups_row[#= rownum++ % 2 + 1 #]'> [# } #] \
\
            <td class='ups_cell'  \
                           lj_ups:picid='[#= picid #]' id='ups_cell[#= picid #]'> \
              <div class='ups_container'> \
              <img src='[#= pic.url #]' width='[#= finiteInt(pic.width/imgScale) #]' \
                 height='[#= finiteInt(pic.height/imgScale) #]' id='ups_upicimg[#= picid #]' class='ups_upic' /> \
               </div> \
\
              <b>[#| pickws.join(', ') #]</b> \
             [# if(pic.comment) { #]<br/>[#= pic.comment #][# } #] \
              <div class='ljclear'>&nbsp;</div>\
            </td> \
\
            [# if (i%2 == 1 || i == pics.ids.length - 1) { #] </tr> [# } \
        } #] \
     </table> \
  [# } #] \
";

// Copied here from entry.js
function insertViewThumbs() {
    var lj_userpicselect = $('lj_userpicselect');
    lj_userpicselect.innerHTML = 'View Thumbnails';
}

/* input completion library */

/* TODO:
    -- test on non-US keyboard layouts (too much use of KeyCode)
    -- lazy data model (xmlhttprequest, or generic callbacks)
    -- drop-down menu?
    -- option to disable comma-separated mode (or explicitly ask for it)
*/

/*
  Copyright (c) 2005, Six Apart, Ltd.
  All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:

  * Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.

  * Redistributions in binary form must reproduce the above
  copyright notice, this list of conditions and the following disclaimer
  in the documentation and/or other materials provided with the
  distribution.

  * Neither the name of "Six Apart" nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/


/* ***************************************************************************

  Class: InputCompleteData

  About: An InputComplete object needs a data source to auto-complete
          from.  This is that model.  You can create one from an
          array, or create a lazy version that gets its data over the
          network, on demand.  You will probably not use this class'
          methods directly, as they're called by the InputComplete
          object.

          The closer a word is to the beginning of the array, the more
          likely it will be recommended as the word the user is typing.

          If you pass the string "ignorecase" as the second argument in
          the constructor, then the case of both the user's input and
          the data in the array will be ignored when looking for a match.

  Constructor:

    var model = new InputCompleteData ([ "foo", "bar", "alpha" ]);

*************************************************************************** */

var InputCompleteData = new Class ( Object, {
    init: function () {
        if (arguments[0] instanceof Array) {
            this.source = [];

            // copy the user-provided array (which is sorted most
            // likely to least likely) into our internal form, which
            // is opposite, with most likely at the end.
            var arg = arguments[0];
            for (var i=arg.length-1; i>=0; i--) {
                this.source.length++;
                this.source[this.source.length-1] = arg[i];
            }
        }

        this.ignoreCase = 0;
        if (arguments[1] == "ignorecase") {
            this.ignoreCase = 1;
        }
    },

    // method: given prefix, returns best suffix, or null if no answer
    bestFinish: function (pre) {
        if (! pre || pre.length == 0)
            return null;

        if (! this.source)
            return null;

        var i;
        for (i=this.source.length-1; i>=0; i--) {
            var item = this.source[i];

            var itemToCompare = item;
            var preToCompare = pre;
            if (this.ignoreCase) {
                itemToCompare = item.toLowerCase();
                preToCompare = pre.toLowerCase();
            }

            if (itemToCompare.substring(0, pre.length) == preToCompare) {
                var suff = item.substring(pre.length, item.length);
                return suff;
            }
        }

        return null;
    },

    // method: given a piece of data, learn it, and prioritize it for future completions
    learn: function (word) {
        if (!word) return false;
        if (!this.source) return false;
        this.source[this.source.length++] = word;

        if (this.onModelChange)
            this.onModelChange();
    },

    getItems: function () {
        if (!this.source) return [];

        // return only unique items to caller
        var uniq = [];
        var seen = {};
        for (i=this.source.length-1; i>=0; i--) {
            var item = this.source[i];
            if (! seen[item]) {
                seen[item] = 1;
                uniq.length++;
                uniq[uniq.length - 1] = item;
            }
        }

        return uniq;
    },

    dummy: 1
});

/* ***************************************************************************

  Class: InputComplete

  About:

  Constructor:

*************************************************************************** */

var InputComplete = new Class( Object, {
    init: function () {
        var opts = arguments[0];
        var ele;
        var model;
        var debug;

        if (arguments.length == 1) {
            ele = opts["target"];
            model = opts["model"];
            debug = opts["debug"];
        } else {
            ele = arguments[0];
            model = arguments[1];
            debug = arguments[2];
        }

        this.ele   = ele;
        this.model = model;
        this.debug = debug;

        // no model?  don't setup object.
        if (! ele) {
            this.disabled = true;
            return;
        }

        // return false if auto-complete won't work anyway
        if (! (("selectionStart" in ele) || (document.selection && document.selection.createRange)) ) {
            this.disabled = true;
            return false;
        }

        DOM.addEventListener(ele, "focus",   InputComplete.onFocus.bindEventListener(this));
        DOM.addEventListener(ele, "keydown", InputComplete.onKeyDown.bindEventListener(this));
        DOM.addEventListener(ele, "keyup",   InputComplete.onKeyUp.bindEventListener(this));
        DOM.addEventListener(ele, "blur",    InputComplete.onBlur.bindEventListener(this));
    },

    dbg: function (msg) {
        if (this.debug) {
            this.debug(msg);
        }
    },

    // returns the word currently being typed, or null
    wordInProgress: function () {
        var sel = this.getSelectedRange();
        if (!sel) return null;

        var cidx = sel.selectionStart; // current indx
        var sidx = cidx;  // start of word index
        while (sidx > 0 && this.ele.value.charAt(sidx) != ',') {
            sidx--;
        }
        var skipStartForward = function (chr) { return (chr == "," || chr == " "); }

        while (skipStartForward(this.ele.value.charAt(sidx))) {
            sidx++;
        }

        return this.ele.value.substring(sidx, this.ele.value.length);
    },

    // appends some selected text after the care
    addSelectedText: function (chars) {
        var sel = this.getSelectedRange();
        this.ele.value = this.ele.value + chars;
        this.setSelectedRange(sel.selectionStart, this.ele.value.length);
    },

    moveCaretToEnd: function () {
        var len = this.ele.value.length;
        this.setSelectedRange(len, len);
    },

    getSelectedRange: function () {
        var ret = {};
        var ele = this.ele;

        if ("selectionStart" in ele) {
            ret.selectionStart = ele.selectionStart;
            ret.selectionEnd   = ele.selectionEnd;
            return ret;
        }

        if (document.selection && document.selection.createRange) {
            var range = document.selection.createRange();
            ret.selectionStart = InputComplete.IEOffset(range, "StartToStart");
            ret.selectionEnd   = InputComplete.IEOffset(range, "EndToEnd");
            return ret;
        }

        return null;
    },

    setSelectedRange: function (sidx, eidx) {
        var ele = this.ele;

        // IE
        if (document.selection && document.selection.createRange) {
            ele.focus();
            var sel = document.selection.createRange ();
            sel.moveStart('character', -ele.value.length);
            sel.moveStart('character', sidx);
            sel.moveEnd('character', eidx - sidx);
            sel.select();
            return true;
        }

        // preferred to setting selectionStart and end
        if (ele.setSelectionRange) {
            ele.focus();
            ele.setSelectionRange(sidx, eidx);
            return true;
        }

        // mozilla
        if ("selectionStart" in ele) {
            ele.selectionStart = sidx;
            ele.selectionEnd   = eidx;
            return true;
        }

        return false;
    },

    // returns true if caret is at end of line, or everything to the right
    // of us is selected
    caretAtEndOfNotSelected: function (sel) {
        sel = sel || this.getSelectedRange();
        var len = this.ele.value.length;
        return sel.selectionEnd == len;
    },

    disable: function () {
        this.disabled = true;
    },

    dummy: 1
});

InputComplete.onKeyDown = function (e) {
    if (this.disabled) return;

    var code = e.keyCode || e.which;

    this.dbg("onKeyDown, code="+code+", shift="+e.shiftKey);
    
    // if comma, but not with a shift which would be "<".  (FIXME: what about other keyboards layouts?)
    //FIXME: may be there is a stable cross-browser way to detect so-called other keyboard layouts - but i don't know anything easier than ... (see onKeyUp changes in tis revision)
    /*if ((code == 188 || code == 44) && ! e.shiftKey && this.caretAtEndOfNotSelected()) {
        this.moveCaretToEnd();
        return Event.stop(e);
    }*/

    return true;
};

InputComplete.onKeyUp = function (e) {
    if (this.disabled) return;

    var val = this.ele.value;

    var code = e.keyCode || e.which;
    this.dbg("keyUp = " + code);
    
    
    // ignore tab, backspace, left, right, delete, and enter
    if (code == 9 || code == 8 || code == 37 || code == 39 || code == 46 || code == 13)
       return false;

    var sel = this.getSelectedRange();

    var ss = sel.selectionStart;
    var se = sel.selectionEnd;

    this.dbg("keyUp, got ss="+ss +  ", se="+se+", val.length="+val.length);

    // only auto-complete if we're at the end of the line
    if (se != val.length) return false;

    var chr = String.fromCharCode(code);

    this.dbg("keyUp, got chr="+chr);
    //if (code == 188 || chr == ",") {
    if(/,$/.test(val)){	
        if (! this.caretAtEndOfNotSelected(sel)) {
            return false;
        }

        this.dbg("hit comma! .. value = " + this.ele.value);

        this.ele.value = this.ele.value.replace(/[\s,]+$/, "") + ", ";
        this.moveCaretToEnd();

        return Event.stop(e);
    }


    var inProg = this.wordInProgress();
    if (!inProg) return true;

    var rest = this.model.bestFinish(inProg);

    if (rest && rest.length > 0) {
        this.addSelectedText(rest);
    }
};

InputComplete.onBlur = function (e) {
    if (this.disabled) return;

    var tg = e.target;
    var list = tg.value;

    var noendjunk = list.replace(/[\s,]+$/, "");
    if (noendjunk != list) {
        tg.value = list = noendjunk;
    }

    var tags = list.split(",");
    for (var i =0; i<tags.length; i++) {
        var tag = tags[i].replace(/^\s+/,"").replace(/\s+$/,"");
        if (tag.length) {
            this.model.learn(tag);
        }
    }
};

InputComplete.onFocus = function (e) {
    if (this.disabled) return;
};


InputComplete.IEOffset = function ( range, compareType ) {
    if (this.disabled) return;

    var range2 = range.duplicate();
    range2.collapse( true );
    var parent = range2.parentElement();
    var length = range2.text.length;
    range2.move("character", -parent.value.length);

    var delta = max( 1, finiteInt( length * 0.5 ) );
    range2.collapse( true );
    var offset = 0;
    var steps = 0;

    // bail after 10k iterations in case of borkage
    while( (test = range2.compareEndPoints( compareType, range )) != 0 ) {
        if( test < 0 ) {
            range2.move( "character", delta );
            offset += delta;
        } else {
            range2.move( "character", -delta );
            offset -= delta;
        }
        delta = max( 1, finiteInt( delta * 0.5 ) );
        steps++;
        if( steps > 1000 )
            throw "unable to find textrange endpoint in " + steps + " steps";
    }

    return offset;
};
// datasource base class, the "M" in MVC
// subclass this and override theData to provide your data

DataSource = new Class(Object, {

  init: function (initialData) {
    DataSource.superClass.init.apply(this, arguments);
    this.watchers = [];
    this.theData = defined(initialData) ? initialData : [];
    this.sortField = "";
    this.sortType = "";
    this.sortDesc = false;
  },

  addWatcher: function (callback) {
    this.watchers.add(callback);
  },

  removeWatcher: function (callback) {
    this.watchers.remove(callback);
  },

  // call this if updating data and not using _setData
  _updated: function () {
    this.callWatchers();
  },

  callWatchers: function () {
    for (var i = 0; i < this.watchers.length; i++)
      this.watchers[i].apply(this, [this.data()]);
  },

  setData: function (theData) {
    this.theData = theData;

    if (this.sortField)
      this.sortDataBy(this.sortField, this.sortType, this.sortDesc);

    this._setData(theData);
  },

  _setData: function (theData) {
    this.theData = theData;
    this.callWatchers();
    return theData;
  },

  data: function () {
    return this.theData;
  },

  sortBy: function () {
    return this.sortField;
  },

  sortInverted: function () {
    return this.sortDesc;
  },

  // mimic some array functionality
  push: function (data) {
    this.theData.push(data);
    this.callWatchers();
  },

  pop: function () {
    var val = this.theData.pop();
    this.callWatchers();
    return val;
  },

  indexOf: function (value) {
    return this.theData.indexOf(value);
  },

  remove: function (value) {
    this.theData.remove(value);
    this.callWatchers();
  },

  empty: function () {
    this.theData = [];
    this.callWatchers();
  },

  length: function () {
    return this.theData.length;
  },

  totalLength: function () {
    return this.allData().length;
  },

  allData: function () {
    var theData = this.theData;

    if (this.dataField && theData)
      theData = theData[this.dataField];

    return theData;
  },

  sortDataBy: function (field, type, invert) {
    this.sortField = field;
    this.sortDesc = invert;
    this.sortType = type;

    if (!field || !this.theData || !this.theData.sort)
      return;

    var sorted = this.theData.sort(function (a, b) {
      var ad = a[""+field], bd = b[""+field];
      ad = ad ? ad : "";
      bd = bd ? bd : "";

      switch(type) {

      case "string":
        var aname = ad.toUpperCase(), bname = bd.toUpperCase();

        if (aname < bname)
          return -1;
        else if (aname > bname)
          return 1;
        else
          return 0;

      case "isodate":
        var datA = Date.fromISOString(ad) || new Date(0);
        var datB = Date.fromISOString(bd) || new Date(0);

        return ((datA - datB) || 0);

      default:
      case "numeric":
        return ad - bd;

      }
    });

    if (invert)
      sorted.reverse();

    this._setData(sorted);
  }

});
/*
  This is a datasource you can attach to a table. It will enable
  the selection of rows or cells in the table.

  The data in the datasource is elements that are selected.

  $id:$
*/

SelectableTable = new Class(DataSource, {

    // options:
    //   table: what table element to attach to
    //   selectableClass: if you only want elements with a certain class to be selectable,
    //       specifiy this class with selectableClass
    //   multiple: can more than one thing be selected at once? default is true
    //   selectedClass: class to apply to selected elements
    //   checkboxClass: since there are frequently checkboxes associated with selectable elements,
    //       you can specify the class of your checkboxes to make them stay in sync
    //   selectableItem: What type of elements can be selected. Values are "cell" or "row"
    init: function (opts) {
        SelectableTable.superClass.init.apply(this, []);

        var table = opts.table;
        var selectableClass = opts.selectableClass;
        var multiple = opts.multiple;
        var selectedClass = opts.selectedClass
        var checkboxClass = opts.checkboxClass
        var selectableItem = opts.selectableItem;

        selectableItem = selectableItem == "cell" ? "cell" : "row";

        if (!defined(multiple)) multiple = true;

        this.table = table;
        this.selectableClass = selectableClass;
        this.multiple = multiple;
        this.selectedClass = opts.selectedClass;
        this.checkboxClass = opts.checkboxClass;

        this.selectedElements = [];

        // if it's not a table, die
        if (!table || !table.tagName || table.tagName.toLowerCase() != "table") return null;

        // get selectable items
        var tableElements = table.getElementsByTagName("*");

        var selectableElements;

        if (selectableItem == "cell") {
            selectableElements = DOM.filterElementsByTagName(tableElements, "td");
        } else {
            selectableElements = DOM.filterElementsByTagName(tableElements, "tr");
        }

        var self = this;
        selectableElements.forEach(function(ele) {
            // if selectableClass is defined and this element doesn't have the class, skip it
            if (selectableClass && !DOM.hasClassName(ele, selectableClass)) return;

            // attach click handler to every element inside the element
            var itemElements = ele.getElementsByTagName("*");
            for (var i = 0; i < itemElements.length; i++) {
                self.attachClickHandler(itemElements[i], ele);
            }

            // attach click handler to the element itself
            self.attachClickHandler(ele, ele);
        });
    },

    // stop our handling of this event
    stopHandlingEvent: function (evt) {
        if (!evt) return;

        // w3c
        if (evt.stopPropagation)
        evt.stopPropagation();

        // ie
        try {
            event.cancelBubble = true;
        } catch(e) {}
    },

    // attach a click handler to this element
    attachClickHandler: function (ele, parent) {
        if (!ele) return;

        var self = this;

        var rowClicked = function (evt) {
            // if it was a control-click, they're probably trying to open a new tab or something.
            // let's not handle it
            if (evt.ctrlKey) return false;

            var tagName = ele.tagName.toLowerCase();

            // if this is a link or has an onclick handler,
            // return true and tell other events to return true
            if ((ele.href && tagName != "img") || ele.onclick) {
                self.stopHandlingEvent(evt);
                return true;
            }

            // if this is the child of a link, propagate the event up
            var ancestors = DOM.getAncestors(ele, true);
            for (var i = 0; i < ancestors.length; i++) {
                var ancestor = ancestors[i];
                if (ancestor.href && ancestor.tagName.toLowerCase() != "img") {
                    return true;
                }
            }

            // if this is an input or select element, skip it
            if ((tagName == "select" || tagName == "input") && parent.checkbox != ele) {
                self.stopHandlingEvent(evt);
                return true;
            }

            // toggle selection of this parent element
            if (self.selectedElements.indexOf(parent) != -1) {
                if (self.selectedClass) DOM.removeClassName(parent, self.selectedClass);

                self.selectedElements.remove(parent);
            } else {
                if (self.selectedClass) DOM.addClassName(parent, self.selectedClass);

                if (self.multiple) {
                    self.selectedElements.push(parent);
                } else {
                    if (self.selectedClass && self.selectedElements.length > 0) {
                        var oldParent = self.selectedElements[0];
                        if (oldParent) {
                            DOM.removeClassName(oldParent, self.selectedClass);
                            if (oldParent.checkbox) oldParent.checkbox.checked = "";
                        }
                    }

                    self.selectedElements = [parent];
                }
            }

            // update our data
            self.setData(self.selectedElements);

            // if there's a checkbox associated with this parent, set it's value
            // to the parent selected value
            if (parent.checkbox) parent.checkbox.checked = (self.selectedElements.indexOf(parent) != -1) ? "on" : '';
            if (parent.checkbox == ele) { self.stopHandlingEvent(evt); return true; }

            // always? not sure
            if (evt)
                Event.stop(evt);
        }

        // if this is a checkbox we need to keep in sync, set up its event handler
        if (this.checkboxClass && ele.tagName.toLowerCase() == "input"
            && ele.type == "checkbox" && DOM.hasClassName(ele, this.checkboxClass)) {

            parent.checkbox = ele;

            // override default event handler for the checkbox
            DOM.addEventListener(ele, "click", function (evt) {
                return true;
            });
        }

        // attach a method to the row so other people can programatically
        // select it.
        ele.rowClicked = rowClicked;

        DOM.addEventListener(ele, "click", rowClicked);
    }

});
// x_core.js, X v3.15.2, Cross-Browser.com DHTML Library
// Copyright (c) 2004 Michael Foster, Licensed LGPL (gnu.org)

// global vars still duplicated in xlib.js - I still don't know what I'm going to do about this
var xVersion='3.15.2',xNN4,xOp7,xOp5or6,xIE4Up,xIE4,xIE5,xMac,xUA=navigator.userAgent.toLowerCase();
if (window.opera){
  xOp7=(xUA.indexOf('opera 7')!=-1 || xUA.indexOf('opera/7')!=-1);
  if (!xOp7) xOp5or6=(xUA.indexOf('opera 5')!=-1 || xUA.indexOf('opera/5')!=-1 || xUA.indexOf('opera 6')!=-1 || xUA.indexOf('opera/6')!=-1);
}
else if (document.all && xUA.indexOf('msie')!=-1) {
  xIE4Up=parseInt(navigator.appVersion)>=4;
  xIE4=xUA.indexOf('msie 4')!=-1;
  xIE5=xUA.indexOf('msie 5')!=-1;
}
else if (document.layers) {xNN4=true;}
xMac=xUA.indexOf('mac')!=-1;

function xGetElementById(e) {
  if(typeof(e)!='string') return e;
  if(document.getElementById) e=document.getElementById(e);
  else if(document.all) e=document.all[e];
  else e=null;
  return e;
}
function xParent(e,bNode){
  if (!(e=xGetElementById(e))) return null;
  var p=null;
  if (!bNode && xDef(e.offsetParent)) p=e.offsetParent;
  else if (xDef(e.parentNode)) p=e.parentNode;
  else if (xDef(e.parentElement)) p=e.parentElement;
  return p;
}
function xDef() {
  for(var i=0; i<arguments.length; ++i){if(typeof(arguments[i])=='undefined') return false;}
  return true;
}
function xStr(s) {
  for(var i=0; i<arguments.length; ++i){if(typeof(arguments[i])!='string') return false;}
  return true;
}
function xNum(n) {
  for(var i=0; i<arguments.length; ++i){if(typeof(arguments[i])!='number') return false;}
  return true;
}
function xShow(e) {
  if(!(e=xGetElementById(e))) return;
  if(e.style && xDef(e.style.visibility)) e.style.visibility='visible';
}
function xHide(e) {
  if(!(e=xGetElementById(e))) return;
  if(e.style && xDef(e.style.visibility)) e.style.visibility='hidden';
}
function xZIndex(e,uZ) {
  if(!(e=xGetElementById(e))) return 0;
  if(e.style && xDef(e.style.zIndex)) {
    if(xNum(uZ)) e.style.zIndex=uZ;
    uZ=parseInt(e.style.zIndex);
  }
  return uZ;
}
function xColor(e,sColor) {
  if(!(e=xGetElementById(e))) return '';
  var c='';
  if(e.style && xDef(e.style.color)) {
    if(xStr(sColor)) e.style.color=sColor;
    c=e.style.color;
  }
  return c;
}
function xBackground(e,sColor,sImage) {
  if(!(e=xGetElementById(e))) return '';
  var bg='';
  if(e.style) {
    if(xStr(sColor)) {
      if(!xOp5or6) e.style.backgroundColor=sColor;
      else e.style.background=sColor;
    }
    if(xStr(sImage)) e.style.backgroundImage=(sImage!='')? 'url('+sImage+')' : null;
    if(!xOp5or6) bg=e.style.backgroundColor;
    else bg=e.style.background;
  }
  return bg;
}
function xMoveTo(e,iX,iY) {
  xLeft(e,iX);
  xTop(e,iY);
}
function xLeft(e,iX) {
  if(!(e=xGetElementById(e))) return 0;
  var css=xDef(e.style);
  if (css && xStr(e.style.left)) {
    if(xNum(iX)) e.style.left=iX+'px';
    else {
      iX=parseInt(e.style.left);
      if(isNaN(iX)) iX=0;
    }
  }
  else if(css && xDef(e.style.pixelLeft)) {
    if(xNum(iX)) e.style.pixelLeft=iX;
    else iX=e.style.pixelLeft;
  }
  return iX;
}
function xTop(e,iY) {
  if(!(e=xGetElementById(e))) return 0;
  var css=xDef(e.style);
  if(css && xStr(e.style.top)) {
    if(xNum(iY)) e.style.top=iY+'px';
    else {
      iY=parseInt(e.style.top);
      if(isNaN(iY)) iY=0;
    }
  }
  else if(css && xDef(e.style.pixelTop)) {
    if(xNum(iY)) e.style.pixelTop=iY;
    else iY=e.style.pixelTop;
  }
  return iY;
}
function xPageX(e) {
  if (!(e=xGetElementById(e))) return 0;
  var x = 0;
  while (e) {
    if (xDef(e.offsetLeft)) x += e.offsetLeft;
    e = xDef(e.offsetParent) ? e.offsetParent : null;
  }
  return x;
}
function xPageY(e) {
  if (!(e=xGetElementById(e))) return 0;
  var y = 0;
  while (e) {
    if (xDef(e.offsetTop)) y += e.offsetTop;
    e = xDef(e.offsetParent) ? e.offsetParent : null;
  }
//  if (xOp7) return y - document.body.offsetTop; // v3.14, temporary hack for opera bug 130324
  return y;
}
function xOffsetLeft(e) {
  if (!(e=xGetElementById(e))) return 0;
  if (xDef(e.offsetLeft)) return e.offsetLeft;
  else return 0;
}
function xOffsetTop(e) {
  if (!(e=xGetElementById(e))) return 0;
  if (xDef(e.offsetTop)) return e.offsetTop;
  else return 0;
}
function xScrollLeft(e) {
  var offset=0;
  if (!(e=xGetElementById(e))) {
    if(document.documentElement && document.documentElement.scrollLeft) offset=document.documentElement.scrollLeft;
    else if(document.body && xDef(document.body.scrollLeft)) offset=document.body.scrollLeft;
  }
  else { if (xNum(e.scrollLeft)) offset = e.scrollLeft; }
  return offset;
}
function xScrollTop(e) {
  var offset=0;
  if (!(e=xGetElementById(e))) {
    if(document.documentElement && document.documentElement.scrollTop) offset=document.documentElement.scrollTop;
    else if(document.body && xDef(document.body.scrollTop)) offset=document.body.scrollTop;
  }
  else { if (xNum(e.scrollTop)) offset = e.scrollTop; }
  return offset;
}
function xHasPoint(ele, iLeft, iTop, iClpT, iClpR, iClpB, iClpL) {
  if (!xNum(iClpT)){iClpT=iClpR=iClpB=iClpL=0;}
  else if (!xNum(iClpR)){iClpR=iClpB=iClpL=iClpT;}
  else if (!xNum(iClpB)){iClpL=iClpR; iClpB=iClpT;}
  var thisX = xPageX(ele), thisY = xPageY(ele);
  return (iLeft >= thisX + iClpL && iLeft <= thisX + xWidth(ele) - iClpR &&
          iTop >=thisY + iClpT && iTop <= thisY + xHeight(ele) - iClpB );
}
function xResizeTo(e,uW,uH) {
  xWidth(e,uW);
  xHeight(e,uH);
}
function xWidth(e,uW) {
  if(!(e=xGetElementById(e))) return 0;
  if (xNum(uW)) {
    if (uW<0) uW = 0;
    else uW=Math.round(uW);
  }
  else uW=-1;
  var css=xDef(e.style);
  if(css && xDef(e.offsetWidth) && xStr(e.style.width)) {
    if(uW>=0) xSetCW(e, uW);
    uW=e.offsetWidth;
  }
  else if(css && xDef(e.style.pixelWidth)) {
    if(uW>=0) e.style.pixelWidth=uW;
    uW=e.style.pixelWidth;
  }
  return uW;
}
function xHeight(e,uH) {
  if(!(e=xGetElementById(e))) return 0;
  if (xNum(uH)) {
    if (uH<0) uH = 0;
    else uH=Math.round(uH);
  }
  else uH=-1;
  var css=xDef(e.style);
  if(css && xDef(e.offsetHeight) && xStr(e.style.height)) {
    if(uH>=0) xSetCH(e, uH);
    uH=e.offsetHeight;
  }
  else if(css && xDef(e.style.pixelHeight)) {
    if(uH>=0) e.style.pixelHeight=uH;
    uH=e.style.pixelHeight;
  }
  return uH;
}
function xGetCS(ele,sP){return parseInt(document.defaultView.getComputedStyle(ele,'').getPropertyValue(sP));}
function xSetCW(ele,uW){
  var pl=0,pr=0,bl=0,br=0;
  if(xDef(document.defaultView) && xDef(document.defaultView.getComputedStyle)){
    pl=xGetCS(ele,'padding-left');
    pr=xGetCS(ele,'padding-right');
    bl=xGetCS(ele,'border-left-width');
    br=xGetCS(ele,'border-right-width');
  }
  else if(xDef(ele.currentStyle,document.compatMode)){
    if(document.compatMode=='CSS1Compat'){
      pl=parseInt(ele.currentStyle.paddingLeft);
      pr=parseInt(ele.currentStyle.paddingRight);
      bl=parseInt(ele.currentStyle.borderLeftWidth);
      br=parseInt(ele.currentStyle.borderRightWidth);
    }
  }
  else if(xDef(ele.offsetWidth,ele.style.width)){ // ?
    ele.style.width=uW+'px';
    pl=ele.offsetWidth-uW;
  }
  if(isNaN(pl)) pl=0; if(isNaN(pr)) pr=0; if(isNaN(bl)) bl=0; if(isNaN(br)) br=0;
  var cssW=uW-(pl+pr+bl+br);
  if(isNaN(cssW)||cssW<0) return;
  else ele.style.width=cssW+'px';
}
function xSetCH(ele,uH){
  var pt=0,pb=0,bt=0,bb=0;
  if(xDef(document.defaultView) && xDef(document.defaultView.getComputedStyle)){
    pt=xGetCS(ele,'padding-top');
    pb=xGetCS(ele,'padding-bottom');
    bt=xGetCS(ele,'border-top-width');
    bb=xGetCS(ele,'border-bottom-width');
  }
  else if(xDef(ele.currentStyle,document.compatMode)){
    if(document.compatMode=='CSS1Compat'){
      pt=parseInt(ele.currentStyle.paddingTop);
      pb=parseInt(ele.currentStyle.paddingBottom);
      bt=parseInt(ele.currentStyle.borderTopWidth);
      bb=parseInt(ele.currentStyle.borderBottomWidth);
    }
  }
  else if(xDef(ele.offsetHeight,ele.style.height)){ // ?
    ele.style.height=uH+'px';
    pt=ele.offsetHeight-uH;
  }
  if(isNaN(pt)) pt=0; if(isNaN(pb)) pb=0; if(isNaN(bt)) bt=0; if(isNaN(bb)) bb=0;
  var cssH=uH-(pt+pb+bt+bb);
  if(isNaN(cssH)||cssH<0) return;
  else ele.style.height=cssH+'px';
}
function xClip(e,iTop,iRight,iBottom,iLeft) {
  if(!(e=xGetElementById(e))) return;
  if(e.style) {
    if (xNum(iLeft)) e.style.clip='rect('+iTop+'px '+iRight+'px '+iBottom+'px '+iLeft+'px)';
    else e.style.clip='rect(0 '+parseInt(e.style.width)+'px '+parseInt(e.style.height)+'px 0)';
  }
}
function xClientWidth() {
  var w=0;
  if(xOp5or6) w=window.innerWidth;
  else if(!window.opera && document.documentElement && document.documentElement.clientWidth)
    w=document.documentElement.clientWidth;
  else if(document.body && document.body.clientWidth)
    w=document.body.clientWidth;
  else if(xDef(window.innerWidth,window.innerHeight,document.height)) {
    w=window.innerWidth;
    if(document.height>window.innerHeight) w-=16;
  }
  return w;
}
function xClientHeight() {
  var h=0;
  if(xOp5or6) h=window.innerHeight;
  else if(!window.opera && document.documentElement && document.documentElement.clientHeight)
    h=document.documentElement.clientHeight;
  else if(document.body && document.body.clientHeight)
    h=document.body.clientHeight;
  else if(xDef(window.innerWidth,window.innerHeight,document.width)) {
    h=window.innerHeight;
    if(document.width>window.innerWidth) h-=16;
  }
  return h;
}
function xInnerHtml(e, sHtml) {
  if(!(e=xGetElementById(e))) return '';
  if (xStr(e.innerHTML)) {
    if (xStr(sHtml)) e.innerHTML = sHtml;
    else return e.innerHTML;
  }
}
    var lastDiv;
    lastDiv = 'qrdiv';

    var Site;
    if (! Site) Site = new Object()

    function quickreply(dtid, pid, newsubject) {
        // Mac IE 5.x does not like dealing with
        // nextSibling since it does not support it
        if (xIE4Up && xMac) { return true; }

        // on IE, cancel the bubble of the event up to the page. other
        // browsers don't seem to bubble events up registered this way.
        var ev = window.event;
        if (ev) {
            if (ev.stopPropagation)
               ev.stopPropagation();
            if (ev.cancelBubble != "undefined")
                ev.cancelBubble = true;
        }

        var targetname = "ljqrt" + dtid;

        var ptalkid = xGetElementById('parenttalkid');
        var rto = xGetElementById('replyto');
        var dtid_field = xGetElementById('dtid');
        var qr_div = xGetElementById('qrdiv');
        var cur_div = xGetElementById(targetname);
        var qr_form_div  = xGetElementById('qrformdiv');
        var qr_form = xGetElementById('qrform');
        var subject = xGetElementById('subject');

        // Is this a dumb browser?
        if( !ptalkid || !rto || !dtid_field || !qr_div || !cur_div || !qr_form || 
            !qr_form_div || !subject) {
            return true;
        }

        ptalkid.value = pid;
        dtid_field.value = dtid;
        rto.value = pid;

        if (lastDiv == 'qrdiv') {
            if (! showQRdiv(qr_div)) {
               return true;
            }

            // Only one swap
            if (! swapnodes(qr_div, cur_div)) {
                return true;
            }
        } else if (lastDiv != dtid) {
            var last_div = xGetElementById(lastDiv);
            // Two swaps
            if ((last_div != cur_div) && ! (swapnodes(last_div, cur_div) && swapnodes(qr_div, last_div))) {
                return true;
            }
        }

        lastDiv = targetname;

        if(!subject.value) subject.value = newsubject;

        if(cur_div.className) {
          qr_form_div.className = cur_div.className;
        } else {
          qr_form_div.className = "";
        }

        var qr_body = qr_form.body;  // the textarea of the qr_form
        if (qr_body) {
            // have to set a timeout because most browsers won't let you focus
            // on an element that's still in the process of being created.
            // so lame.
            window.setTimeout(function () { qr_body.focus() }, 250);
        }

        // So it does not follow the link
        return false;
    }

    function regEvent (target, evt, func) {
      if (! target) { return; }
      if (target.attachEvent)
        target.attachEvent("on"+evt, func);
      if (target.addEventListener)
        target.addEventListener(evt, func, false);
    }

    function moreopts()
    {
        var qr_form = xGetElementById('qrform');
        var basepath = xGetElementById('basepath');
        var dtid = xGetElementById('dtid');
        var pidform = xGetElementById('parenttalkid');

        // do not do the default form action (post comment) if something is broke
        if (!qr_form || !basepath || !dtid || !pidform) return false;

        var replyto = Number(dtid.value);
        var pid = Number(pidform.value);

        if(replyto > 0 && pid > 0) {
          //a reply to a comment
          qr_form.action = basepath.value + "replyto=" + replyto;
        } else {
          qr_form.action = basepath.value + "mode=reply";
        }

        // we changed the form action so submit ourselves
        // and don't use the default form action
        qr_form.submit();
        return false;
    }

   function submitform()
   {
        var submitmore = xGetElementById('submitmoreopts');
        var submit = xGetElementById('submitpost');
        if (!submitmore || !submit) return false;

        submit.disabled = true;
        submitmore.disabled = true;

        // New top-level comments
        var dtid = xGetElementById('dtid');
        if (!Number(dtid.value)) {
            dtid.value =+ 0;
        }

        var qr_form = xGetElementById('qrform');
        qr_form.action = Site.siteroot + '/talkpost_do.bml';
        qr_form.submit();

        // don't do default form action
        return false;
   }

   function swapnodes (orig, to_swap) {
        var orig_pn = xParent(orig, true);
        var next_sibling = orig.nextSibling;
        var to_swap_pn = xParent(to_swap, true);
        if (! to_swap_pn) {
            return false;
        }

        to_swap_pn.replaceChild(orig, to_swap);
        orig_pn.insertBefore(to_swap, next_sibling);
        return true;
   }

   function checkLength() {
        var textbox = xGetElementById('body');
        if (!textbox) return true;
        if (textbox.value.length > 4300) {
             alert('Sorry, but your comment of ' + textbox.value.length + ' characters exceeds the maximum character length of 4300.  Please try shortening it and then post again.');
             return false;
        }
        return true;
   }

    // Maintain entry through browser navigations.
    function save_entry() {
        var qr_body = xGetElementById('body');
        if (!qr_body) return false;
        var qr_subject = xGetElementById('subject');
        var do_spellcheck = xGetElementById('do_spellcheck');
        var qr_dtid = xGetElementById('dtid');
        var qr_ptid = xGetElementById('parenttalkid');
        var qr_upic = xGetElementById('prop_picture_keyword');

        var qr_saved_body = xGetElementById('saved_body');
        var qr_saved_subject = xGetElementById('saved_subject');
        var saved_do_spellcheck = xGetElementById('saved_spell');
        var qr_saved_dtid = xGetElementById('saved_dtid');
        var qr_saved_ptid = xGetElementById('saved_ptid');
        var qr_saved_upic = xGetElementById('saved_upic');

        qr_saved_body.value = qr_body.value;
        qr_saved_subject.value = qr_subject.value;
        if(do_spellcheck) {
          saved_do_spellcheck.value = do_spellcheck.checked;
        }

        qr_saved_dtid.value = qr_dtid.value;
        qr_saved_ptid.value = qr_ptid.value;

        if (qr_upic) { // if it was in the form
            qr_saved_upic.value = qr_upic.selectedIndex;
        }

        return false;
    }

    // Restore saved_entry text across platforms.
    function restore_entry() {
        setTimeout(
            function () {

                var saved_body = xGetElementById('saved_body');
                if (!saved_body || saved_body.value == "") return false;

                var dtid = xGetElementById('saved_dtid');
                if (! dtid) return false;
                var ptid = xGetElementById('saved_ptid');
                ptid.value = parseInt( ptid.value );

                quickreply(dtid.value, ptid.value, saved_body.value);

                var body = xGetElementById('body');
                if (! body) return false;
                body.value = saved_body.value;

                // Some browsers require we explicitly set this after the div has moved
                // and is now no longer hidden
                var qr_saved_subject = xGetElementById('saved_subject');
                var qr_saved_spell = xGetElementById('saved_spell');
                var qr_saved_dtid = xGetElementById('saved_dtid');
                var qr_saved_ptid = xGetElementById('saved_ptid');
                var qr_saved_upic = xGetElementById('saved_upic');

                var subject = xGetElementById('subject');
                if (! subject) return false;
                subject.value = qr_saved_subject.value

                var prop_picture_keyword = xGetElementById('prop_picture_keyword');
                if (prop_picture_keyword) { // if it was in the form
                    prop_picture_keyword.selectedIndex = qr_saved_upic.value;
                }

                var spell_check = xGetElementById('do_spellcheck');
                if (! spell_check) return false;
                if (qr_saved_spell.value == 'true') {
                    spell_check.checked = true;
                } else {
                    spell_check.checked = false;
                }

            }, 100);
        return false;
    }

    function showQRdiv(qr_div) {
        if (! qr_div) {
            qr_div = xGetElementById('qrdiv');
            if (! qr_div) {
                return false;
            }
        } else if (qr_div.style && xDef(qr_div.style.display)) {
            qr_div.style.display='inline';
            return true;
        } else {
            return false;
        }
    }

    //after the functions have been defined, register them
    regEvent(window, 'load', restore_entry);
    regEvent(window, 'unload', save_entry);
var Site;
if (! Site) Site = new Object();

// called by S2:
function setStyle (did, attr, val) {
    if (! document.getElementById) return;
    var de = document.getElementById(did);
    if (! de) return;
    if (de.style)
        de.style[attr] = val
}

// called by S2:
function setInner (did, val) {
    if (! document.getElementById) return;
    var de = document.getElementById(did);
    if (! de) return;
    de.innerHTML = val;
}

// called by S2:
function hideElement (did) {
    if (! document.getElementById) return;
    var de = document.getElementById(did);
    if (! de) return;
    de.style.display = 'none';
}

// called by S2:
function setAttr (did, attr, classname) {
    if (! document.getElementById) return;
    var de = document.getElementById(did);
    if (! de) return;
    de.setAttribute(attr, classname);
}

function getXTR () {
    var xtr;
    var ex;

    if (typeof(XMLHttpRequest) != "undefined") {
        xtr = new XMLHttpRequest();
    } else {
        try {
            xtr = new ActiveXObject("Msxml2.XMLHTTP.4.0");
        } catch (ex) {
            try {
                xtr = new ActiveXObject("Msxml2.XMLHTTP");
            } catch (ex) {
            }
        }
    }

    // let me explain this.  Opera 8 does XMLHttpRequest, but not setRequestHeader.
    // no problem, we thought:  we'll test for setRequestHeader and if it's not present
    // then fall back to the old behavior (treat it as not working).  BUT --- IE6 won't
    // let you even test for setRequestHeader without throwing an exception (you need
    // to call .open on the .xtr first or something)
    try {
        if (xtr && ! xtr.setRequestHeader)
            xtr = null;
    } catch (ex) { }

    return xtr;
}

// push new element 'ne' after sibling 'oe' old element
function addAfter (oe, ne) {
    if (oe.nextSibling) {
        oe.parentNode.insertBefore(ne, oe.nextSibling);
    } else {
        oe.parentNode.appendChild(ne);
    }
}

// hsv to rgb
// h, s, v = [0, 1), [0, 1], [0, 1]
// r, g, b = [0, 255], [0, 255], [0, 255]
function hsv_to_rgb (h, s, v)
{
    if (s == 0) {
	v *= 255;
	return [v,v,v];
    }

    h *= 6;
    var i = Math.floor(h);
    var f = h - i;
    var p = v * (1 - s);
    var q = v * (1 - s * f);
    var t = v * (1 - s * (1 - f));

    v = Math.floor(v * 255 + 0.5);
    t = Math.floor(t * 255 + 0.5);
    p = Math.floor(p * 255 + 0.5);
    q = Math.floor(q * 255 + 0.5);

    if (i == 0) return [v,t,p];
    if (i == 1) return [q,v,p];
    if (i == 2) return [p,v,t];
    if (i == 3) return [p,q,v];
    if (i == 4) return [t,p,v];
    return [v,p,q];
}

// stops the bubble
function stopBubble (e) {
    if (e.stopPropagation)
        e.stopPropagation();
    if ("cancelBubble" in e)
        e.cancelBubble = true;
}

// stops the bubble, as well as the default action
function stopEvent (e) {
    stopBubble(e);
    if (e.preventDefault)
        e.preventDefault();
    if ("returnValue" in e)
        e.returnValue = false;
    return false;
}

function scrollTop () {
    if (window.innerHeight)
        return window.pageYOffset;
    if (document.documentElement && document.documentElement.scrollTop)
        return document.documentElement.scrollTop;
    if (document.body)
        return document.body.scrollTop;
}

function scrollLeft () {
    if (window.innerWidth)
        return window.pageXOffset;
    if (document.documentElement && document.documentElement.scrollLeft)
        return document.documentElement.scrollLeft;
    if (document.body)
        return document.body.scrollLeft;
}

function getElementPos (obj)
{
    var pos = new Object();
    if (!obj)
        return null;

    var it;

    it = obj;
    pos.x = 0;
    if (it.offsetParent) {
	while (it.offsetParent) {
	    pos.x += it.offsetLeft;
	    it = it.offsetParent;
	}
    }
    else if (it.x)
	pos.x += it.x;

    it = obj;
    pos.y = 0;
    if (it.offsetParent) {
	while (it.offsetParent) {
	    pos.y += it.offsetTop;
	    it = it.offsetParent;
	}
    }
    else if (it.y)
	pos.y += it.y;

    return pos;
}

// returns the mouse position of the event, or failing that, the top-left
// of the event's target element.  (or the fallBack element, which takes
// precendence over the event's target element if specified)
function getEventPos (e, fallBack)
{
    var pos = { x:0, y:0 };

    if (!e) var e = window.event;
    if (e.pageX && e.pageY) {
        // useful case (relative to document)
        pos.x = e.pageX;
        pos.y = e.pageY;
    }
    else if (e.clientX && e.clientY) {
        // IE case (relative to viewport, so need scroll info)
        pos.x = e.clientX + scrollLeft();
        pos.y = e.clientY + scrollTop();
    } else {
	var targ = fallBack || getTarget(e);
	var pos = getElementPos(targ);
	return pos;
    }
    return pos;
}

var curPopup = null;
var curPopup_id = 0;

function killPopup () {
    if (!curPopup)
        return true;

    var popup = curPopup;
    curPopup = null;

    var opp = 1.0;

    var fade = function () {
        opp -= 0.15;

        if (opp <= 0.1) {
            popup.parentNode.removeChild(popup);
        } else {
            popup.style.filter = "alpha(opacity=" + Math.floor(opp * 100) + ")";
            popup.style.opacity = opp;
            window.setTimeout(fade, 20);
        }
    };
    fade();

    return true;
}

var pendingReqs = new Object ();

function deleteComment (ditemid) {

    var hasopt = function (opt) {
        var el = document.getElementById("ljpopdel" + ditemid + opt);
        if (!el) return false;
        if (el.checked) return true;
        return false;
    };
    var opt_delthread = hasopt("thread");
    var opt_ban = hasopt("ban");
    var opt_spam = hasopt("spam");

    killPopup();

    var todel = document.getElementById("ljcmt" + ditemid);

    var col = 0;
    var pulse = 0;
    var is_deleted = 0;
    var is_error = 0;

    var xtr = getXTR();
    if (! xtr) {
        alert("JS_ASSERT: no xtr now, but earlier?");
        return false;
    }
    pendingReqs[ditemid] = xtr;

    var state_callback = function () {
        if (xtr.readyState != 4)
             return;

        if (xtr.status == 200) {
            var val = eval(xtr.responseText);
            is_deleted = val;
            if (! is_deleted) is_error = 1;
        } else {
            alert("Error contacting server to delete comment.");
            is_error = 1;
        }
    };

    var error_callback = function () {
        alert("Error deleting " + ditemid);
        is_error = 1;
    };

    xtr.onreadystatechange = state_callback;
    xtr.open("POST", "/" + LJ_cmtinfo.journal + "/__rpc_delcomment?mode=js&journal=" + LJ_cmtinfo.journal + "&id=" + ditemid, true);
    var postdata = "confirm=1";
    if (opt_ban) postdata += "&ban=1";
    if (opt_spam) postdata += "&spam=1";
    if (opt_delthread) postdata += "&delthread=1";
    if (LJ_cmtinfo.form_auth) postdata += "&lj_form_auth=" + LJ_cmtinfo.form_auth;

    xtr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xtr.send(postdata);

    var flash = function () {
        var rgb = hsv_to_rgb(0, Math.cos((pulse + 1) / 2), 1);
        pulse += 3.14159 / 5;
        var color = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";

        todel.style.border = "2px solid " + color;
        if (is_error) {
            todel.style.border = "";
            // and let timer expire
        } else if (is_deleted) {
            removeComment(ditemid, opt_delthread);
        } else {
            window.setTimeout(flash, 50);
        }
    };

    window.setTimeout(flash, 5);
}

function removeComment (ditemid, killChildren) {
    var todel = document.getElementById("ljcmt" + ditemid);
    if (todel) {
        todel.style.display = 'none';

        var userhook = window["userhook_delete_comment_ARG"];
        if (userhook)
            userhook(ditemid);
    }
    if (killChildren) {
        var com = LJ_cmtinfo ? LJ_cmtinfo[ditemid] : null;
        for (var i = 0; i < com.rc.length; i++) {
            removeComment(com.rc[i], true);
        }
    }
}

function docClicked (e) {
  if (curPopup)
    killPopup();

  // we didn't handle anything, who are we kidding
}

function createDeleteFunction (ae, dItemid) {
    return function (e) {
        if (!e) e = window.event;
        var FS = arguments.callee;

        var finalHeight = 100;

        if (e.shiftKey || (curPopup && curPopup_id != dItemid)) {
            killPopup();
        }

        var doIT = 0;
        // immediately delete on shift key
        if (e.shiftKey) {
            doIT = 1;
        } else {
            if (! LJ_cmtinfo)
                return true;

            var com = LJ_cmtinfo[dItemid];
            var remoteUser = LJ_cmtinfo["remote"];
            if (!com || !remoteUser)
                return true;
            var canAdmin = LJ_cmtinfo["canAdmin"];

            var clickTarget = getTarget(e);
            var used_keyboard = clickTarget.nodeName == "A";

            var pos = used_keyboard ? getElementPos(ae) : getEventPos(e);
            var lx = pos.x + 5 - 250;
            if (lx < 5) lx = 5;
            var de;

            if (curPopup && curPopup_id == dItemid) {
                de = curPopup;
                de.style.left = lx + "px";
                de.style.top = (pos.y + 5) + "px";
                return stopEvent(e);
            }

            de = document.createElement("div");
            de.style.textAlign = "left";
	    de.className = 'ljcmtmanage';
	    de.style.height = "10px";
            de.style.overflow = "hidden";
            de.style.position = "absolute";
            de.style.left = lx + "px";
            de.style.top = (pos.y + 5) + "px";
            de.style.width = "250px";
            de.style.zIndex = 3;
  	    regEvent(de, "click", function (e) {
		e = e || window.event;
                stopBubble(e);
		return true;
	    });

            var inHTML = "<form style='display: inline' id='ljdelopts" + dItemid + "'><span style='font-face: Arial; font-size: 8pt'><b>Delete comment?</b><br />";
            var lbl;
            if (remoteUser != "" && com.u != "" && com.u != remoteUser && canAdmin) {
                lbl = "ljpopdel" + dItemid + "ban";
                inHTML += "<input type='checkbox' value='ban' id='" + lbl + "'> <label for='" + lbl + "'>Ban <b>" + com.u + "</b> from commenting</label><br />";
            } else {
                finalHeight -= 15;
            }

            if (remoteUser != com.u && canAdmin) {
                lbl = "ljpopdel" + dItemid + "spam";
                inHTML += "<input type='checkbox' value='spam' id='" + lbl + "'> <label for='" + lbl + "'>Mark this comment as spam</label><br />";
            } else {
                finalHeight -= 15;
            }

            if (com.rc && com.rc.length && canAdmin) {
                lbl = "ljpopdel" + dItemid + "thread";
                inHTML += "<input type='checkbox' value='thread' id='" + lbl + "'> <label for='" + lbl + "'>Delete thread (all subcomments)</label><br />";
            } else {
                finalHeight -= 15;
            }
            inHTML += "<input type='button' value='Delete' onclick='deleteComment(" + dItemid + ");' /> <input type='button' value='Cancel' onclick='killPopup()' /></span><br /><span style='font-face: Arial; font-size: 8pt'><i>shift-click to delete without options</i></span></form>";
            de.innerHTML = inHTML;

            // we do this so keyboard tab order is correct:
            addAfter(ae, de);

            curPopup = de;
            curPopup_id = dItemid;

            var height = 10;
            var grow = function () {
                height += 7;
                if (height > finalHeight) {
                    de.style.height = null;
                    de.style.filter = "";
                    de.style.opacity = 1.0;
                } else {
                    de.style.height = height + "px";
                    window.setTimeout(grow, 20);
                }
            };
            grow();

        }

        if (doIT) {
            deleteComment(dItemid);
        }

        return stopEvent(e);
    }
}

function poofAt (pos) {
    var de = document.createElement("div");
    de.style.position = "absolute";
    de.style.background = "#FFF";
    de.style.overflow = "hidden";
    var opp = 1.0;

    var top = pos.y;
    var left = pos.x;
    var width = 5;
    var height = 5;
    document.body.appendChild(de);

    var fade = function () {
        opp -= 0.15;
        width += 10;
        height += 10;
        top -= 5;
        left -= 5;

        if (opp <= 0.1) {
            de.parentNode.removeChild(de);
        } else {
            de.style.left = left + "px";
            de.style.top = top + "px";
            de.style.height = height + "px";
            de.style.width = width + "px";
            de.style.filter = "alpha(opacity=" + Math.floor(opp * 100) + ")";
            de.style.opacity = opp;
            window.setTimeout(fade, 20);
        }
    };
    fade();
}

function getTarget (ev) {
    var target;
    if (ev.target)
        target = ev.target;
    else if (ev.srcElement)
        target = ev.srcElement;

    // Safari bug:
    if (target && target.nodeType == 3)
        target = target.parentNode;

    return target;
}

function updateLink (ae, resObj, clickTarget) {
    ae.href = resObj.newurl;
    var userhook = window["userhook_" + resObj.mode + "_comment_ARG"];
    var did_something = 0;

    if (clickTarget && clickTarget.src && clickTarget.src == resObj.oldimage) {
        clickTarget.src = resObj.newimage;
        did_something = 1;
    }

    if (userhook) {
        userhook(resObj.id);
        did_something = 1;
    }

    // if all else fails, at least remove the link so they're not as confused
    if (! did_something) {
        if (ae && ae.style)
            ae.style.display = 'none';
        if (clickTarget && clickTarget.style)
            clickTarget.style.dispay = 'none';
    }

}

var tsInProg = new Object();  // dict of { ditemid => 1 }
function createModerationFunction (ae, dItemid) {
    return function (e) {
        if (!e) e = window.event;

        if (tsInProg[dItemid])
            return stopEvent(e);
        tsInProg[dItemid] = 1;

        var clickTarget = getTarget(e);

        var used_keyboard = clickTarget.nodeName == "A";

        var imgTarget;
        var imgs = ae.getElementsByTagName("img");
        if (imgs.length)
            imgTarget = imgs[0]

        if (! clickTarget || typeof(clickTarget) != "object")
            return true;

        var clickPos = used_keyboard ? getElementPos(imgTarget || ae) : getEventPos(e);

        var de = document.createElement("img");
        de.style.position = "absolute";
        de.width = 17;
        de.height = 17;
        de.src = Site.imgprefix + "/hourglass.gif";
        de.style.top = (clickPos.y - 8) + "px";
        de.style.left = (clickPos.x - 8) + "px";
        document.body.appendChild(de);

        var xtr = getXTR();
        var state_callback = function () {
            if (xtr.readyState != 4) return;

            document.body.removeChild(de);
            var rpcRes;

            if (xtr.status == 200) {
                var resObj = eval(xtr.responseText);
                if (resObj) {
                    poofAt(clickPos);
                    updateLink(ae, resObj, imgTarget);
                    tsInProg[dItemid] = 0;
                } else {
                    tsInProg[dItemid] = 0;
                }

            } else {
                alert("Error contacting server.");
                tsInProg[dItemid] = 0;
            }
        };

        xtr.onreadystatechange = state_callback;

        var postUrl = ae.href.replace(/.+talkscreen\.bml/, "/" + LJ_cmtinfo.journal + "/__rpc_talkscreen");

        //var postUrl = ae.href;
        xtr.open("POST", postUrl + "&jsmode=1", true);

        var postdata = "confirm=Y&lj_form_auth=" + LJ_cmtinfo.form_auth;

        xtr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xtr.send(postdata);

        return stopEvent(e);
    };
}

function setupAjax () {
    var ct = document.links.length;
    for (var i=0; i<ct; i++) {
        var ae = document.links[i];
        if (ae.href.indexOf("talkscreen.bml") != -1) {
            ae.onclick = createModerationFunction(ae, dItemid);

        } else if (ae.href.indexOf("delcomment.bml") != -1) {

            var findIDre = /id=(\d+)/;
            var reMatch = findIDre.exec(ae.href);
            if (! reMatch) return true;

            var dItemid = reMatch[1];
            var todel = document.getElementById("ljcmt" + dItemid);
            if (! todel) return true;

            if (LJ_cmtinfo && LJ_cmtinfo.disableInlineDelete) continue;

            ae.onclick = createDeleteFunction(ae, dItemid);
        }

    }
}

function regEvent (target, evt, func) {
    if (! target) return;
    if (target.attachEvent)
        target.attachEvent("on"+evt, func);
    if (target.addEventListener)
        target.addEventListener(evt, func, false);
}

if (document.getElementById && getXTR()) {
       regEvent(window, "load", setupAjax);
	regEvent(document, "click", docClicked);
        document.write("<style> div.ljcmtmanage { color: #000; background: #e0e0e0; border: 2px solid #000; padding: 3px; }</style>");
}
// ljtalk for ctxpopup
LiveJournal.register_hook("ctxpopup_extrainfo", function (userdata) {
    var content = document.createElement("div");

    if (userdata.is_person) {
        if (userdata.is_online !== '') {
            // online status
            var onlineStatusLabel = document.createElement("span");
            var jabberTitle = userdata.jabber_title;
            onlineStatusLabel.innerHTML = jabberTitle + ": ";
            DOM.addClassName(onlineStatusLabel, "OnlineStatus");
            content.appendChild(onlineStatusLabel);

            // build status
            var onlineStatus = document.createElement("span");


            var onlineStatusText = document.createElement("span");
            onlineStatus.appendChild(onlineStatusText);

            content.appendChild(onlineStatus);

            if (userdata.is_online) {
                onlineStatusText.innerHTML = "Online";
            } else if (userdata.is_online == '0') {
                onlineStatusText.innerHTML = "Offline";
            }
        }
    }

    return content;
});

// for updating ljcom widgets from livejournal ones
LiveJournal.register_hook("update_other_widgets", function (from_widget) {
    if (from_widget == "LayoutChooser" && Customize.AdLayout) {
        Customize.AdLayout.updateContent();
    }
});
