
/* ----- mergedJavascripts.js ----- */
/* Merged Plone Javascript file
 * This file is dynamically assembled from separate parts.
 * Some of these parts have 3rd party licenses or copyright information attached
 * Such information is valid for that section,
 * not for the entire composite file
 * originating files are separated by ----- filename.js -----
 */

/* ----- register_function.js ----- */
/* Essential javascripts, used a lot. 
 * These should be placed inline
 * We have to be certain they are loaded before anything that uses them 
 */

// check for ie5 mac
var bugRiddenCrashPronePieceOfJunk = (
    navigator.userAgent.indexOf('MSIE 5') != -1
    &&
    navigator.userAgent.indexOf('Mac') != -1
)

// check for W3CDOM compatibility
var W3CDOM = (!bugRiddenCrashPronePieceOfJunk &&
               document.getElementsByTagName &&
               document.createElement);

// cross browser function for registering event handlers
function registerEventListener(elem, event, func) {
    if (elem.addEventListener) {
        elem.addEventListener(event, func, false);
        return true;
    } else if (elem.attachEvent) {
        var result = elem.attachEvent("on"+event, func);
        return result;
    }
    // maybe we could implement something with an array
    return false;
}

// cross browser function for unregistering event handlers
function unRegisterEventListener(elem, event, func) {
    if (elem.removeEventListener) {
        elem.removeEventListener(event, func, false);
        return true;
    } else if (elem.detachEvent) {
        var result = elem.detachEvent("on"+event, func);
        return result;
    }
    // maybe we could implement something with an array
    return false;
}

function registerPloneFunction(func) {
    // registers a function to fire onload.
    registerEventListener(window, "load", func);
}

function unRegisterPloneFunction(func) {
    // unregisters a function so it does not fire onload.
    unRegisterEventListener(window, "load", func);
}

function getContentArea() {
    // returns our content area element
    if (W3CDOM) {
        var node = document.getElementById('region-content');
        if (!node) {
            node = document.getElementById('content');
        }
        return node;
    }
} 


/* ----- cssQuery.js ----- */
/*
	cssQuery, version 2.0.2 (2005-08-19)
	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
	License: http://creativecommons.org/licenses/LGPL/2.1/
*/
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('7 x=6(){7 1D="2.0.2";7 C=/\\s*,\\s*/;7 x=6(s,A){33{7 m=[];7 u=1z.32.2c&&!A;7 b=(A)?(A.31==22)?A:[A]:[1g];7 1E=18(s).1l(C),i;9(i=0;i<1E.y;i++){s=1y(1E[i]);8(U&&s.Z(0,3).2b("")==" *#"){s=s.Z(2);A=24([],b,s[1])}1A A=b;7 j=0,t,f,a,c="";H(j<s.y){t=s[j++];f=s[j++];c+=t+f;a="";8(s[j]=="("){H(s[j++]!=")")a+=s[j];a=a.Z(0,-1);c+="("+a+")"}A=(u&&V[c])?V[c]:21(A,t,f,a);8(u)V[c]=A}m=m.30(A)}2a x.2d;5 m}2Z(e){x.2d=e;5[]}};x.1Z=6(){5"6 x() {\\n  [1D "+1D+"]\\n}"};7 V={};x.2c=L;x.2Y=6(s){8(s){s=1y(s).2b("");2a V[s]}1A V={}};7 29={};7 19=L;x.15=6(n,s){8(19)1i("s="+1U(s));29[n]=12 s()};x.2X=6(c){5 c?1i(c):o};7 D={};7 h={};7 q={P:/\\[([\\w-]+(\\|[\\w-]+)?)\\s*(\\W?=)?\\s*([^\\]]*)\\]/};7 T=[];D[" "]=6(r,f,t,n){7 e,i,j;9(i=0;i<f.y;i++){7 s=X(f[i],t,n);9(j=0;(e=s[j]);j++){8(M(e)&&14(e,n))r.z(e)}}};D["#"]=6(r,f,i){7 e,j;9(j=0;(e=f[j]);j++)8(e.B==i)r.z(e)};D["."]=6(r,f,c){c=12 1t("(^|\\\\s)"+c+"(\\\\s|$)");7 e,i;9(i=0;(e=f[i]);i++)8(c.l(e.1V))r.z(e)};D[":"]=6(r,f,p,a){7 t=h[p],e,i;8(t)9(i=0;(e=f[i]);i++)8(t(e,a))r.z(e)};h["2W"]=6(e){7 d=Q(e);8(d.1C)9(7 i=0;i<d.1C.y;i++){8(d.1C[i]==e)5 K}};h["2V"]=6(e){};7 M=6(e){5(e&&e.1c==1&&e.1f!="!")?e:23};7 16=6(e){H(e&&(e=e.2U)&&!M(e))28;5 e};7 G=6(e){H(e&&(e=e.2T)&&!M(e))28;5 e};7 1r=6(e){5 M(e.27)||G(e.27)};7 1P=6(e){5 M(e.26)||16(e.26)};7 1o=6(e){7 c=[];e=1r(e);H(e){c.z(e);e=G(e)}5 c};7 U=K;7 1h=6(e){7 d=Q(e);5(2S d.25=="2R")?/\\.1J$/i.l(d.2Q):2P(d.25=="2O 2N")};7 Q=6(e){5 e.2M||e.1g};7 X=6(e,t){5(t=="*"&&e.1B)?e.1B:e.X(t)};7 17=6(e,t,n){8(t=="*")5 M(e);8(!14(e,n))5 L;8(!1h(e))t=t.2L();5 e.1f==t};7 14=6(e,n){5!n||(n=="*")||(e.2K==n)};7 1e=6(e){5 e.1G};6 24(r,f,B){7 m,i,j;9(i=0;i<f.y;i++){8(m=f[i].1B.2J(B)){8(m.B==B)r.z(m);1A 8(m.y!=23){9(j=0;j<m.y;j++){8(m[j].B==B)r.z(m[j])}}}}5 r};8(![].z)22.2I.z=6(){9(7 i=0;i<1z.y;i++){o[o.y]=1z[i]}5 o.y};7 N=/\\|/;6 21(A,t,f,a){8(N.l(f)){f=f.1l(N);a=f[0];f=f[1]}7 r=[];8(D[t]){D[t](r,A,f,a)}5 r};7 S=/^[^\\s>+~]/;7 20=/[\\s#.:>+~()@]|[^\\s#.:>+~()@]+/g;6 1y(s){8(S.l(s))s=" "+s;5 s.P(20)||[]};7 W=/\\s*([\\s>+~(),]|^|$)\\s*/g;7 I=/([\\s>+~,]|[^(]\\+|^)([#.:@])/g;7 18=6(s){5 s.O(W,"$1").O(I,"$1*$2")};7 1u={1Z:6(){5"\'"},P:/^(\'[^\']*\')|("[^"]*")$/,l:6(s){5 o.P.l(s)},1S:6(s){5 o.l(s)?s:o+s+o},1Y:6(s){5 o.l(s)?s.Z(1,-1):s}};7 1s=6(t){5 1u.1Y(t)};7 E=/([\\/()[\\]?{}|*+-])/g;6 R(s){5 s.O(E,"\\\\$1")};x.15("1j-2H",6(){D[">"]=6(r,f,t,n){7 e,i,j;9(i=0;i<f.y;i++){7 s=1o(f[i]);9(j=0;(e=s[j]);j++)8(17(e,t,n))r.z(e)}};D["+"]=6(r,f,t,n){9(7 i=0;i<f.y;i++){7 e=G(f[i]);8(e&&17(e,t,n))r.z(e)}};D["@"]=6(r,f,a){7 t=T[a].l;7 e,i;9(i=0;(e=f[i]);i++)8(t(e))r.z(e)};h["2G-10"]=6(e){5!16(e)};h["1x"]=6(e,c){c=12 1t("^"+c,"i");H(e&&!e.13("1x"))e=e.1n;5 e&&c.l(e.13("1x"))};q.1X=/\\\\:/g;q.1w="@";q.J={};q.O=6(m,a,n,c,v){7 k=o.1w+m;8(!T[k]){a=o.1W(a,c||"",v||"");T[k]=a;T.z(a)}5 T[k].B};q.1Q=6(s){s=s.O(o.1X,"|");7 m;H(m=s.P(o.P)){7 r=o.O(m[0],m[1],m[2],m[3],m[4]);s=s.O(o.P,r)}5 s};q.1W=6(p,t,v){7 a={};a.B=o.1w+T.y;a.2F=p;t=o.J[t];t=t?t(o.13(p),1s(v)):L;a.l=12 2E("e","5 "+t);5 a};q.13=6(n){1d(n.2D()){F"B":5"e.B";F"2C":5"e.1V";F"9":5"e.2B";F"1T":8(U){5"1U((e.2A.P(/1T=\\\\1v?([^\\\\s\\\\1v]*)\\\\1v?/)||[])[1]||\'\')"}}5"e.13(\'"+n.O(N,":")+"\')"};q.J[""]=6(a){5 a};q.J["="]=6(a,v){5 a+"=="+1u.1S(v)};q.J["~="]=6(a,v){5"/(^| )"+R(v)+"( |$)/.l("+a+")"};q.J["|="]=6(a,v){5"/^"+R(v)+"(-|$)/.l("+a+")"};7 1R=18;18=6(s){5 1R(q.1Q(s))}});x.15("1j-2z",6(){D["~"]=6(r,f,t,n){7 e,i;9(i=0;(e=f[i]);i++){H(e=G(e)){8(17(e,t,n))r.z(e)}}};h["2y"]=6(e,t){t=12 1t(R(1s(t)));5 t.l(1e(e))};h["2x"]=6(e){5 e==Q(e).1H};h["2w"]=6(e){7 n,i;9(i=0;(n=e.1F[i]);i++){8(M(n)||n.1c==3)5 L}5 K};h["1N-10"]=6(e){5!G(e)};h["2v-10"]=6(e){e=e.1n;5 1r(e)==1P(e)};h["2u"]=6(e,s){7 n=x(s,Q(e));9(7 i=0;i<n.y;i++){8(n[i]==e)5 L}5 K};h["1O-10"]=6(e,a){5 1p(e,a,16)};h["1O-1N-10"]=6(e,a){5 1p(e,a,G)};h["2t"]=6(e){5 e.B==2s.2r.Z(1)};h["1M"]=6(e){5 e.1M};h["2q"]=6(e){5 e.1q===L};h["1q"]=6(e){5 e.1q};h["1L"]=6(e){5 e.1L};q.J["^="]=6(a,v){5"/^"+R(v)+"/.l("+a+")"};q.J["$="]=6(a,v){5"/"+R(v)+"$/.l("+a+")"};q.J["*="]=6(a,v){5"/"+R(v)+"/.l("+a+")"};6 1p(e,a,t){1d(a){F"n":5 K;F"2p":a="2n";1a;F"2o":a="2n+1"}7 1m=1o(e.1n);6 1k(i){7 i=(t==G)?1m.y-i:i-1;5 1m[i]==e};8(!Y(a))5 1k(a);a=a.1l("n");7 m=1K(a[0]);7 s=1K(a[1]);8((Y(m)||m==1)&&s==0)5 K;8(m==0&&!Y(s))5 1k(s);8(Y(s))s=0;7 c=1;H(e=t(e))c++;8(Y(m)||m==1)5(t==G)?(c<=s):(s>=c);5(c%m)==s}});x.15("1j-2m",6(){U=1i("L;/*@2l@8(@\\2k)U=K@2j@*/");8(!U){X=6(e,t,n){5 n?e.2i("*",t):e.X(t)};14=6(e,n){5!n||(n=="*")||(e.2h==n)};1h=1g.1I?6(e){5/1J/i.l(Q(e).1I)}:6(e){5 Q(e).1H.1f!="2g"};1e=6(e){5 e.2f||e.1G||1b(e)};6 1b(e){7 t="",n,i;9(i=0;(n=e.1F[i]);i++){1d(n.1c){F 11:F 1:t+=1b(n);1a;F 3:t+=n.2e;1a}}5 t}}});19=K;5 x}();',62,190,'|||||return|function|var|if|for||||||||pseudoClasses||||test|||this||AttributeSelector|||||||cssQuery|length|push|fr|id||selectors||case|nextElementSibling|while||tests|true|false|thisElement||replace|match|getDocument|regEscape||attributeSelectors|isMSIE|cache||getElementsByTagName|isNaN|slice|child||new|getAttribute|compareNamespace|addModule|previousElementSibling|compareTagName|parseSelector|loaded|break|_0|nodeType|switch|getTextContent|tagName|document|isXML|eval|css|_1|split|ch|parentNode|childElements|nthChild|disabled|firstElementChild|getText|RegExp|Quote|x22|PREFIX|lang|_2|arguments|else|all|links|version|se|childNodes|innerText|documentElement|contentType|xml|parseInt|indeterminate|checked|last|nth|lastElementChild|parse|_3|add|href|String|className|create|NS_IE|remove|toString|ST|select|Array|null|_4|mimeType|lastChild|firstChild|continue|modules|delete|join|caching|error|nodeValue|textContent|HTML|prefix|getElementsByTagNameNS|end|x5fwin32|cc_on|standard||odd|even|enabled|hash|location|target|not|only|empty|root|contains|level3|outerHTML|htmlFor|class|toLowerCase|Function|name|first|level2|prototype|item|scopeName|toUpperCase|ownerDocument|Document|XML|Boolean|URL|unknown|typeof|nextSibling|previousSibling|visited|link|valueOf|clearCache|catch|concat|constructor|callee|try'.split('|'),0,{}))


/* ----- plone_javascript_variables.js ----- */

// Global Plone variables that need to be accessible to the Javascripts
var portal_url = 'http://lrnlab.edfac.usyd.edu.au';
var form_modified_message = 'Your form has not been saved. All changes you have made will be lost.';
var form_resubmit_message = 'Your already clicked the submit button. Do you really want to submit this form again?';


/* ----- nodeutilities.js ----- */

function wrapNode(node, wrappertype, wrapperclass){
    /* utility function to wrap a node in an arbitrary element of type "wrappertype"
     * with a class of "wrapperclass" */
    var wrapper = document.createElement(wrappertype)
    wrapper.className = wrapperclass;
    var innerNode = node.parentNode.replaceChild(wrapper,node);
    wrapper.appendChild(innerNode);
};

function nodeContained(innernode, outernode){
    // check if innernode is contained in outernode
    var node = innernode.parentNode;
    while (node != document) {
        if (node == outernode) {
            return true; 
        }
        node=node.parentNode;
    }
    return false;
};

function findContainer(node, func) {
    // Starting with the given node, find the nearest containing element
    // for which the given function returns true.

    while (node != null) {
        if (func(node)) {
            return node;
        }
        node = node.parentNode;
    }
    return false;
};

function hasClassName(node, class_name) {
    return new RegExp('\\b'+class_name+'\\b').test(node.className);
};

function addClassName(node, class_name) {
    if (!node.className) {
        node.className = class_name;
    } else if (!hasClassName(node, class_name)) {
        var className = node.className+" "+class_name;
        // cleanup
        node.className = className.split(/\s+/).join(' ');
    }
};

function removeClassName(node, class_name) {
    var className = node.className;
    if (className) {
        // remove
        className = className.replace(new RegExp('\\b'+class_name+'\\b'), '');
        // cleanup
        className = className.replace(/\s+/g, ' ');
        node.className = className.replace(/\s+$/g, '');
    }
};

function replaceClassName(node, old_class, new_class, ignore_missing) {
    if (ignore_missing && !hasClassName(node, old_class)) {
        addClassName(node, new_class);
    } else {
        var className = node.className;
        if (className) {
            // replace
            className = className.replace(new RegExp('\\b'+old_class+'\\b'), new_class);
            // cleanup
            className = className.replace(/\s+/g, ' ');
            node.className = className.replace(/\s+$/g, '');
        }
    }
};

function walkTextNodes(node, func, data) {
    // traverse childnodes and call func when a textnode is found
    if (!node){return false}
    if (node.hasChildNodes) {
        // we can't use for (i in childNodes) here, because the number of
        // childNodes might change (higlightsearchterms)
        for (var i=0;i<node.childNodes.length;i++) {
            walkTextNodes(node.childNodes[i], func, data);
        }
        if (node.nodeType == 3) {
            // this is a text node
            func(node, data);
        }
    }
};

/* These are two functions, because getInnerTextFast doesn't always return the
 * the same results, as it depends on the implementation of node.innerText of
 * the browser. getInnerTextCompatible will always return the same values, but
 * is a bit slower. The difference is just in the whitespace, so if this
 * doesn't matter, you should always use getInnerTextFast.
 */

function getInnerTextCompatible(node) {
    var result = new Array();
    walkTextNodes(node,
                  function(n, d){d.push(n.nodeValue)},
                  result);
    return result.join("");
};

function getInnerTextFast(node) {
    if (node.innerText) {
        return node.innerText;
    } else {
        return getInnerTextCompatible(node);
    }
};

/* This function reorder nodes in the DOM.
 * fetch_func - the function which returns the value for comparison
 * cmp_func - the compare function, if not provided then the string of the
 * value returned by fetch_func is used.
 */
function sortNodes(nodes, fetch_func, cmp_func) {
    // terminate if we hit a non-compliant DOM implementation
    if (!W3CDOM){return false};

    // wrapper for sorting
    var SortNodeWrapper = function(node) {
        this.value = fetch_func(node);
        this.cloned_node = node.cloneNode(true);
        this.toString = function() {
            if (this.value.toString) {
                return this.value.toString();
            } else {
                return this.value;
            }
        }
    }

    // wrap nodes
    var items = new Array();
    for (var i=0; i<nodes.length; i++) {
        items.push(new SortNodeWrapper(nodes[i]));
    }

    //sort
    if (cmp_func) {
        items.sort(cmp_func);
    } else {
        items.sort();
    }

    // reorder nodes
    for (var i=0; i<items.length; i++) {
        var dest = nodes[i];
        dest.parentNode.replaceChild(items[i].cloned_node, dest);
    }
};


/* ----- cookie_functions.js ----- */
function createCookie(name,value,days) {
    if (days) {
        var date = new Date();
        date.setTime(date.getTime()+(days*24*60*60*1000));
        var expires = "; expires="+date.toGMTString();
    } else {
        expires = "";
    }
    document.cookie = name+"="+escape(value)+expires+"; path=/;";
};

function readCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for(var i=0;i < ca.length;i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') {
            c = c.substring(1,c.length);
        }
        if (c.indexOf(nameEQ) == 0) {
            return unescape(c.substring(nameEQ.length,c.length));
        }
    }
    return null;
};


/* ----- livesearch.js ----- */
/*
// +----------------------------------------------------------------------+
// | Copyright (c) 2004 Bitflux GmbH                                      |
// +----------------------------------------------------------------------+
// | Licensed under the Apache License, Version 2.0 (the "License");      |
// | you may not use this file except in compliance with the License.     |
// | You may obtain a copy of the License at                              |
// | http://www.apache.org/licenses/LICENSE-2.0                           |
// | Unless required by applicable law or agreed to in writing, software  |
// | distributed under the License is distributed on an "AS IS" BASIS,    |
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or      |
// | implied. See the License for the specific language governing         |
// | permissions and limitations under the License.                       |
// +----------------------------------------------------------------------+
// | Author: Bitflux GmbH <devel@bitflux.ch>                              |
// +----------------------------------------------------------------------+

*/
var liveSearchReq = false;
var t = null;
var liveSearchLast = "";
var queryTarget = "livesearch_reply?q=";

var searchForm = null;
var searchInput = null; 

var isIE = false;


var _cache = new Object();

var widthOffset=1;

function calculateWidth(){
}


function getElementDimensions(elemID) {
    var base = document.getElementById(elemID);
    var offsetTrail = base;
    var offsetLeft = 0;
    var offsetTop = 0;
    var width = 0;
    
    while (offsetTrail) {
        offsetLeft += offsetTrail.offsetLeft;
        offsetTop += offsetTrail.offsetTop;
        offsetTrail = offsetTrail.offsetParent;
    }
    if (navigator.userAgent.indexOf("Mac") != -1 &&
        typeof document.body.leftMargin != "undefined") {
        offsetLeft += document.body.leftMargin;
        offsetTop += document.body.topMargin;
    }

    if(!isIE){
    width =  searchInput.offsetWidth-widthOffset*2;
    }
    else {
    width = searchInput.offsetWidth;
    }

    return { left:offsetLeft, 
         top:offsetTop, 
         width: width, 
             height: base.offsetHeight,
         bottom: offsetTop + base.offsetHeight, 
         right : offsetLeft + width};
}

function liveSearchInit() {
    searchInput = document.getElementById('searchGadget');
    if (searchInput == null || searchInput == undefined) return
//  Only keypress catches repeats in moz/FF but keydown is needed for
//  khtml based browsers.
    if (navigator.userAgent.indexOf("KHTML") > 0) {
        searchInput.addEventListener("keydown",liveSearchKeyPress,false);
        searchInput.addEventListener("focus",liveSearchDoSearch,false);
        searchInput.addEventListener("keydown",liveSearchStart, false);
        searchInput.addEventListener("blur",liveSearchHideDelayed,false);
    } else if (searchInput.addEventListener) {
        searchInput.addEventListener("keypress",liveSearchKeyPress,false);
        searchInput.addEventListener("blur",liveSearchHideDelayed,false);
        searchInput.addEventListener("keypress",liveSearchStart, false);
    } else {
        searchInput.attachEvent("onkeydown",liveSearchKeyPress);
        searchInput.attachEvent("onkeydown",liveSearchStart);
//      searchInput.attachEvent("onblur",liveSearchHide);
        isIE = true;
    }

//  Why doesn't this work in konq, setting it inline does.
    searchInput.setAttribute("autocomplete","off");

    var pos = getElementDimensions('searchGadget'); 
    result = document.getElementById('LSResult');
    pos.left = pos.left - result.offsetParent.offsetLeft + pos.width;
    result.style.display='none';
}


function liveSearchHideDelayed() {
    window.setTimeout("liveSearchHide()",400);
}
    
function liveSearchHide() { 
    document.getElementById("LSResult").style.display = "none";
    var highlight = document.getElementById("LSHighlight");
    if (highlight) {
        highlight.removeAttribute("id");
    }
}

function getFirstHighlight() {
    var set = getHits();
    return set[0];
}

function getLastHighlight() {
    var set = getHits();
    return set[set.length-1];
}

function getHits() {
    var res = document.getElementById("LSShadow");
    var set = res.getElementsByTagName('li');
    return set
}

function findChild(object, specifier) {
    var cur = object.firstChild;
    try {
    while (cur != undefined) {
        cur = cur.nextSibling;
        if (specifier(cur) == true) return cur;
    }
    } catch(e) {};
    return null;
    
}

function findNext(object, specifier) {
 var cur = object;
 try {
 while (cur != undefined) {

    cur = cur.nextSibling;
    if (cur.nodeType==3) cur=cur.nextSibling;
    
    if (cur != undefined) {
        if (specifier(cur) == true) return cur;
    } else { break }
 }
 } catch(e) {};
 return null;
}

function findPrev(object, specifier) {
 var cur = object;
 try {
        cur = cur.previousSibling;
        if (cur.nodeType==3) cur=cur.previousSibling;
        if (cur!=undefined) {
            if (specifier(cur) == true) 
                return cur;
        } 
 } catch(e) {};
 return null;
}


function liveSearchKeyPress(event) {
    if (event.keyCode == 40 )
    //KEY DOWN
    {
        highlight = document.getElementById("LSHighlight");
        if (!highlight) {
            highlight = getFirstHighlight();
        } else {
            highlight.removeAttribute("id");
            highlight = findNext(highlight, function (o) {return o.className =="LSRow";});

        }
        if (highlight) {
            highlight.setAttribute("id","LSHighlight");
        } 
        if (!isIE) { event.preventDefault(); }
    } 
    //KEY UP
    else if (event.keyCode == 38 ) {
        highlight = document.getElementById("LSHighlight");
        if (!highlight) {
            highlight = getLastHighlight();
        } 
        else {
            highlight.removeAttribute("id");
            highlight = findPrev(highlight, function (o) {return o.className=='LSRow';});
        }
        if (highlight) {
                highlight.setAttribute("id","LSHighlight");
        }
        if (!isIE) { event.preventDefault(); }
    } 
    //ESC
    else if (event.keyCode == 27) {
        highlight = document.getElementById("LSHighlight");
        if (highlight) {
            highlight.removeAttribute("id");
        }
        document.getElementById("LSResult").style.display = "none";
    } 
}
function liveSearchStart(event) {
    if (t) {
        window.clearTimeout(t);
    }
    code = event.keyCode;
    if (code!=40 && code!=38 && code!=27 && code!=37 && code!=39) {
        t = window.setTimeout("liveSearchDoSearch()",200);
    } 
}

function liveSearchDoSearch() {

    if (typeof liveSearchRoot == "undefined") {
        liveSearchRoot = "";
    }
    if (typeof liveSearchRootSubDir == "undefined") {
        liveSearchRootSubDir = "";
    }

    if (liveSearchLast != searchInput.value) {
    if (liveSearchReq && liveSearchReq.readyState < 4) {
        liveSearchReq.abort();
    }
    if ( searchInput.value == "") {
        liveSearchHide();
        return false;
    }

    // Do nothing as long as we have less then two characters - 
    // the search results makes no sense, and it's harder on the server.
    if ( searchInput.value.length < 2) {
        liveSearchHide();
        return false;
    }

    // Do we have cached results
    var result = _cache[searchInput.value];
    if (result) {
        showResult(result); 
        return;
    }
    liveSearchReq = new XMLHttpRequest();
    liveSearchReq.onreadystatechange= liveSearchProcessReqChange;
    // need to use encodeURIComponent instead of encodeURI, to escape +
    liveSearchReq.open("GET", liveSearchRoot + queryTarget + encodeURIComponent(searchInput.value) );
    liveSearchLast = searchInput.value;
    liveSearchReq.send(null);
    }
}

function showResult(result) {
  var  res = document.getElementById("LSResult");
  res.style.display = "block";
  var  sh = document.getElementById("LSShadow");
  sh.innerHTML = result;
}

function liveSearchProcessReqChange() {
    if (liveSearchReq.readyState == 4) {
        if (liveSearchReq.status > 299 || liveSearchReq.status < 200  ||
            liveSearchReq.responseText.length < 10) return; 
    showResult(liveSearchReq.responseText);
    _cache[liveSearchLast] = liveSearchReq.responseText;
    }
}

function liveSearchSubmit() {
    var highlight = document.getElementById("LSHighlight");
    
    if (highlight){
        target = highlight.getElementsByTagName('a')[0];
        window.location = liveSearchRoot + liveSearchRootSubDir + target;
        return false;
    } 
    else {
        return true;
    }
}



if (window.addEventListener) window.addEventListener("load",liveSearchInit,false);
else if (window.attachEvent) window.attachEvent("onload", liveSearchInit);



/* ----- fullscreenmode.js ----- */
function toggleFullScreenMode() {
    var body = cssQuery('body')[0];
    if(document.getElementById('icon-full_screen')) {
    var fsicon = document.getElementById('icon-full_screen'); }

    if (hasClassName(body, 'fullscreen')) {
        // unset cookie
        removeClassName(body, 'fullscreen');
        createCookie('fullscreenMode', '');
        if(fsicon) { fsicon.src = 'fullscreenexpand_icon.gif'; }
    } else {
        // set cookie
        addClassName(body, 'fullscreen');
        createCookie('fullscreenMode', '1');
        if(fsicon) { fsicon.src = 'fullscreencollapse_icon.gif'; }
    }
};

function fullscreenModeLoad() {
    if(document.getElementById('icon-full_screen')) {
    var fsicon = document.getElementById('icon-full_screen'); }
    // based on cookie
    if (readCookie('fullscreenMode') == '1') {
        var body = cssQuery('body')[0];
        addClassName(body, 'fullscreen');
        if(fsicon) { fsicon.src = 'fullscreencollapse_icon.gif'; }
    }
};
registerPloneFunction(fullscreenModeLoad)


/* ----- select_all.js ----- */
// Functions for selecting all checkboxes in folder_contents/search_form view
function selectAll(id, formName) {
    // Get the elements. if formName is provided, get the elements inside the form
    if (formName==null) {
        checkboxes = document.getElementsByName(id)
        for (i = 0; i < checkboxes.length; i++){
            checkboxes[i].checked = true ;
            }
    } else {
        for (i=0; i<document.forms[formName].elements.length;i++){
            if (document.forms[formName].elements[i].name==id){
                document.forms[formName].elements[i].checked=true; 
                }
            }
        }
    }
function deselectAll(id, formName) {
    if (formName==null) {
        checkboxes = document.getElementsByName(id)
        for (i = 0; i < checkboxes.length; i++){
            checkboxes[i].checked = false ;}
    } else {
        for (i=0; i<document.forms[formName].elements.length;i++){
            if (document.forms[formName].elements[i].name==id){
                document.forms[formName].elements[i].checked=false;
                }
            }
        }
    }
function toggleSelect(selectbutton, id, initialState, formName) {
    /* required selectbutton: you can pass any object that will function as a toggle
     * optional id: id of the the group of checkboxes that needs to be toggled (default=ids:list
     * optional initialState: initial state of the group. (default=false)
     * e.g. folder_contents is false, search_form=true because the item boxes
     * are checked initially.
     * optional formName: name of the form in which the boxes reside, use this if there are more
     * forms on the page with boxes with the same name
     */
    id=id || 'ids:list'  // defaults to ids:list, this is the most common usage

    if (selectbutton.isSelected==null){
        initialState=initialState || false;
        selectbutton.isSelected=initialState;
        }
    /* create and use a property on the button itself so you don't have to 
     * use a global variable and we can have as much groups on a page as we like.
     */
    if (selectbutton.isSelected == false) {
        selectbutton.setAttribute('src', portal_url + '/select_none_icon.gif');
        selectbutton.isSelected=true;
        return selectAll(id, formName);
    } else {
        selectbutton.setAttribute('src',portal_url + '/select_all_icon.gif');
        selectbutton.isSelected=false;
        return deselectAll(id, formName);
        }
    } 

/* ----- dropdown.js ----- */
/*
 * This is the code for the dropdown menus. It uses the following markup:
 *
 * <dl class="actionMenu" id="uniqueIdForThisMenu">
 *   <dt class="actionMenuHeader">
 *     <!-- The following a-tag needs to be clicked to dropdown the menu -->
 *     <a href="some_destination">A Title</a>
 *   </dt>
 *   <dd class="actionMenuContent">
 *     <!-- Here can be any content you want -->
 *   </dd>
 * </dl>
 *
 * When the menu is toggled, then the dl with the class actionMenu will get an
 * additional class which switches between 'activated' and 'deactivated'.
 * You can use this to style it accordingly, for example:
 *
 * .actionMenu.activated {
 *   display: block;
 * }
 *
 * .actionMenu.deactivated {
 *   display: none;
 * }
 *
 * When you click somewhere else than the menu, then all open menus will be
 * deactivated. When you move your mouse over the a-tag of another menu, then
 * that one will be activated and all others deactivated. When you click on a
 * link inside the actionMenuContent element, then the menu will be closed and
 * the link followed.
 *
 * This file uses functions from register_function.js, cssQuery.js and
 * nodeutils.js.
 *
 */

function isActionMenu(node) {
    if (hasClassName(node, 'actionMenu')) {
        return true;
    }
    return false;
};

function hideAllMenus() {
    var menus = cssQuery('dl.actionMenu');
    for (var i=0; i < menus.length; i++) {
        replaceClassName(menus[i], 'activated', 'deactivated', true);
    }
};

function toggleMenuHandler(event) {
    if (!event) var event = window.event; // IE compatibility

    // terminate if we hit a non-compliant DOM implementation
    // returning true, so the link is still followed
    if (!W3CDOM){return true;}

    var container = findContainer(this, isActionMenu);
    if (!container) {
        return true;
    }

    // check if the menu is visible
    if (hasClassName(container, 'activated')) {
        // it's visible - hide it
        replaceClassName(container, 'activated', 'deactivated', true);
    } else {
        // it's invisible - make it visible
        replaceClassName(container, 'deactivated', 'activated', true);
    }

    return false;
};

function hideMenusHandler(event) {
    if (!event) var event = window.event; // IE compatibility

    hideAllMenus();

    // we want to follow this link
    return true;
};

function actionMenuDocumentMouseDown(event) {
    if (!event) var event = window.event; // IE compatibility

    if (event.target)
        targ = event.target;
    else if (event.srcElement)
        targ = event.srcElement;

    var container = findContainer(targ, isActionMenu);
    if (container) {
        // targ is part of the menu, so just return and do the default
        return true;
    }

    hideAllMenus();

    return true;
};

function actionMenuMouseOver(event) {
    if (!event) var event = window.event; // IE compatibility

    if (!this.tagName && (this.tagName == 'A' || this.tagName == 'a')) {
        return true;
    }

    var container = findContainer(this, isActionMenu);
    if (!container) {
        return true;
    }
    var menu_id = container.id;

    var switch_menu = false;
    // hide all menus
    var menus = cssQuery('dl.actionMenu');
    for (var i=0; i < menus.length; i++) {
        var menu = menus[i]
        // check if the menu is visible
        if (hasClassName(menu, 'activated')) {
            switch_menu = true;
        }
        // turn off menu when it's not the current one
        if (menu.id != menu_id) {
            replaceClassName(menu, 'activated', 'deactivated', true);
        }
    }

    if (switch_menu) {
        var menu = cssQuery('#'+menu_id)[0];
        if (menu) {
            replaceClassName(menu, 'deactivated', 'activated', true);
        }
    }

    return true;
};

function initializeMenus() {
    // terminate if we hit a non-compliant DOM implementation
    if (!W3CDOM) {return false;}

    document.onmousedown = actionMenuDocumentMouseDown;

    hideAllMenus();

    // add toggle function to header links
    var menu_headers = cssQuery('dl.actionMenu > dt.actionMenuHeader > a');
    for (var i=0; i < menu_headers.length; i++) {
        var menu_header = menu_headers[i];

        menu_header.onclick = toggleMenuHandler;
        menu_header.onmouseover = actionMenuMouseOver;
    }

    // add hide function to all links in the dropdown, so the dropdown closes
    // when any link is clicked
    var menu_contents = cssQuery('dl.actionMenu > dd.actionMenuContent');
    for (var i=0; i < menu_contents.length; i++) {
        menu_contents[i].onclick = hideMenusHandler;
    }

    // uncomment to enable sorting of elements
    //var nodes = cssQuery('#objectMenu > dd.actionMenuContent li');
    //sortNodes(nodes, getInnerTextFast);
};

registerPloneFunction(initializeMenus);


/* ----- mark_special_links.js ----- */
/* Scan all links in the document and set classes on them if
 * they point outside the site, or are special protocols
 * To disable this effect for links on a one-by-one-basis,
 * give them a class of 'link-plain'
 */

function scanforlinks() {
    // terminate if we hit a non-compliant DOM implementation
    if (!W3CDOM) { return false; }

    contentarea = getContentArea();
    if (!contentarea) { return false; }

    links = contentarea.getElementsByTagName('a');
    for (i=0; i < links.length; i++) {
        if ( (links[i].getAttribute('href'))
             && (links[i].className.indexOf('link-plain')==-1) ) {
            var linkval = links[i].getAttribute('href');

            // check if the link href is a relative link, or an absolute link to
            // the current host.
            if (linkval.toLowerCase().indexOf(window.location.protocol
                                              + '//'
                                              + window.location.host)==0) {
                // absolute link internal to our host - do nothing
            } else if (linkval.indexOf('http:') != 0) {
                // not a http-link. Possibly an internal relative link, but also
                // possibly a mailto or other protocol add tests for relevant
                // protocols as you like.
                protocols = ['mailto', 'ftp', 'news', 'irc', 'h323', 'sip',
                             'callto', 'https', 'feed', 'webcal'];
                // h323, sip and callto are internet telephony VoIP protocols
                for (p=0; p < protocols.length; p++) {
                    if (linkval.indexOf(protocols[p]+':') == 0) {
                        // if the link matches one of the listed protocols, add
                        // className = link-protocol
                        wrapNode(links[i], 'span', 'link-'+protocols[p]);
                        break;
                    }
                }
            } else {
                // we are in here if the link points to somewhere else than our
                // site.
                if ( links[i].getElementsByTagName('img').length == 0 ) {
                    // we do not want to mess with those links that already have
                    // images in them
                    wrapNode(links[i], 'span', 'link-external');
                    // uncomment the next line if you want external links to be
                    // opened in a new window.
                    // links[i].setAttribute('target', '_blank');
                }
            }
        }
    }
};

registerPloneFunction(scanforlinks);


/* ----- collapsiblesections.js ----- */
/*
 * This is the code for the collapsibles. It uses the following markup:
 *
 * <dl class="collapsible">
 *   <dt class="collapsibleHeader">
 *     A Title
 *   </dt>
 *   <dd class="collapsibleContent">
 *     <!-- Here can be any content you want -->
 *   </dd>
 * </dl>
 *
 * When the collapsible is toggled, then the dl will get an additional class
 * which switches between 'collapsedBlockCollapsible' and
 * 'expandedBlockCollapsible'. You can use this to style it accordingly, for
 * example:
 *
 * .expandedBlockCollapsible .collapsibleContent {
 *   display: block;
 * }
 *
 * .collapsedBlockCollapsible .collapsibleContent {
 *   display: none;
 * }
 *
 * If you add the 'collapsedOnLoad' class to the dl, then it will get
 * collapsed on page load, this is done, so the content is accessible even when
 * javascript is disabled.
 *
 * If you add the 'inline' class to the dl, then it will toggle between
 * 'collapsedInlineCollapsible' and 'expandedInlineCollapsible' instead of
 * 'collapsedBlockCollapsible' and 'expandedBlockCollapsible'.
 *
 * This file uses functions from register_function.js, cssQuery.js and
 * nodeutils.js.
 *
 */

function isCollapsible(node) {
    if (hasClassName(node, 'collapsible')) {
        return true;
    }
    return false;
};

function toggleCollapsible(event) {
    if (!event) var event = window.event; // IE compatibility

    if (!this.tagName && (this.tagName == 'DT' || this.tagName == 'dt')) {
        return true;
    }

    var container = findContainer(this, isCollapsible);
    if (!container) {
        return true;
    }

    if (hasClassName(container, 'collapsedBlockCollapsible')) {
        replaceClassName(container, 'collapsedBlockCollapsible', 'expandedBlockCollapsible');
    } else if (hasClassName(container, 'expandedBlockCollapsible')) {
        replaceClassName(container, 'expandedBlockCollapsible', 'collapsedBlockCollapsible');
    } else if (hasClassName(container, 'collapsedInlineCollapsible')) {
        replaceClassName(container, 'collapsedInlineCollapsible', 'expandedInlineCollapsible');
    } else if (hasClassName(container, 'expandedInlineCollapsible')) {
        replaceClassName(container, 'expandedInlineCollapsible', 'collapsedInlineCollapsible');
    }
};

function activateCollapsibles() {
    if (!W3CDOM) {return false;}

    var collapsibles = cssQuery('dl.collapsible');
    for (var i=0; i < collapsibles.length; i++) {
        var collapsible = collapsibles[i];

        var collapsible_header = cssQuery('dt.collapsibleHeader', collapsible)[0];
        collapsible_header.onclick = toggleCollapsible;

        if (hasClassName(collapsible, 'inline')) {
            // the collapsible should be inline
            if (hasClassName(collapsible, 'collapsedOnLoad')) {
                replaceClassName(collapsible, 'collapsedOnLoad', 'collapsedInlineCollapsible');
            } else {
                addClassName(collapsible, 'expandedInlineCollapsible');
            }
        } else {
            // the collapsible is a block
            if (hasClassName(collapsible, 'collapsedOnLoad')) {
                replaceClassName(collapsible, 'collapsedOnLoad', 'collapsedBlockCollapsible');
            } else {
                addClassName(collapsible, 'expandedBlockCollapsible');
            }
        }
    }
};

registerPloneFunction(activateCollapsibles);

function tog() {
  // tog: toggle the visibility of html elements (arguments[1..]) from none to
  // arguments[0].  Return what should be returned in a javascript onevent().
  display = arguments[0];
  for( var i=1; i<arguments.length; i++ ) {    
    var x = document.getElementById(arguments[i]);
    if (!x) continue;
    if (x.style.display == "none" || x.style.display == "") {
      x.style.display = display;
    } else {
      x.style.display = "none";
    }
  } 

  var e = is_ie ? window.event : this;
  if (e) {
    if (is_ie) {
      e.cancelBubble = true;
      e.returnValue = false;
      return false;
    } else {
      return false;
    }
  }
}

/*****************************************************************************
 *
 * Sarissa XML library version 0.9.6
 * Copyright (c) 2003 Manos Batsis, 
 * mailto: mbatsis at users full stop sourceforge full stop net
 * This software is distributed under the Kupu License. See
 * LICENSE.txt for license text. See the Sarissa homepage at
 * http://sarissa.sourceforge.net for more information.
 *
 *****************************************************************************

 * ====================================================================
 * About
 * ====================================================================
 * Sarissa cross browser XML library 
 * @version 0.9.6
 * @author: Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
 *
 * Sarissa is an ECMAScript library acting as a cross-browser wrapper for native XML APIs.
 * The library supports Gecko based browsers like Mozilla and Firefox,
 * Internet Explorer (5.5+ with MSXML3.0+) and, last but not least, KHTML based browsers like
 * Konqueror and Safari.
 *
 */
/**
 * <p>Sarissa is a utility class. Provides static methods for DOMDocument and 
 * XMLHTTP objects, DOM Node serializatrion to XML strings and other goodies.</p>
 * @constructor
 */
function Sarissa(){};
/** @private */
Sarissa.PARSED_OK = "Document contains no parsing errors";
/**
 * Tells you whether transformNode and transformNodeToObject are available. This functionality
 * is contained in sarissa_ieemu_xslt.js and is deprecated. If you want to control XSLT transformations
 * use the XSLTProcessor
 * @deprecated
 * @type boolean
 */
Sarissa.IS_ENABLED_TRANSFORM_NODE = false;
/**
 * tells you whether XMLHttpRequest (or equivalent) is available
 * @type boolean
 */
Sarissa.IS_ENABLED_XMLHTTP = false;
/**
 * tells you whether selectNodes/selectSingleNode is available
 * @type boolean
 */
Sarissa.IS_ENABLED_SELECT_NODES = false;
var _sarissa_iNsCounter = 0;
var _SARISSA_IEPREFIX4XSLPARAM = "";
var _SARISSA_HAS_DOM_IMPLEMENTATION = document.implementation && true;
var _SARISSA_HAS_DOM_CREATE_DOCUMENT = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.createDocument;
var _SARISSA_HAS_DOM_FEATURE = _SARISSA_HAS_DOM_IMPLEMENTATION && document.implementation.hasFeature;
var _SARISSA_IS_MOZ = _SARISSA_HAS_DOM_CREATE_DOCUMENT && _SARISSA_HAS_DOM_FEATURE;
var _SARISSA_IS_SAFARI = navigator.userAgent.toLowerCase().indexOf("applewebkit") != -1;
var _SARISSA_IS_IE = document.all && window.ActiveXObject && navigator.userAgent.toLowerCase().indexOf("msie") > -1  && navigator.userAgent.toLowerCase().indexOf("opera") == -1;

if(!window.Node || !window.Node.ELEMENT_NODE){
    var Node = {ELEMENT_NODE: 1, ATTRIBUTE_NODE: 2, TEXT_NODE: 3, CDATA_SECTION_NODE: 4, ENTITY_REFERENCE_NODE: 5,  ENTITY_NODE: 6, PROCESSING_INSTRUCTION_NODE: 7, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_TYPE_NODE: 10, DOCUMENT_FRAGMENT_NODE: 11, NOTATION_NODE: 12};
};

// IE initialization
if(_SARISSA_IS_IE){
    // for XSLT parameter names, prefix needed by IE
    _SARISSA_IEPREFIX4XSLPARAM = "xsl:";
    // used to store the most recent ProgID available out of the above
    var _SARISSA_DOM_PROGID = "";
    var _SARISSA_XMLHTTP_PROGID = "";
    /**
     * Called when the Sarissa_xx.js file is parsed, to pick most recent
     * ProgIDs for IE, then gets destroyed.
     * @param idList an array of MSXML PROGIDs from which the most recent will be picked for a given object
     * @param enabledList an array of arrays where each array has two items; the index of the PROGID for which a certain feature is enabled
     */
    pickRecentProgID = function (idList, enabledList){
        // found progID flag
        var bFound = false;
        for(var i=0; i < idList.length && !bFound; i++){
            try{
                var oDoc = new ActiveXObject(idList[i]);
                o2Store = idList[i];
                bFound = true;
                for(var j=0;j<enabledList.length;j++)
                    if(i <= enabledList[j][1])
                        Sarissa["IS_ENABLED_"+enabledList[j][0]] = true;
            }catch (objException){
                // trap; try next progID
            };
        };
        if (!bFound)
            throw "Could not retreive a valid progID of Class: " + idList[idList.length-1]+". (original exception: "+e+")";
        idList = null;
        return o2Store;
    };
    // pick best available MSXML progIDs
    _SARISSA_DOM_PROGID = pickRecentProgID(["Msxml2.DOMDocument.5.0", "Msxml2.DOMDocument.4.0", "Msxml2.DOMDocument.3.0", "MSXML2.DOMDocument", "MSXML.DOMDocument", "Microsoft.XMLDOM"], [["SELECT_NODES", 2],["TRANSFORM_NODE", 2]]);
    _SARISSA_XMLHTTP_PROGID = pickRecentProgID(["Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"], [["XMLHTTP", 4]]);
    _SARISSA_THREADEDDOM_PROGID = pickRecentProgID(["Msxml2.FreeThreadedDOMDocument.5.0", "MSXML2.FreeThreadedDOMDocument.4.0", "MSXML2.FreeThreadedDOMDocument.3.0"]);
    _SARISSA_XSLTEMPLATE_PROGID = pickRecentProgID(["Msxml2.XSLTemplate.5.0", "Msxml2.XSLTemplate.4.0", "MSXML2.XSLTemplate.3.0"], [["XSLTPROC", 2]]);
    // we dont need this anymore
    pickRecentProgID = null;
    //============================================
    // Factory methods (IE)
    //============================================
    // see non-IE version
    Sarissa.getDomDocument = function(sUri, sName){
        var oDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
        // if a root tag name was provided, we need to load it in the DOM
        // object
        if (sName){
            // if needed, create an artifical namespace prefix the way Moz
            // does
            if (sUri){
                oDoc.loadXML("<a" + _sarissa_iNsCounter + ":" + sName + " xmlns:a" + _sarissa_iNsCounter + "=\"" + sUri + "\" />");
                // don't use the same prefix again
                ++_sarissa_iNsCounter;
            }
            else
                oDoc.loadXML("<" + sName + "/>");
        };
        return oDoc;
    };
    // see non-IE version   
    Sarissa.getParseErrorText = function (oDoc) {
        var parseErrorText = Sarissa.PARSED_OK;
        if(oDoc.parseError != 0){
            parseErrorText = "XML Parsing Error: " + oDoc.parseError.reason + 
                "\nLocation: " + oDoc.parseError.url + 
                "\nLine Number " + oDoc.parseError.line + ", Column " + 
                oDoc.parseError.linepos + 
                ":\n" + oDoc.parseError.srcText +
                "\n";
            for(var i = 0;  i < oDoc.parseError.linepos;i++){
                parseErrorText += "-";
            };
            parseErrorText +=  "^\n";
        };
        return parseErrorText;
    };
    // see non-IE version
    Sarissa.setXpathNamespaces = function(oDoc, sNsSet) {
        oDoc.setProperty("SelectionLanguage", "XPath");
        oDoc.setProperty("SelectionNamespaces", sNsSet);
    };   
    /**
     * Basic implementation of Mozilla's XSLTProcessor for IE. 
     * Reuses the same XSLT stylesheet for multiple transforms
     * @constructor
     */
    XSLTProcessor = function(){
        this.template = new ActiveXObject(_SARISSA_XSLTEMPLATE_PROGID);
        this.processor = null;
    };
    /**
     * Impoprts the given XSLT DOM and compiles it to a reusable transform
     * @argument xslDoc The XSLT DOMDocument to import
     */
    XSLTProcessor.prototype.importStylesheet = function(xslDoc){
        // convert stylesheet to free threaded
        var converted = new ActiveXObject(_SARISSA_THREADEDDOM_PROGID); 
        converted.loadXML(xslDoc.xml);
        this.template.stylesheet = converted;
        this.processor = this.template.createProcessor();
        // (re)set default param values
        this.paramsSet = new Array();
    };
    /**
     * Transform the given XML DOM
     * @argument sourceDoc The XML DOMDocument to transform
     * @return The transformation result as a DOM Document
     */
    XSLTProcessor.prototype.transformToDocument = function(sourceDoc){
        this.processor.input = sourceDoc;
        var outDoc = new ActiveXObject(_SARISSA_DOM_PROGID);
        this.processor.output = outDoc; 
        this.processor.transform();
        return outDoc;
    };
    /**
     * Not sure if this works in IE. Maybe this will allow non-well-formed
     * transformation results (i.e. with no single root element)
     * @argument sourceDoc The XML DOMDocument to transform
     * @return The transformation result as a DOM Fragment
     */
    XSLTProcessor.prototype.transformToFragment = function(sourceDoc, ownerDocument){
        return this.transformToDocument(sourceDoc);
    };
    /**
     * Set global XSLT parameter of the imported stylesheet
     * @argument nsURI The parameter namespace URI
     * @argument name The parameter base name
     * @argument value The new parameter value
     */
    XSLTProcessor.prototype.setParameter = function(nsURI, name, value){
        /* nsURI is optional but cannot be null */
        if(nsURI){
            this.processor.addParameter(name, value, nsURI);
        }else{
            this.processor.addParameter(name, value);
        };
        /* update updated params for getParameter */
        if(!this.paramsSet[""+nsURI]){
            this.paramsSet[""+nsURI] = new Array();
        };
        this.paramsSet[""+nsURI][name] = value;
    };
    /**
     * Gets a parameter if previously set by setParameter. Returns null
     * otherwise
     * @argument name The parameter base name
     * @argument value The new parameter value
     * @return The parameter value if reviously set by setParameter, null otherwise
     */
    XSLTProcessor.prototype.getParameter = function(nsURI, name){
        if(this.paramsSet[""+nsURI] && this.paramsSet[""+nsURI][name])
            return this.paramsSet[""+nsURI][name];
        else
            return null;
    };
}
else{ /* end IE initialization, try to deal with real browsers now ;-) */
   if(_SARISSA_HAS_DOM_CREATE_DOCUMENT){
        if(window.XMLDocument){
            /**
            * <p>Emulate IE's onreadystatechange attribute</p>
            */
            XMLDocument.prototype.onreadystatechange = null;
            /**
            * <p>Emulates IE's readyState property, which always gives an integer from 0 to 4:</p>
            * <ul><li>1 == LOADING,</li>
            * <li>2 == LOADED,</li>
            * <li>3 == INTERACTIVE,</li>
            * <li>4 == COMPLETED</li></ul>
            */
            XMLDocument.prototype.readyState = 0;
            /**
            * <p>Emulate IE's parseError attribute</p>
            */
            XMLDocument.prototype.parseError = 0;

            // NOTE: setting async to false will only work with documents
            // called over HTTP (meaning a server), not the local file system,
            // unless you are using Moz 1.4+.
            // BTW the try>catch block is for 1.4; I haven't found a way to check if
            // the property is implemented without
            // causing an error and I dont want to use user agent stuff for that...
            var _SARISSA_SYNC_NON_IMPLEMENTED = false;
            try{
                /**
                * <p>Emulates IE's async property for Moz versions prior to 1.4.
                * It controls whether loading of remote XML files works
                * synchronously or asynchronously.</p>
                */
                XMLDocument.prototype.async = true;
                _SARISSA_SYNC_NON_IMPLEMENTED = true;
            }catch(e){/* trap */};
            /**
            * <p>Keeps a handle to the original load() method. Internal use and only
            * if Mozilla version is lower than 1.4</p>
            * @private
            */
            XMLDocument.prototype._sarissa_load = XMLDocument.prototype.load;

            /**
            * <p>Overrides the original load method to provide synchronous loading for
            * Mozilla versions prior to 1.4, using an XMLHttpRequest object (if
            * async is set to false)</p>
            * @returns the DOM Object as it was before the load() call (may be  empty)
            */
            XMLDocument.prototype.load = function(sURI) {
                var oDoc = document.implementation.createDocument("", "", null);
                Sarissa.copyChildNodes(this, oDoc);
                this.parseError = 0;
                Sarissa.__setReadyState__(this, 1);
                try {
                    if(this.async == false && _SARISSA_SYNC_NON_IMPLEMENTED) {
                        var tmp = new XMLHttpRequest();
                        tmp.open("GET", sURI, false);
                        tmp.send(null);
                        Sarissa.__setReadyState__(this, 2);
                        Sarissa.copyChildNodes(tmp.responseXML, this);
                        Sarissa.__setReadyState__(this, 3);
                    }
                    else {
                        this._sarissa_load(sURI);
                    };
                }
                catch (objException) {
                    this.parseError = -1;
                }
                finally {
                    if(this.async == false){
                        Sarissa.__handleLoad__(this);
                    };
                };
                return oDoc;
            };
        };//if(window.XMLDocument)

        /**
         * <p>Ensures the document was loaded correctly, otherwise sets the
         * parseError to -1 to indicate something went wrong. Internal use</p>
         * @private
         */
        Sarissa.__handleLoad__ = function(oDoc){
            if (!oDoc.documentElement || oDoc.documentElement.tagName == "parsererror")
                oDoc.parseError = -1;
            Sarissa.__setReadyState__(oDoc, 4);
        };
        
        /**
        * <p>Attached by an event handler to the load event. Internal use.</p>
        * @private
        */
        _sarissa_XMLDocument_onload = function(){
            Sarissa.__handleLoad__(this);
        };
        
        /**
         * <p>Sets the readyState property of the given DOM Document object.
         * Internal use.</p>
         * @private
         * @argument oDoc the DOM Document object to fire the
         *          readystatechange event
         * @argument iReadyState the number to change the readystate property to
         */
        Sarissa.__setReadyState__ = function(oDoc, iReadyState){
            oDoc.readyState = iReadyState;
            if (oDoc.onreadystatechange != null && typeof oDoc.onreadystatechange == "function")
                oDoc.onreadystatechange();
        };
        /**
        * <p>Factory method to obtain a new DOM Document object</p>
        * @argument sUri the namespace of the root node (if any)
        * @argument sUri the local name of the root node (if any)
        * @returns a new DOM Document
        */
        Sarissa.getDomDocument = function(sUri, sName){
            var oDoc = document.implementation.createDocument(sUri?sUri:"", sName?sName:"", null);
            oDoc.addEventListener("load", _sarissa_XMLDocument_onload, false);
            return oDoc;
        };        
    };//if(_SARISSA_HAS_DOM_CREATE_DOCUMENT)
};
//==========================================
// Common stuff
//==========================================
if(!window.DOMParser){
    /** 
    * DOMParser is a utility class, used to construct DOMDocuments from XML strings
    * @constructor
    */
    DOMParser = function() {
    };
    /** 
    * Construct a new DOM Document from the given XMLstring
    * @param sXml the given XML string
    * @param contentType the content type of the document the given string represents (one of text/xml, application/xml, application/xhtml+xml). 
    * @return a new DOM Document from the given XML string
    */
    DOMParser.prototype.parseFromString = function(sXml, contentType){
        var doc = Sarissa.getDomDocument();
        doc.loadXML(sXml);
        return doc;
    };
    
};

if(window.XMLHttpRequest){
    Sarissa.IS_ENABLED_XMLHTTP = true;
}
else if(_SARISSA_IS_IE){
    /**
     * Emulate XMLHttpRequest
     * @constructor
     */
    XMLHttpRequest = function() {
        return new ActiveXObject(_SARISSA_XMLHTTP_PROGID);
    };
    Sarissa.IS_ENABLED_XMLHTTP = true;
};

if(!window.document.importNode && _SARISSA_IS_IE){
    try{
        /**
        * Implements importNode for the current window document in IE using innerHTML.
        * Testing showed that DOM was multiple times slower than innerHTML for this,
        * sorry folks. If you encounter trouble (who knows what IE does behind innerHTML)
        * please gimme a call.
        * @param oNode the Node to import
        * @param bChildren whether to include the children of oNode
        * @returns the imported node for further use
        */
        window.document.importNode = function(oNode, bChildren){
            var importNode = document.createElement("div");
            if(bChildren)
                importNode.innerHTML = Sarissa.serialize(oNode);
            else
                importNode.innerHTML = Sarissa.serialize(oNode.cloneNode(false));
            return importNode.firstChild;
        };
        }catch(e){};
};
if(!Sarissa.getParseErrorText){
    /**
     * <p>Returns a human readable description of the parsing error. Usefull
     * for debugging. Tip: append the returned error string in a &lt;pre&gt;
     * element if you want to render it.</p>
     * <p>Many thanks to Christian Stocker for the initial patch.</p>
     * @argument oDoc The target DOM document
     * @returns The parsing error description of the target Document in
     *          human readable form (preformated text)
     */
    Sarissa.getParseErrorText = function (oDoc){
        var parseErrorText = Sarissa.PARSED_OK;
        if(oDoc.parseError != 0){
            /*moz*/
            if(oDoc.documentElement.tagName == "parsererror"){
                parseErrorText = oDoc.documentElement.firstChild.data;
                parseErrorText += "\n" +  oDoc.documentElement.firstChild.nextSibling.firstChild.data;
            }/*konq*/
            else if(oDoc.documentElement.tagName == "html"){
                parseErrorText = Sarissa.getText(oDoc.documentElement.getElementsByTagName("h1")[0], false) + "\n";
                parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("body")[0], false) + "\n";
                parseErrorText += Sarissa.getText(oDoc.documentElement.getElementsByTagName("pre")[0], false);
            };
        };
        return parseErrorText;
    };
};
Sarissa.getText = function(oNode, deep){
    var s = "";
    var nodes = oNode.childNodes;
    for(var i=0; i < nodes.length; i++){
        var node = nodes[i];
        var nodeType = node.nodeType;
        if(nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE){
            s += node.data;
        }
        else if(deep == true
                    && (nodeType == Node.ELEMENT_NODE
                        || nodeType == Node.DOCUMENT_NODE
                        || nodeType == Node.DOCUMENT_FRAGMENT_NODE)){
            s += Sarissa.getText(node, true);
        };
    };
    return s;
};
if(window.XMLSerializer){
    /**
     * <p>Factory method to obtain the serialization of a DOM Node</p>
     * @returns the serialized Node as an XML string
     */
    Sarissa.serialize = function(oDoc){
        return (new XMLSerializer()).serializeToString(oDoc);
    };
}else{
    if((Sarissa.getDomDocument("","foo", null)).xml){
        // see non-IE version
        Sarissa.serialize = function(oDoc) {
            // TODO: check for HTML document and return innerHTML instead
            return oDoc.xml;
        };
        /**
         * Utility class to serialize DOM Node objects to XML strings
         * @constructor
         */
        XMLSerializer = function(){};
        /**
         * Serialize the given DOM Node to an XML string
         * @param oNode the DOM Node to serialize
         */
        XMLSerializer.prototype.serializeToString = function(oNode) {
            return oNode.xml;
        };
    };
};

/**
 * strips tags from a markup string
 */
Sarissa.stripTags = function (s) {
    return s.replace(/<[^>]+>/g,"");
};
/**
 * <p>Deletes all child nodes of the given node</p>
 * @argument oNode the Node to empty
 */
Sarissa.clearChildNodes = function(oNode) {
    while(oNode.hasChildNodes()){
        oNode.removeChild(oNode.firstChild);
    };
};
/**
 * <p> Copies the childNodes of nodeFrom to nodeTo</p>
 * <p> <b>Note:</b> The second object's original content is deleted before 
 * the copy operation, unless you supply a true third parameter</p>
 * @argument nodeFrom the Node to copy the childNodes from
 * @argument nodeTo the Node to copy the childNodes to
 * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is false
 */
Sarissa.copyChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
    if(!bPreserveExisting){
        Sarissa.clearChildNodes(nodeTo);
    };
    var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
    var nodes = nodeFrom.childNodes;
    if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
        for(var i=0;i < nodes.length;i++) {
            nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
        };
    }
    else{
        for(var i=0;i < nodes.length;i++) {
            nodeTo.appendChild(nodes[i].cloneNode(true));
        };
    };
};

/**
 * <p> Moves the childNodes of nodeFrom to nodeTo</p>
 * <p> <b>Note:</b> The second object's original content is deleted before 
 * the move operation, unless you supply a true third parameter</p>
 * @argument nodeFrom the Node to copy the childNodes from
 * @argument nodeTo the Node to copy the childNodes to
 * @argument bPreserveExisting whether to preserve the original content of nodeTo, default is false
 */
Sarissa.moveChildNodes = function(nodeFrom, nodeTo, bPreserveExisting) {
    if(!bPreserveExisting){
        Sarissa.clearChildNodes(nodeTo);
    };
    
    var nodes = nodeFrom.childNodes;
    // if within the same doc, just move, else copy and delete
    if(nodeFrom.ownerDocument == nodeTo.ownerDocument){
        nodeTo.appendChild(nodes[i]);
    }else{
        var ownerDoc = nodeTo.nodeType == Node.DOCUMENT_NODE ? nodeTo : nodeTo.ownerDocument;
         if(ownerDoc.importNode && (!_SARISSA_IS_IE)) {
            for(var i=0;i < nodes.length;i++) {
                nodeTo.appendChild(ownerDoc.importNode(nodes[i], true));
            };
        }
        else{
            for(var i=0;i < nodes.length;i++) {
                nodeTo.appendChild(nodes[i].cloneNode(true));
            };
        };
        Sarissa.clearChildNodes(nodeFrom);
    };
    
};

/** 
 * <p>Serialize any object to an XML string. All properties are serialized using the property name
 * as the XML element name. Array elements are rendered as <code>array-item</code> elements, 
 * using their index/key as the value of the <code>key</code> attribute.</p>
 * @argument anyObject the object to serialize
 * @argument objectName a name for that object
 * @return the XML serializationj of the given object as a string
 */
Sarissa.xmlize = function(anyObject, objectName, indentSpace){
    indentSpace = indentSpace?indentSpace:'';
    var s = indentSpace  + '<' + objectName + '>';
    var isLeaf = false;
    if(!(anyObject instanceof Object) || anyObject instanceof Number || anyObject instanceof String 
        || anyObject instanceof Boolean || anyObject instanceof Date){
        s += Sarissa.escape(""+anyObject);
        isLeaf = true;
    }else{
        s += "\n";
        var itemKey = '';
        var isArrayItem = anyObject instanceof Array;
        for(var name in anyObject){
            s += Sarissa.xmlize(anyObject[name], (isArrayItem?"array-item key=\""+name+"\"":name), indentSpace + "   ");
        };
        s += indentSpace;
    };
    return s += (objectName.indexOf(' ')!=-1?"</array-item>\n":"</" + objectName + ">\n");
};

/** 
 * Escape the given string chacters that correspond to the five predefined XML entities
 * @param sXml the string to escape
 */
Sarissa.escape = function(sXml){
    return sXml.replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
};

/** 
 * Unescape the given string. This turns the occurences of the predefined XML 
 * entities to become the characters they represent correspond to the five predefined XML entities
 * @param sXml the string to unescape
 */
Sarissa.unescape = function(sXml){
    return sXml.replace(/&apos;/g,"'")
        .replace(/&quot;/g,"\"")
        .replace(/&gt;/g,">")
        .replace(/&lt;/g,"<")
        .replace(/&amp;/g,"&");
};
//   EOF

/*****************************************************************************
 *
 * Sarissa XML library version 0.9.6
 * Copyright (c) 2003 Manos Batsis, 
 * mailto: mbatsis at users full stop sourceforge full stop net
 * This software is distributed under the Kupu License. See
 * LICENSE.txt for license text. See the Sarissa homepage at
 * http://sarissa.sourceforge.net for more information.
 *
 *****************************************************************************

 * ====================================================================
 * About
 * ====================================================================
 * Sarissa cross browser XML library - IE XPath Emulation 
 * @version 0.9.6
 * @author: Manos Batsis, mailto: mbatsis at users full stop sourceforge full stop net
 *
 * This script emulates Internet Explorer's selectNodes and selectSingleNode
 * for Mozilla. Associating namespace prefixes with URIs for your XPath queries
 * is easy with IE's setProperty. 
 * USers may also map a namespace prefix to a default (unprefixed) namespace in the
 * source document with Sarissa.setXpathNamespaces
 *
 */
if(_SARISSA_HAS_DOM_FEATURE && document.implementation.hasFeature("XPath", "3.0")){
    /**
    * <p>SarissaNodeList behaves as a NodeList but is only used as a result to <code>selectNodes</code>,
    * so it also has some properties IEs proprietery object features.</p>
    * @private
    * @constructor
    * @argument i the (initial) list size
    */
    function SarissaNodeList(i){
        this.length = i;
    };
    /** <p>Set an Array as the prototype object</p> */
    SarissaNodeList.prototype = new Array(0);
    /** <p>Inherit the Array constructor </p> */
    SarissaNodeList.prototype.constructor = Array;
    /**
    * <p>Returns the node at the specified index or null if the given index
    * is greater than the list size or less than zero </p>
    * <p><b>Note</b> that in ECMAScript you can also use the square-bracket
    * array notation instead of calling <code>item</code>
    * @argument i the index of the member to return
    * @returns the member corresponding to the given index
    */
    SarissaNodeList.prototype.item = function(i) {
        return (i < 0 || i >= this.length)?null:this[i];
    };
    /**
    * <p>Emulate IE's expr property
    * (Here the SarissaNodeList object is given as the result of selectNodes).</p>
    * @returns the XPath expression passed to selectNodes that resulted in
    *          this SarissaNodeList
    */
    SarissaNodeList.prototype.expr = "";
    /** dummy, used to accept IE's stuff without throwing errors */
    XMLDocument.prototype.setProperty  = function(x,y){};
    /**
    * <p>Programmatically control namespace URI/prefix mappings for XPath
    * queries.</p>
    * <p>This method comes especially handy when used to apply XPath queries
    * on XML documents with a default namespace, as there is no other way
    * of mapping that to a prefix.</p>
    * <p>Using no namespace prefix in DOM Level 3 XPath queries, implies you
    * are looking for elements in the null namespace. If you need to look
    * for nodes in the default namespace, you need to map a prefix to it
    * first like:</p>
    * <pre>Sarissa.setXpathNamespaces(oDoc, &quot;xmlns:myprefix=&amp;aposhttp://mynsURI&amp;apos&quot;);</pre>
    * <p><b>Note 1 </b>: Use this method only if the source document features
    * a default namespace (without a prefix), otherwise just use IE's setProperty
    * (moz will rezolve non-default namespaces by itself). You will need to map that
    * namespace to a prefix for queries to work.</p>
    * <p><b>Note 2 </b>: This method calls IE's setProperty method to set the
    * appropriate namespace-prefix mappings, so you dont have to do that.</p>
    * @param oDoc The target XMLDocument to set the namespace mappings for.
    * @param sNsSet A whilespace-seperated list of namespace declarations as
    *            those would appear in an XML document. E.g.:
    *            <code>&quot;xmlns:xhtml=&apos;http://www.w3.org/1999/xhtml&apos;
    * xmlns:&apos;http://www.w3.org/1999/XSL/Transform&apos;&quot;</code>
    * @throws An error if the format of the given namespace declarations is bad.
    */
    Sarissa.setXpathNamespaces = function(oDoc, sNsSet) {
        //oDoc._sarissa_setXpathNamespaces(sNsSet);
        oDoc._sarissa_useCustomResolver = true;
        var namespaces = sNsSet.indexOf(" ")>-1?sNsSet.split(" "):new Array(sNsSet);
        oDoc._sarissa_xpathNamespaces = new Array(namespaces.length);
        for(var i=0;i < namespaces.length;i++){
            var ns = namespaces[i];
            var colonPos = ns.indexOf(":");
            var assignPos = ns.indexOf("=");
            if(colonPos == 5 && assignPos > colonPos+2){
                var prefix = ns.substring(colonPos+1, assignPos);
                var uri = ns.substring(assignPos+2, ns.length-1);
                oDoc._sarissa_xpathNamespaces[prefix] = uri;
            }else{
                throw "Bad format on namespace declaration(s) given";
            };
        };
    };
    /**
    * @private Flag to control whether a custom namespace resolver should
    *          be used, set to true by Sarissa.setXpathNamespaces
    */
    XMLDocument.prototype._sarissa_useCustomResolver = false;
    /** @private */
    XMLDocument.prototype._sarissa_xpathNamespaces = new Array();
    /**
    * <p>Extends the XMLDocument to emulate IE's selectNodes.</p>
    * @argument sExpr the XPath expression to use
    * @argument contextNode this is for internal use only by the same
    *           method when called on Elements
    * @returns the result of the XPath search as a SarissaNodeList
    * @throws An error if no namespace URI is found for the given prefix.
    */
    XMLDocument.prototype.selectNodes = function(sExpr, contextNode){
        var nsDoc = this;
        var nsresolver = this._sarissa_useCustomResolver
        ? function(prefix){
            var s = nsDoc._sarissa_xpathNamespaces[prefix];
            if(s)return s;
            else throw "No namespace URI found for prefix: '" + prefix+"'";
            }
        : this.createNSResolver(this.documentElement);
            var oResult = this.evaluate(sExpr,
                    (contextNode?contextNode:this),
                    nsresolver,
                    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        var nodeList = new SarissaNodeList(oResult.snapshotLength);
        nodeList.expr = sExpr;
        for(var i=0;i<nodeList.length;i++)
            nodeList[i] = oResult.snapshotItem(i);
        return nodeList;
    };
    /**
    * <p>Extends the Element to emulate IE's selectNodes</p>
    * @argument sExpr the XPath expression to use
    * @returns the result of the XPath search as an (Sarissa)NodeList
    * @throws An
    *             error if invoked on an HTML Element as this is only be
    *             available to XML Elements.
    */
    Element.prototype.selectNodes = function(sExpr){
        var doc = this.ownerDocument;
        if(doc.selectNodes)
            return doc.selectNodes(sExpr, this);
        else
            throw "Method selectNodes is only supported by XML Elements";
    };
    /**
    * <p>Extends the XMLDocument to emulate IE's selectSingleNodes.</p>
    * @argument sExpr the XPath expression to use
    * @argument contextNode this is for internal use only by the same
    *           method when called on Elements
    * @returns the result of the XPath search as an (Sarissa)NodeList
    */
    XMLDocument.prototype.selectSingleNode = function(sExpr, contextNode){
        var ctx = contextNode?contextNode:null;
        sExpr = "("+sExpr+")[1]";
        var nodeList = this.selectNodes(sExpr, ctx);
        if(nodeList.length > 0)
            return nodeList.item(0);
        else
            return null;
    };
    /**
    * <p>Extends the Element to emulate IE's selectNodes.</p>
    * @argument sExpr the XPath expression to use
    * @returns the result of the XPath search as an (Sarissa)NodeList
    * @throws An error if invoked on an HTML Element as this is only be
    *             available to XML Elements.
    */
    Element.prototype.selectSingleNode = function(sExpr){
        var doc = this.ownerDocument;
        if(doc.selectSingleNode)
            return doc.selectSingleNode(sExpr, this);
        else
            throw "Method selectNodes is only supported by XML Elements";
    };
    Sarissa.IS_ENABLED_SELECT_NODES = true;
};

window._ = function(msgid, interpolations) {
    /* dummy _ function for systems that don't want to use i18n */
    if (interpolations) {
        for (var id in interpolations) {
            var value = interpolations[id];
            var reg = new RegExp('\\\$\\\{' + id + '\\\}', 'g');
            msgid = msgid.replace(reg, ""+value);
        };
    };
    return msgid;
};

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupuhelpers.js 25040 2006-03-27 14:49:21Z fschulze $

/*

Some notes about the scripts:

- Problem with bound event handlers:
    
    When a method on an object is used as an event handler, the method uses 
    its reference to the object it is defined on. The 'this' keyword no longer
    points to the class, but instead refers to the element on which the event
    is bound. To overcome this problem, you can wrap the method in a class that
    holds a reference to the object and have a method on the wrapper that calls
    the input method in the input object's context. This wrapped method can be
    used as the event handler. An example:

    class Foo() {
        this.foo = function() {
            // the method used as an event handler
            // using this here wouldn't work if the method
            // was passed to addEventListener directly
            this.baz();
        };
        this.baz = function() {
            // some method on the same object
        };
    };

    f = new Foo();

    // create the wrapper for the function, args are func, context
    wrapper = new ContextFixer(f.foo, f);

    // the wrapper can be passed to addEventListener, 'this' in the method
    // will be pointing to the right context.
    some_element.addEventListener("click", wrapper.execute, false);

- Problem with window.setTimeout:

    The window.setTimeout function has a couple of problems in usage, all 
    caused by the fact that it expects a *string* argument that will be
    evalled in the global namespace rather than a function reference with
    plain variables as arguments. This makes that the methods on 'this' can
    not be called (the 'this' variable doesn't exist in the global namespace)
    and references to variables in the argument list aren't allowed (since
    they don't exist in the global namespace). To overcome these problems, 
    there's now a singleton instance of a class called Timer, which has one 
    public method called registerFunction. This can be called with a function
    reference and a variable number of extra arguments to pass on to the 
    function.

    Usage:

        timer_instance.registerFunction(this, this.myFunc, 10, 'foo', bar);

        will call this.myFunc('foo', bar); in 10 milliseconds (with 'this'
        as its context).

*/

//----------------------------------------------------------------------------
// Helper classes and functions
//----------------------------------------------------------------------------

function addEventHandler(element, event, method, context) {
    /* method to add an event handler for both IE and Mozilla */
    var wrappedmethod = new ContextFixer(method, context);
    var args = new Array(null, null);
    for (var i=4; i < arguments.length; i++) {
        args.push(arguments[i]);
    };
    wrappedmethod.args = args;
    try {
        if (_SARISSA_IS_MOZ) {
            element.addEventListener(event, wrappedmethod.execute, false);
        } else if (_SARISSA_IS_IE) {
            element.attachEvent("on" + event, wrappedmethod.execute);
        } else {
            throw _("Unsupported browser!");
        };
        return wrappedmethod.execute;
    } catch(e) {
        alert(_('exception ${message} while registering an event handler ' +
                'for element ${element}, event ${event}, method ${method}',
                {'message': e.message, 'element': element,
                    'event': event,
                    'method': method}));
    };
};

function removeEventHandler(element, event, method) {
    /* method to remove an event handler for both IE and Mozilla */
    if (_SARISSA_IS_MOZ) {
        window.removeEventListener(event, method, false);
    } else if (_SARISSA_IS_IE) {
        element.detachEvent("on" + event, method);
    } else {
        throw _("Unsupported browser!");
    };
};

/* Replacement for window.document.getElementById()
 * selector can be an Id (so we maintain backwards compatability)
 * but is intended to be a subset of valid CSS selectors.
 * For now we only support the format: "#id tag.class"
 */
function getFromSelector(selector) {
    var match = /#(\S+)\s*([^ .]+)\.(\S+)/.exec(selector);
    if (!match) {
        return window.document.getElementById(selector);
    }
    var id=match[1], tag=match[2], className=match[3];
    var base = window.document.getElementById(id);
    return getBaseTagClass(base, tag, className);
}

function getBaseTagClass(base, tag, className) {
    var classPat = new RegExp('\\b'+className+'\\b');

    var nodes = base.getElementsByTagName(tag);
    for (var i = 0; i < nodes.length; i++) {
        if (classPat.test(nodes[i].className)) {
            return nodes[i];
        }
    }
    return null;
}

function openPopup(url, width, height) {
    /* open and center a popup window */
    var sw = screen.width;
    var sh = screen.height;
    var left = sw / 2 - width / 2;
    var top = sh / 2 - height / 2;
    var win = window.open(url, 'someWindow', 
                'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top);
    return win;
};

function selectSelectItem(select, item) {
    /* select a certain item from a select */
    for (var i=0; i < select.options.length; i++) {
        var option = select.options[i];
        if (option.value == item) {
            select.selectedIndex = i;
            return;
        }
    }
    select.selectedIndex = 0;
};

function ParentWithStyleChecker(tagnames, style, stylevalue, command) {
    /* small wrapper that provides a generic function to check if a
       button should look pressed in */
    return function(selNode, button, editor, event) {
        /* check if the button needs to look pressed in */
        if (command) {
            var result = editor.getInnerDocument().queryCommandState(command)
            if (result || editor.getSelection().getContentLength() == 0) {
                return result;
            };
        };
        var currnode = selNode;
        while (currnode && currnode.style) {
            for (var i=0; i < tagnames.length; i++) {
                if (currnode.nodeName.toLowerCase() == tagnames[i].toLowerCase()) {
                    return true;
                };
            };
            if (style && currnode.style[style] == stylevalue) {
                return true;
            };
            currnode = currnode.parentNode;
        };
        return false;
    };
};

function _load_dict_helper(element) {
    /* walks through a set of XML nodes and builds a nested tree of objects */
    var dict = {};
    for (var i=0; i < element.childNodes.length; i++) {
        var child = element.childNodes[i];
        if (child.nodeType == 1) {
            var value = '';
            for (var j=0; j < child.childNodes.length; j++) {
                // test if we can recurse, if so ditch the string (probably
                // ignorable whitespace) and dive into the node
                if (child.childNodes[j].nodeType == 1) {
                    value = _load_dict_helper(child);
                    break;
                } else if (typeof(value) == typeof('')) {
                    value += child.childNodes[j].nodeValue;
                };
            };
            if (typeof(value) == typeof('') && !isNaN(parseInt(value)) && 
                    parseInt(value).toString().length == value.length) {
                value = parseInt(value);
            } else if (typeof(value) != typeof('')) {
                if (value.length == 1) {
                    value = value[0];
                };
            };
            var name = child.nodeName.toLowerCase();
            if (dict[name] != undefined) {
                if (!dict[name].push) {
                    dict[name] = new Array(dict[name], value);
                } else {
                    dict[name].push(value);
                };
            } else {
                dict[name] = value;
            };
        };
    };
    return dict;
};

function loadDictFromXML(document, islandid) {
    /* load configuration values from an XML chunk

        this is quite generic, it just reads data from a chunk of XML into
        an object, checking if the object is complete should be done in the
        calling context.
    */
    var dict = {};
    var confnode = getFromSelector(islandid);
    var root = null;
    for (var i=0; i < confnode.childNodes.length; i++) {
        if (confnode.childNodes[i].nodeType == 1) {
            root = confnode.childNodes[i];
            break;
        };
    };
    if (!root) {
        throw(_('No element found in the config island!'));
    };
    dict = _load_dict_helper(root);
    return dict;
};

function NodeIterator(node, continueatnextsibling) {
    /* simple node iterator

        can be used to recursively walk through all children of a node,
        the next() method will return the next node until either the next
        sibling of the startnode is reached (when continueatnextsibling is 
        false, the default) or when there's no node left (when 
        continueatnextsibling is true)

        returns false if no nodes are left
    */
    this.node = node;
    this.current = node;
    this.terminator = continueatnextsibling ? null : node;
    
    this.next = function() {
        /* return the next node */
        if (this.current === false) {
            // restart
            this.current = this.node;
        };
        var current = this.current;
        if (current.firstChild) {
            this.current = current.firstChild;
        } else {
            // walk up parents until we finish or find one with a nextSibling
            while (current != this.terminator && !current.nextSibling) {
                current = current.parentNode;
            };
            if (current == this.terminator) {
                this.current = false;
            } else {
                this.current = current.nextSibling;
            };
        };
        return this.current;
    };

    this.reset = function() {
        /* reset the iterator so it starts at the first node */
        this.current = this.node;
    };

    this.setCurrent = function(node) {
        /* change the current node
            
            can be really useful for specific hacks, the user must take
            care that the node is inside the iterator's scope or it will
            go wild
        */
        this.current = node;
    };
};

/* selection classes, these are wrappers around the browser-specific
    selection objects to provide a generic abstraction layer
*/
function BaseSelection() {
    /* superclass for the Selection objects
    
        this will contain higher level methods that don't contain 
        browser-specific code
    */
    this.splitNodeAtSelection = function(node) {
        /* split the node at the current selection

            remove any selected text, then split the node on the location
            of the selection, thus creating a new node, this is attached to
            the node's parent after the node

            this will fail if the selection is not inside the node
        */
        if (!this.selectionInsideNode(node)) {
            throw(_('Selection not inside the node!'));
        };
        // a bit sneaky: what we'll do is insert a new br node to replace
        // the current selection, then we'll walk up to that node in both
        // the original and the cloned node, in the original we'll remove
        // the br node and everything that's behind it, on the cloned one
        // we'll remove the br and everything before it
        // anyway, we'll end up with 2 nodes, the first already in the 
        // document (the original node) and the second we can just attach
        // to the doc after the first one
        var doc = this.document.getDocument();
        var br = doc.createElement('br');
        br.setAttribute('node_splitter', 'indeed');
        this.replaceWithNode(br);
        
        var clone = node.cloneNode(true);

        // now walk through the original node
        var iterator = new NodeIterator(node);
        var currnode = iterator.next();
        var remove = false;
        while (currnode) {
            if (currnode.nodeName.toLowerCase() == 'br' && currnode.getAttribute('node_splitter') == 'indeed') {
                // here's the point where we should start removing
                remove = true;
            };
            // we should fetch the next node before we remove the current one, else the iterator
            // will fail (since the current node is removed)
            var lastnode = currnode;
            currnode = iterator.next();
            // XXX this will leave nodes that *became* empty in place, since it doesn't visit it again,
            // perhaps we should do a second pass that removes the rest(?)
            if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) {
                lastnode.parentNode.removeChild(lastnode);
            };
        };

        // and through the clone
        var iterator = new NodeIterator(clone);
        var currnode = iterator.next();
        var remove = true;
        while (currnode) {
            var lastnode = currnode;
            currnode = iterator.next();
            if (lastnode.nodeName.toLowerCase() == 'br' && lastnode.getAttribute('node_splitter') == 'indeed') {
                // here's the point where we should stop removing
                lastnode.parentNode.removeChild(lastnode);
                remove = false;
            };
            if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) {
                lastnode.parentNode.removeChild(lastnode);
            };
        };

        // next we need to attach the node to the document
        if (node.nextSibling) {
            node.parentNode.insertBefore(clone, node.nextSibling);
        } else {
            node.parentNode.appendChild(clone);
        };

        // this will change the selection, so reselect
        this.reset();

        // return a reference to the clone
        return clone;
    };

    this.selectionInsideNode = function(node) {
        /* returns a Boolean to indicate if the selection is resided
            inside the node
        */
        var currnode = this.parentElement();
        while (currnode) {
            if (currnode == node) {
                return true;
            };
            currnode = currnode.parentNode;
        };
        return false;
    };
};

function MozillaSelection(document) {
    this.document = document;
    this.selection = document.getWindow().getSelection();
    
    this.selectNodeContents = function(node) {
        /* select the contents of a node */
        this.selection.removeAllRanges();
        this.selection.selectAllChildren(node);
    };

    this.collapse = function(collapseToEnd) {
        try {
            if (!collapseToEnd) {
                this.selection.collapseToStart();
            } else {
                this.selection.collapseToEnd();
            };
        } catch(e) {};
    };

    this.replaceWithNode = function(node, selectAfterPlace) {
        // XXX this should be on a range object
        /* replaces the current selection with a new node
            returns a reference to the inserted node 

            newnode is the node to replace the content with, selectAfterPlace
            can either be a DOM node that should be selected after the new
            node was placed, or some value that resolves to true to select
            the placed node
        */
        // get the first range of the selection
        // (there's almost always only one range)
        var range = this.selection.getRangeAt(0);

        // deselect everything
        this.selection.removeAllRanges();

        // remove content of current selection from document
        range.deleteContents();

        // get location of current selection
        var container = range.startContainer;
        var pos = range.startOffset;

        // make a new range for the new selection
        var range = this.document.getDocument().createRange();

        if (container.nodeType == 3 && node.nodeType == 3) {
            // if we insert text in a textnode, do optimized insertion
            container.insertData(pos, node.nodeValue);

            // put cursor after inserted text
            range.setEnd(container, pos + node.length);
            range.setStart(container, pos + node.length);
        } else {
            var afterNode;
            if (container.nodeType == 3) {
                // when inserting into a textnode
                // we create 2 new textnodes
                // and put the node in between

                var textNode = container;
                var container = textNode.parentNode;
                var text = textNode.nodeValue;

                // text before the split
                var textBefore = text.substr(0,pos);
                // text after the split
                var textAfter = text.substr(pos);

                var beforeNode = this.document.getDocument().createTextNode(textBefore);
                afterNode = this.document.getDocument().createTextNode(textAfter);

                // insert the 3 new nodes before the old one
                container.insertBefore(afterNode, textNode);
                container.insertBefore(node, afterNode);
                container.insertBefore(beforeNode, node);

                // remove the old node
                container.removeChild(textNode);
            } else {
                // else simply insert the node
                afterNode = container.childNodes[pos];
                if (afterNode) {
                    container.insertBefore(node, afterNode);
                } else {
                    container.appendChild(node);
                };
            }

            range.setEnd(afterNode, 0);
            range.setStart(afterNode, 0);
        }

        if (selectAfterPlace) {
            // a bit implicit here, but I needed this to be backward 
            // compatible and also I didn't want yet another argument,
            // JavaScript isn't as nice as Python in that respect (kwargs)
            // if selectAfterPlace is a DOM node, select all of that node's
            // contents, else select the newly added node's
            this.selection = this.document.getWindow().getSelection();
            this.selection.addRange(range);
            if (selectAfterPlace.nodeType == 1) {
                this.selection.selectAllChildren(selectAfterPlace);
            } else {
                if (node.hasChildNodes()) {
                    this.selection.selectAllChildren(node);
                } else {
                    var range = this.selection.getRangeAt(0).cloneRange();
                    this.selection.removeAllRanges();
                    range.selectNode(node);
                    this.selection.addRange(range);
                };
            };
            this.document.getWindow().focus();
        };
        return node;
    };

    this.startOffset = function() {
        // XXX this should be on a range object
        var startnode = this.startNode();
        var startnodeoffset = 0;
        if (startnode == this.selection.anchorNode) {
            startnodeoffset = this.selection.anchorOffset;
        } else {
            startnodeoffset = this.selection.focusOffset;
        };
        var parentnode = this.parentElement();
        if (startnode == parentnode) {
            return startnodeoffset;
        };
        var currnode = parentnode.firstChild;
        var offset = 0;
        if (!currnode) {
            // 'Control range', range consists of a single element, so startOffset is 0
            if (startnodeoffset != 0) {
                // just an assertion to see if my assumption about this case is right
                throw(_('Start node offset detected in a node without children!'));
            };
            return 0;
        };
        while (currnode != startnode) {
            if (currnode.nodeType == 3) {
                offset += currnode.nodeValue.length;
            };
            currnode = currnode.nextSibling;
        };
        return offset + startnodeoffset;
    };

    this.startNode = function() {
        // XXX this should be on a range object
        var anode = this.selection.anchorNode;
        var aoffset = this.selection.anchorOffset;
        var onode = this.selection.focusNode;
        var ooffset = this.selection.focusOffset;
        var arange = this.document.getDocument().createRange();
        arange.setStart(anode, aoffset);
        var orange = this.document.getDocument().createRange();
        orange.setStart(onode, ooffset);
        return arange.compareBoundaryPoints('START_TO_START', orange) <= 0 ? anode : onode;
    };

    this.endOffset = function() {
        // XXX this should be on a range object
        var endnode = this.endNode();
        var endnodeoffset = 0;
        if (endnode = this.selection.focusNode) {
            endnodeoffset = this.selection.focusOffset;
        } else {
            endnodeoffset = this.selection.anchorOffset;
        };
        var parentnode = this.parentElement();
        var currnode = parentnode.firstChild;
        var offset = 0;
        if (parentnode == endnode) {
            for (var i=0; i < parentnode.childNodes.length; i++) {
                var child = parentnode.childNodes[i];
                if (i == endnodeoffset) {
                    return offset;
                };
                if (child.nodeType == 3) {
                    offset += child.nodeValue.length;
                };
            };
        };
        if (!currnode) {
            // node doesn't have any content, so offset is always 0
            if (endnodeoffset != 0) {
                // just an assertion to see if my assumption about this case is right
                var msg = _('End node offset detected in a node without ' +
                            'children!');
                alert(msg);
                throw(msg);
            };
            return 0;
        };
        while (currnode != endnode) {
            if (currnode.nodeType == 3) { // should account for CDATA nodes as well
                offset += currnode.nodeValue.length;
            };
            currnode = currnode.nextSibling;
        };
        return offset + endnodeoffset;
    };

    this.endNode = function() {
        // XXX this should be on a range object
        var anode = this.selection.anchorNode;
        var aoffset = this.selection.anchorOffset;
        var onode = this.selection.focusNode;
        var ooffset = this.selection.focusOffset;
        var arange = this.document.getDocument().createRange();
        arange.setStart(anode, aoffset);
        var orange = this.document.getDocument().createRange();
        orange.setStart(onode, ooffset);
        return arange.compareBoundaryPoints('START_TO_START', orange) > 0 ? anode : onode;
    };

    this.getContentLength = function() {
        // XXX this should be on a range object
        return this.selection.toString().length;
    };

    this.cutChunk = function(startOffset, endOffset) {
        // XXX this should be on a range object
        var range = this.selection.getRangeAt(0);
        
        // set start point
        var offsetParent = this.parentElement();
        var currnode = offsetParent.firstChild;
        var curroffset = 0;

        var startparent = null;
        var startparentoffset = 0;
        
        while (currnode) {
            if (currnode.nodeType == 3) { // XXX need to add CDATA support
                var nodelength = currnode.nodeValue.length;
                if (curroffset + nodelength < startOffset) {
                    curroffset += nodelength;
                } else {
                    startparent = currnode;
                    startparentoffset = startOffset - curroffset;
                    break;
                };
            };
            currnode = currnode.nextSibling;
        };
        // set end point
        var currnode = offsetParent.firstChild;
        var curroffset = 0;

        var endparent = null;
        var endoffset = 0;
        
        while (currnode) {
            if (currnode.nodeType == 3) { // XXX need to add CDATA support
                var nodelength = currnode.nodeValue.length;
                if (curroffset + nodelength < endOffset) {
                    curroffset += nodelength;
                } else {
                    endparent = currnode;
                    endparentoffset = endOffset - curroffset;
                    break;
                };
            };
            currnode = currnode.nextSibling;
        };
        
        // now cut the chunk
        if (!startparent) {
            throw(_('Start offset out of range!'));
        };
        if (!endparent) {
            throw(_('End offset out of range!'));
        };

        var newrange = range.cloneRange();
        newrange.setStart(startparent, startparentoffset);
        newrange.setEnd(endparent, endparentoffset);
        return newrange.extractContents();
    };

    this.getElementLength = function(element) {
        // XXX this should be a helper function
        var length = 0;
        var currnode = element.firstChild;
        while (currnode) {
            if (currnode.nodeType == 3) { // XXX should support CDATA as well
                length += currnode.nodeValue.length;
            };
            currnode = currnode.nextSibling;
        };
        return length;
    };

    this.parentElement = function() {
        /* return the selected node (or the node containing the selection) */
        // XXX this should be on a range object
        if (this.selection.rangeCount == 0) {
            var parent = this.document.getDocument().body;
            while (parent.firstChild) {
                parent = parent.firstChild;
            };
        } else {
            var range = this.selection.getRangeAt(0);
            var parent = range.commonAncestorContainer;

            // the following deals with cases where only a single child is
            // selected, e.g. after a click on an image
            var inv = range.compareBoundaryPoints(Range.START_TO_END, range) < 0;
            var startNode = inv ? range.endContainer : range.startContainer;
            var startOffset = inv ? range.endOffset : range.startOffset;
            var endNode = inv ? range.startContainer : range.endContainer;
            var endOffset = inv ? range.startOffset : range.endOffset;

            var selectedChild = null;
            var child = parent.firstChild;
            while (child) {
                // XXX the additional conditions catch some invisible
                // intersections, but still not all of them
                if (range.intersectsNode(child) &&
                    !(child == startNode && startOffset == child.length) &&
                    !(child == endNode && endOffset == 0)) {
                    if (selectedChild) {
                        // current child is the second selected child found
                        selectedChild = null;
                        break;
                    } else {
                        // current child is the first selected child found
                        selectedChild = child;
                    };
                } else if (selectedChild) {
                    // current child is after the selection
                    break;
                };
                child = child.nextSibling;
            };
            if (selectedChild) {
                parent = selectedChild;
            };
        };
        if (parent.nodeType == Node.TEXT_NODE) {
            parent = parent.parentNode;
        };
        return parent;
    };

    // deprecated alias of parentElement
    this.getSelectedNode = this.parentElement;

    this.moveStart = function(offset) {
        // XXX this should be on a range object
        var offsetparent = this.parentElement();
        // the offset within the offsetparent
        var startoffset = this.startOffset();
        var realoffset = offset + startoffset;
        if (realoffset >= 0) {
            var currnode = offsetparent.firstChild;
            var curroffset = 0;
            var startparent = null;
            var startoffset = 0;
            while (currnode) {
                if (currnode.nodeType == 3) { // XXX need to support CDATA sections
                    var nodelength = currnode.nodeValue.length;
                    if (curroffset + nodelength >= realoffset) {
                        var range = this.selection.getRangeAt(0);
                        //range.setEnd(this.endNode(), this.endOffset());
                        range.setStart(currnode, realoffset - curroffset);
                        return;
                        //this.selection.removeAllRanges();
                        //this.selection.addRange(range);
                    };
                };
                currnode = currnode.nextSibling;
            };
            // if we still haven't found the startparent we should walk to 
            // all nodes following offsetparent as well
            var currnode = offsetparent.nextSibling;
            while (currnode) {
                if (currnode.nodeType == 3) {
                    var nodelength = currnode.nodeValue.length;
                    if (curroffset + nodelength >= realoffset) {
                        var range = this.selection.getRangeAt(0);
                        // XXX does IE switch the begin and end nodes here as well?
                        var endnode = this.endNode();
                        var endoffset = this.endOffset();
                        range.setEnd(currnode, realoffset - curroffset);
                        range.setStart(endnode, endoffset);
                        return;
                    };
                    curroffset += nodelength;
                };
                currnode = currnode.nextSibling;
            };
            throw(_('Offset out of document range'));
        } else if (realoffset < 0) {
            var currnode = offsetparent.prevSibling;
            var curroffset = 0;
            while (currnode) {
                if (currnode.nodeType == 3) { // XXX need to support CDATA sections
                    var currlength = currnode.nodeValue.length;
                    if (curroffset - currlength < realoffset) {
                        var range = this.selection.getRangeAt(0);
                        range.setStart(currnode, realoffset - curroffset);
                    };
                    curroffset -= currlength;
                };
                currnode = currnode.prevSibling;
            };
        } else {
            var range = this.selection.getRangeAt(0);
            range.setStart(offsetparent, 0);
            //this.selection.removeAllRanges();
            //this.selection.addRange(range);
        };
    };

    this.moveEnd = function(offset) {
        // XXX this should be on a range object
    };

    this.reset = function() {
        this.selection = this.document.getWindow().getSelection();
    };

    this.cloneContents = function() {
        /* returns a document fragment with a copy of the contents */
        var range = this.selection.getRangeAt(0);
        return range.cloneContents();
    };

    this.containsNode = function(node) {
        return this.selection.containsNode(node, true);
    }

    this.toString = function() {
        return this.selection.toString();
    };

    this.getRange = function() {
        return this.selection.getRangeAt(0);
    }
    this.restoreRange = function(range) {
        var selection = this.selection;
        selection.removeAllRanges();
        selection.addRange(range);
    }
};

MozillaSelection.prototype = new BaseSelection;

function IESelection(document) {
    this.document = document;
    this.selection = document.getDocument().selection;

    /* If no selection in editable document, IE returns selection from
     * main page, so force an inner selection. */
    var doc = document.getDocument();

    var range = this.selection.createRange()
    var parent = this.selection.type=="Text" ?
        range.parentElement() :
        this.selection.type=="Control" ?  range.parentElement : null;

    if(parent && parent.ownerDocument != doc) {
            var range = doc.body.createTextRange();
            range.collapse();
            range.select();
    }

    this.selectNodeContents = function(node) {
        /* select the contents of a node */
        // a bit nasty, when moveToElementText is called it will move the selection start
        // to just before the element instead of inside it, and since IE doesn't reserve
        // an index for the element itself as well the way to get it inside the element is
        // by moving the start one pos and then moving it back (yuck!)
        var range = this.selection.createRange().duplicate();
        range.moveToElementText(node);
        range.moveStart('character', 1);
        range.moveStart('character', -1);
        range.moveEnd('character', -1);
        range.moveEnd('character', 1);
        range.select();
        this.selection = this.document.getDocument().selection;
    };

    this.collapse = function(collapseToEnd) {
        var range = this.selection.createRange();
        range.collapse(!collapseToEnd);
        range.select();
        this.selection = document.getDocument().selection;
    };

    this.replaceWithNode = function(newnode, selectAfterPlace) {
        /* replaces the current selection with a new node
            returns a reference to the inserted node 

            newnode is the node to replace the content with, selectAfterPlace
            can either be a DOM node that should be selected after the new
            node was placed, or some value that resolves to true to select
            the placed node
        */
        if (this.selection.type == 'Control') {
            var range = this.selection.createRange();
            range.item(0).parentNode.replaceChild(newnode, range.item(0));
            for (var i=1; i < range.length; i++) {
                range.item(i).parentNode.removeChild(range[i]);
            };
            if (selectAfterPlace) {
                var range = this.document.getDocument().body.createTextRange();
                range.moveToElementText(newnode);
                range.select();
            };
        } else {
            var document = this.document.getDocument();
            var range = this.selection.createRange();

            range.pasteHTML('<img id="kupu-tempnode">');
            tempnode = document.getElementById('kupu-tempnode');
            tempnode.replaceNode(newnode);

            if (selectAfterPlace) {
                // see MozillaSelection.replaceWithNode() for some comments about
                // selectAfterPlace
                if (selectAfterPlace.nodeType == Node.ELEMENT_NODE) {
                    range.moveToElementText(selectAfterPlace);
                } else {
                    range.moveToElementText(newnode);
                };
                range.select();
            };
        };
        this.reset();
        return newnode;
    };

    this.startOffset = function() {
        var startoffset = 0;
        var selrange = this.selection.createRange();
        var parent = selrange.parentElement();
        var elrange = selrange.duplicate();
        elrange.moveToElementText(parent);
        var tempstart = selrange.duplicate();
        while (elrange.compareEndPoints('StartToStart', tempstart) < 0) {
            startoffset++;
            tempstart.moveStart('character', -1);
        };

        return startoffset;
    };

    this.endOffset = function() {
        var endoffset = 0;
        var selrange = this.selection.createRange();
        var parent = selrange.parentElement();
        var elrange = selrange.duplicate();
        elrange.moveToElementText(parent);
        var tempend = selrange.duplicate();
        while (elrange.compareEndPoints('EndToEnd', tempend) > 0) {
            endoffset++;
            tempend.moveEnd('character', 1);
        };

        return endoffset;
    };

    this.getContentLength = function() {
        if (this.selection.type == 'Control') {
            return this.selection.createRange().length;
        };
        var contentlength = 0;
        var range = this.selection.createRange();
        var endrange = range.duplicate();
        while (range.compareEndPoints('StartToEnd', endrange) < 0) {
            range.move('character', 1);
            contentlength++;
        };
        return contentlength;
    };

    this.cutChunk = function(startOffset, endOffset) {
        /* cut a chunk of HTML from the selection

            this *should* return the chunk of HTML but doesn't yet
        */
        var range = this.selection.createRange().duplicate();
        range.moveStart('character', startOffset);
        range.moveEnd('character', -endOffset);
        range.pasteHTML('');
        // XXX here it should return the chunk
    };

    this.getElementLength = function(element) {
        /* returns the length of an element *including* 1 char for each child element

            this is defined on the selection since it returns results that can be used
            to work with selection offsets
        */
        var length = 0;
        var range = this.selection.createRange().duplicate();
        range.moveToElementText(element);
        range.moveStart('character', 1);
        range.moveEnd('character', -1);
        var endpoint = range.duplicate();
        endpoint.collapse(false);
        range.collapse();
        while (!range.isEqual(endpoint)) {
            range.moveEnd('character', 1);
            range.moveStart('character', 1);
            length++;
        };
        return length;
    };

    this.parentElement = function() {
        /* return the selected node (or the node containing the selection) */
        // XXX this should be on a range object
        if (this.selection.type == 'Control') {
            return this.selection.createRange().item(0);
        } else {
            return this.selection.createRange().parentElement();
        };
    };

    // deprecated alias of parentElement
    this.getSelectedNode = this.parentElement;

    this.moveStart = function(offset) {
        /* move the start of the selection */
        var range = this.selection.createRange();
        range.moveStart('character', offset);
        range.select();
    };

    this.moveEnd = function(offset) {
        /* moves the end of the selection */
        var range = this.selection.createRange();
        range.moveEnd('character', offset);
        range.select();
    };

    this.reset = function() {
       this.selection = this.document.getDocument().selection;
    };

    this.cloneContents = function() {
        /* returns a document fragment with a copy of the contents */
        var contents = this.selection.createRange().htmlText;
        var doc = this.document.getDocument();
        var docfrag = doc.createElement('span');
        docfrag.innerHTML = contents;
        return docfrag;
    };

    this.containsNode = function(node) {
        var selected = this.selection.createRange();
        
        if (this.selection.type.toLowerCase()=='text') {
            var range = doc.body.createTextRange();
            range.moveToElementText(node);

            if (selected.compareEndPoints('StartToEnd', range) >= 0 ||
                selected.compareEndPoints('EndToStart', range) <= 0) {
                return false;
            }
            return true;
        } else {
            for (var i = 0; i < selected.length; i++) {
                if (selected.item(i).contains(node)) {
                    return true;
                }
            }
            return false;
        }
    };
    
    this.getRange = function() {
        return this.selection.createRange();
    }

    this.restoreRange = function(range) {
        try {
            range.select();
        } catch(e) {
        };
    }

    this.toString = function() {
        return this.selection.createRange().text;
    };
};

IESelection.prototype = new BaseSelection;

/* ContextFixer, fixes a problem with the prototype based model

    When a method is called in certain particular ways, for instance
    when it is used as an event handler, the context for the method
    is changed, so 'this' inside the method doesn't refer to the object
    on which the method is defined (or to which it is attached), but for
    instance to the element on which the method was bound to as an event
    handler. This class can be used to wrap such a method, the wrapper 
    has one method that can be used as the event handler instead. The
    constructor expects at least 2 arguments, first is a reference to the
    method, second the context (a reference to the object) and optionally
    it can cope with extra arguments, they will be passed to the method
    as arguments when it is called (which is a nice bonus of using 
    this wrapper).
*/

function ContextFixer(func, context) {
    /* Make sure 'this' inside a method points to its class */
    this.func = func;
    this.context = context;
    this.args = arguments;
    var self = this;
    
    this.execute = function() {
        /* execute the method */
        var args = new Array();
        // the first arguments will be the extra ones of the class
        for (var i=0; i < self.args.length - 2; i++) {
            args.push(self.args[i + 2]);
        };
        // the last are the ones passed on to the execute method
        for (var i=0; i < arguments.length; i++) {
            args.push(arguments[i]);
        };
        return self.func.apply(self.context, args);
    };

};

/* Alternative implementation of window.setTimeout

    This is a singleton class, the name of the single instance of the
    object is 'timer_instance', which has one public method called
    registerFunction. This method takes at least 2 arguments: a
    reference to the function (or method) to be called and the timeout.
    Arguments to the function are optional arguments to the 
    registerFunction method. Example:

    timer_instance.registerMethod(foo, 100, 'bar', 'baz');

    will call the function 'foo' with the arguments 'bar' and 'baz' with
    a timeout of 100 milliseconds.

    Since the method doesn't expect a string but a reference to a function
    and since it can handle arguments that are resolved within the current
    namespace rather then in the global namespace, the method can be used
    to call methods on objects from within the object (so this.foo calls
    this.foo instead of failing to find this inside the global namespace)
    and since the arguments aren't strings which are resolved in the global
    namespace the arguments work as expected even inside objects.

*/

function Timer() {
    /* class that has a method to replace window.setTimeout */
    this.lastid = 0;
    this.functions = {};
    
    this.registerFunction = function(object, func, timeout) {
        /* register a function to be called with a timeout

            args: 
                func - the function
                timeout - timeout in millisecs
                
            all other args will be passed 1:1 to the function when called
        */
        var args = new Array();
        for (var i=0; i < arguments.length - 3; i++) {
            args.push(arguments[i + 3]);
        }
        var id = this._createUniqueId();
        this.functions[id] = new Array(object, func, args);
        setTimeout("timer_instance._handleFunction(" + id + ")", timeout);
    };

    this._handleFunction = function(id) {
        /* private method that does the actual function call */
        var obj = this.functions[id][0];
        var func = this.functions[id][1];
        var args = this.functions[id][2];
        this.functions[id] = null;
        func.apply(obj, args);
    };

    this._createUniqueId = function() {
        /* create a unique id to store the function by */
        while (this.lastid in this.functions && this.functions[this.lastid]) {
            this.lastid++;
            if (this.lastid > 100000) {
                this.lastid = 0;
            }
        }
        return this.lastid;
    };
};

// create a timer instance in the global namespace, obviously this does some
// polluting but I guess it's impossible to avoid...

// OBVIOUSLY THIS VARIABLE SHOULD NEVER BE OVERWRITTEN!!!
timer_instance = new Timer();

// helper function on the Array object to test for containment
Array.prototype.contains = function(element, objectequality) {
    /* see if some value is in this */
    for (var i=0; i < this.length; i++) {
        if (objectequality) {
            if (element === this[i]) {
                return true;
            };
        } else {
            if (element == this[i]) {
                return true;
            };
        };
    };
    return false;
};

// return a copy of an array with doubles removed
Array.prototype.removeDoubles = function() {
    var ret = [];
    for (var i=0; i < this.length; i++) {
        if (!ret.contains(this[i])) {
            ret.push(this[i]);
        };
    };
    return ret;
};

Array.prototype.map = function(func) {
    /* apply 'func' to each element in the array */
    for (var i=0; i < this.length; i++) {
        this[i] = func(this[i]);
    };
};

Array.prototype.reversed = function() {
    var ret = [];
    for (var i = this.length; i > 0; i--) {
        ret.push(this[i - 1]);
    };
    return ret;
};

// JavaScript has a friggin' blink() function, but not for string stripping...
String.prototype.strip = function() {
    var stripspace = /^\s*([\s\S]*?)\s*$/;
    return stripspace.exec(this)[1];
};

String.prototype.reduceWhitespace = function() {
    /* returns a string in which all whitespace is reduced 
        to a single, plain space */
    var spacereg = /(\s+)/g;
    var copy = this;
    while (true) {
        var match = spacereg.exec(copy);
        if (!match) {
            return copy;
        };
        copy = copy.replace(match[0], ' ');
    };
};

String.prototype.entitize = function() {
    var ret = this.replace(/&/g, '&amp;');
    ret = ret.replace(/"/g, '&quot;');
    ret = ret.replace(/'/g, '&apos;');
    ret = ret.replace(/</g, '&lt;');
    ret = ret.replace(/>/g, '&gt;');
    return ret;
};

String.prototype.deentitize = function() {
    var ret = this.replace(/&gt;/g, '>');
    ret = ret.replace(/&lt;/g, '<');
    ret = ret.replace(/&quot;/g, '"');
    ret = ret.replace(/&apos;/g, "'");
    ret = ret.replace(/&amp;/g, '&');
    return ret;
};

String.prototype.urldecode = function() {
    var reg = /%([a-fA-F0-9]{2})/g;
    var str = this;
    while (true) {
        var match = reg.exec(str);
        if (!match || !match.length) {
            break;
        };
        var repl = new RegExp(match[0], 'g');
        str = str.replace(repl, String.fromCharCode(parseInt(match[1], 16)));
    };
    return str;
};

String.prototype.centerTruncate = function(maxlength) {
    if (this.length <= maxlength) {
        return this;
    };
    var chunklength = maxlength / 2 - 3;
    var start = this.substr(0, chunklength);
    var end = this.substr(this.length - chunklength);
    return start + ' ... ' + end;
};

//----------------------------------------------------------------------------
// Exceptions
//----------------------------------------------------------------------------

function debug(str, win) {
    if (!win) {
        win = window;
    };
    var doc = win.document;
    var div = doc.createElement('div');
    div.appendChild(doc.createTextNode(str));
    doc.getElementsByTagName('body')[0].appendChild(div);
};

// XXX don't know if this is the regular way to define exceptions in JavaScript?
function Exception() {
    return;
};

// throw this as an exception inside an updateState handler to restart the
// update, may be required in situations where updateState changes the structure
// of the document (e.g. does a cleanup or so)
UpdateStateCancelBubble = new Exception();

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupueditor.js 18104 2005-10-03 14:10:11Z duncan $

//----------------------------------------------------------------------------
// Main classes
//----------------------------------------------------------------------------

/* KupuDocument
    
    This essentially wraps the iframe.
    XXX Is this overkill?
    
*/

function KupuDocument(iframe) {
    /* Model */
    
    // attrs
    this.editable = iframe; // the iframe
    this.window = this.editable.contentWindow;
    this.document = this.window.document;

    this._browser = _SARISSA_IS_IE ? 'IE' : 'Mozilla';
    
    // methods
    this.execCommand = function(command, arg) {
        /* delegate execCommand */
        if (arg === undefined) arg = null;
        this.document.execCommand(command, false, arg);
    };
    
    this.reloadSource = function() {
        /* reload the source */
        
        // XXX To temporarily work around problems with resetting the
        // state after a reload, currently the whole page is reloaded.
        // XXX Nasty workaround!! to solve refresh problems...
        document.location = document.location;
    };

    this.getDocument = function() {
        /* returns a reference to the window.document object of the iframe */
        return this.document;
    };

    this.getWindow = function() {
        /* returns a reference to the window object of the iframe */
        return this.window;
    };

    this.getSelection = function() {
        if (this._browser == 'Mozilla') {
            return new MozillaSelection(this);
        } else {
            return new IESelection(this);
        };
    };

    this.getEditable = function() {
        return this.editable;
    };
};

/* KupuEditor

    This controls the document, should be used from the UI.
    
*/

function KupuEditor(document, config, logger) {
    /* Controller */
    
    // attrs
    this.document = document; // the model
    this.config = config; // an object that holds the config values
    this.log = logger; // simple logger object
    this.tools = {}; // mapping id->tool
    this.filters = new Array(); // contentfilters
    
    this._designModeSetAttempts = 0;
    this._initialized = false;

    // some properties to save the selection, required for IE to remember 
    // where in the iframe the selection was
    this._previous_range = null;

    // this property is true if the content is changed, false if no changes 
    // are made yet
    this.content_changed = false;

    // methods
    this.initialize = function() {
        /* Should be called on iframe.onload, will initialize the editor */
        //DOM2Event.initRegistration();
        this._initializeEventHandlers();
        if (this.getBrowserName() == "IE") {
            var body = this.getInnerDocument().getElementsByTagName('body')[0];
            body.setAttribute('contentEditable', 'true');
            // provide an 'afterInit' method on KupuEditor.prototype
            // for additional bootstrapping (after editor init)
            this._initialized = true;
            if (this.afterInit) {
                this.afterInit();
            };
            this._saveSelection();
        } else {
            this._setDesignModeWhenReady();
        };
        this.logMessage(_('Editor initialized'));
    };

    this.setContextMenu = function(menu) {
        /* initialize the contextmenu */
        menu.initialize(this);
    };

    this.registerTool = function(id, tool) {
        /* register a tool */
        this.tools[id] = tool;
        tool.initialize(this);
    };

    this.getTool = function(id) {
        /* get a tool by id */
        return this.tools[id];
    };

    this.registerFilter = function(filter) {
        /* register a content filter method

            the method will be called together with any other registered
            filters before the content is saved to the server, the methods
            can be used to filter any trash out of the content. they are
            called with 1 argument, which is a reference to the rootnode
            of the content tree (the html node)
        */
        this.filters.push(filter);
        filter.initialize(this);
    };

    this.updateStateHandler = function(event) {
        /* check whether the event is interesting enough to trigger the 
        updateState machinery and act accordingly */
        var interesting_codes = new Array(8, 13, 37, 38, 39, 40, 46);
        // unfortunately it's not possible to do this on blur, since that's
        // too late. also (some versions of?) IE 5.5 doesn't support the
        // onbeforedeactivate event, which would be ideal here...
        this._saveSelection();

        if (event.type == 'click' || event.type=='mouseup' ||
                (event.type == 'keyup' && 
                    interesting_codes.contains(event.keyCode))) {
            // Filthy trick to make the updateState method get called *after*
            // the event has been resolved. This way the updateState methods can
            // react to the situation *after* any actions have been performed (so
            // can actually stay up to date).
            this.updateState(event);
        }
    };
    
    this.updateState = function(event) {
        /* let each tool change state if required */
        // first see if the event is interesting enough to trigger
        // the whole updateState machinery
        var selNode = this.getSelectedNode();
        for (var id in this.tools) {
            try {
                this.tools[id].updateState(selNode, event);
            } catch (e) {
                if (e == UpdateStateCancelBubble) {
                    this.updateState(event);
                    break;
                } else {
                    this.logMessage(
                        _('Exception while processing updateState on ' +
                            '${id}: ${msg}', {'id': id, 'msg': e}), 2);
                };
            };
        };
    };
    
    this.saveDocument = function(redirect, synchronous) {
        /* save the document

            the (optional) redirect argument can be used to make the client 
            jump to another URL when the save action was successful.

            synchronous is a boolean to allow sync saving (usually better to
            not save synchronous, since it may make browsers freeze on errors,
            this is used for saveOnPart, though)
        */
        
        // if no dst is available, bail out
        if (!this.config.dst) {
            this.logMessage(_('No destination URL available!'), 2);
            return;
        }
        var sourcetool = this.getTool('sourceedittool');
        if (sourcetool) {sourcetool.cancelSourceMode();};

        // make sure people can't edit or save during saving
        if (!this._initialized) {
            return;
        }
        this._initialized = false;
        
        // set the window status so people can see we're actually saving
        window.status= _("Please wait while saving document...");

        // call (optional) beforeSave() method on all tools
        for (var id in this.tools) {
            var tool = this.tools[id];
            if (tool.beforeSave) {
                try {
                    tool.beforeSave();
                } catch(e) {
                    alert(e);
                    this._initialized = true;
                    return;
                };
            };
        };
        
        // pass the content through the filters
        this.logMessage(_("Starting HTML cleanup"));
        var transform = this._filterContent(this.getInnerDocument().documentElement);

        // serialize to a string
        var contents = this._serializeOutputToString(transform);
        
        this.logMessage(_("Cleanup done, sending document to server"));
        var request = new XMLHttpRequest();
    
        if (!synchronous) {
            request.onreadystatechange = (new ContextFixer(this._saveCallback, 
                                               this, request, redirect)).execute;
            request.open("PUT", this.config.dst, true);
            request.setRequestHeader("Content-type", this.config.content_type);
            request.send(contents);
            this.logMessage(_("Request sent to server"));
        } else {
            this.logMessage(_('Sending request to server'));
            request.open("PUT", this.config.dst, false);
            request.setRequestHeader("Content-type", this.config.content_type);
            request.send(contents);
            this.handleSaveResponse(request,redirect)
        };
    };
    
    this.prepareForm = function(form, id) {
        /* add a field to the form and place the contents in it

            can be used for simple POST support where Kupu is part of a
            form
        */
        var sourcetool = this.getTool('sourceedittool');
        if (sourcetool) {sourcetool.cancelSourceMode();};

        // make sure people can't edit or save during saving
        if (!this._initialized) {
            return;
        }
        this._initialized = false;
        
        // set the window status so people can see we're actually saving
        window.status= _("Please wait while saving document...");

        // call (optional) beforeSave() method on all tools
        for (var tid in this.tools) {
            var tool = this.tools[tid];
            if (tool.beforeSave) {
                try {
                    tool.beforeSave();
                } catch(e) {
                    alert(e);
                    this._initialized = true;
                    return;
                };
            };
        };
        
        // set a default id
        if (!id) {
            id = 'kupu';
        };
        
        // pass the content through the filters
        this.logMessage(_("Starting HTML cleanup"));
        var transform = this._filterContent(this.getInnerDocument().documentElement);
        
        // XXX need to fix this.  Sometimes a spurious "\n\n" text 
        // node appears in the transform, which breaks the Moz 
        // serializer on .xml
        var contents =  this._serializeOutputToString(transform);
        
        this.logMessage(_("Cleanup done, sending document to server"));
        
        // now create the form input, since IE 5.5 doesn't support the 
        // ownerDocument property we use window.document as a fallback (which
        // will almost by definition be correct).
        var document = form.ownerDocument ? form.ownerDocument : window.document;
        var ta = document.createElement('textarea');
        ta.style.visibility = 'hidden';
        var text = document.createTextNode(contents);
        ta.appendChild(text);
        ta.setAttribute('name', id);
        
        // and add it to the form
        form.appendChild(ta);

        // let the calling code know we have added the textarea
        return true;
    };

    this.execCommand = function(command, param) {
        /* general stuff like making current selection bold, italics etc. 
            and adding basic elements such as lists
            */
        if (!this._initialized) {
            this.logMessage(_('Editor not initialized yet!'));
            return;
        };
        if (this.getBrowserName() == "IE") {
            this._restoreSelection();
        } else {
            this.focusDocument();
            if (command != 'useCSS') {
                this.content_changed = true;
                // note the negation: the argument doesn't work as
                // expected...
                // Done here otherwise it doesn't always work or gets lost
                // after some commands
                this.getDocument().execCommand('useCSS', !this.config.use_css);
            };
        };
        this.getDocument().execCommand(command, param);
        var message = _('Command ${command} executed', {'command': command});
        if (param) {
            message = _('Command ${command} executed with parameter ${param}',
                            {'command': command, 'param': param});
        }
        this.updateState();
        this.logMessage(message);
    };

    this.getSelection = function() {
        /* returns a Selection object wrapping the current selection */
        this._restoreSelection();
        return this.getDocument().getSelection();
    };

    this.getSelectedNode = function() {
        /* returns the selected node (read: parent) or none */
        return this.getSelection().parentElement();
    };

    this.getNearestParentOfType = function(node, type) {
        /* well the title says it all ;) */
        var type = type.toLowerCase();
        while (node) {
            if (node.nodeName.toLowerCase() == type) {
                return node
            }   
            var node = node.parentNode;
        }
        return false;
    };

    this.removeNearestParentOfType = function(node, type) {
        var nearest = this.getNearestParentOfType(node, type);
        if (!nearest) {
            return false;
        };
        var parent = nearest.parentNode;
        while (nearest.childNodes.length) {
            var child = nearest.firstChild;
            child = nearest.removeChild(child);
            parent.insertBefore(child, nearest);
        };
        parent.removeChild(nearest);
    };

    this.getDocument = function() {
        /* returns a reference to the document object that wraps the iframe */
        return this.document;
    };

    this.getInnerDocument = function() {
        /* returns a reference to the window.document object of the iframe */
        return this.getDocument().getDocument();
    };

    this.insertNodeAtSelection = function(insertNode, selectNode) {
        /* insert a newly created node into the document */
        if (!this._initialized) {
            this.logMessage(_('Editor not initialized yet!'));
            return;
        };

        this.content_changed = true;

        var browser = this.getBrowserName();
        if (browser != "IE") {
            this.focusDocument();
        };
        
        var ret = this.getSelection().replaceWithNode(insertNode, selectNode);
        this._saveSelection();

        return ret;
    };

    this.focusDocument = function() {
        this.getDocument().getWindow().focus();
    }

    this.logMessage = function(message, severity) {
        /* log a message using the logger, severity can be 0 (message, default), 1 (warning) or 2 (error) */
        this.log.log(message, severity);
    };

    this.registerContentChanger = function(element) {
        /* set this.content_changed to true (marking the content changed) when the 
            element's onchange is called
        */
        addEventHandler(element, 'change', function() {this.content_changed = true;}, this);
    };
    
    // helper methods
    this.getBrowserName = function() {
        /* returns either 'Mozilla' (for Mozilla, Firebird, Netscape etc.) or 'IE' */
        if (_SARISSA_IS_MOZ) {
            return "Mozilla";
        } else if (_SARISSA_IS_IE) {
            return "IE";
        } else {
            throw _("Browser not supported!");
        }
    };
    
    this.handleSaveResponse = function(request, redirect) {
        // mind the 1223 status, somehow IE gives that sometimes (on 204?)
        // at first we didn't want to add it here, since it's a specific IE
        // bug, but too many users had trouble with it...
        if (request.status != '200' && request.status != '204' &&
                request.status != '1223') {
            var msg = _('Error saving your data.\nResponse status: ' + 
                            '${status}.\nCheck your server log for more ' +
                            'information.', {'status': request.status});
            alert(msg);
            window.status = _("Error saving document");
        } else if (redirect) { // && (!request.status || request.status == '200' || request.status == '204'))
            window.document.location = redirect;
            this.content_changed = false;
        } else {
            // clear content_changed before reloadSrc so saveOnPart is not triggered
            this.content_changed = false;
            if (this.config.reload_after_save) {
                this.reloadSrc();
            };
            // we're done so we can start editing again
            window.status= _("Document saved");
        };
        this._initialized = true;
    };

    // private methods
    this._addEventHandler = addEventHandler;

    this._saveCallback = function(request, redirect) {
        /* callback for Sarissa */
        if (request.readyState == 4) {
            this.handleSaveResponse(request, redirect)
        };
    };
    
    this.reloadSrc = function() {
        /* reload the src, called after a save when reload_src is set to true */
        // XXX Broken!!!
        /*
        if (this.getBrowserName() == "Mozilla") {
            this.getInnerDocument().designMode = "Off";
        }
        */
        // XXX call reloadSrc() which has a workaround, reloads the full page
        // instead of just the iframe...
        this.getDocument().reloadSource();
        if (this.getBrowserName() == "Mozilla") {
            this.getInnerDocument().designMode = "On";
        };
        /*
        var selNode = this.getSelectedNode();
        this.updateState(selNode);
        */
    };

    this._initializeEventHandlers = function() {
        /* attache the event handlers to the iframe */
        // Initialize DOM2Event compatibility
        // XXX should come back and change to passing in an element
        this._addEventHandler(this.getInnerDocument(), "click", this.updateStateHandler, this);
        this._addEventHandler(this.getInnerDocument(), "dblclick", this.updateStateHandler, this);
        this._addEventHandler(this.getInnerDocument(), "keyup", this.updateStateHandler, this);
        this._addEventHandler(this.getInnerDocument(), "keyup", function() {this.content_changed = true}, this);
        this._addEventHandler(this.getInnerDocument(), "mouseup", this.updateStateHandler, this);
    };

    this._setDesignModeWhenReady = function() {
        /* Rather dirty polling loop to see if Mozilla is done doing it's
            initialization thing so design mode can be set.
        */
        this._designModeSetAttempts++;
        if (this._designModeSetAttempts > 25) {
            alert(_('Couldn\'t set design mode. Kupu will not work on this browser.'));
            return;
        };
        var success = false;
        try {
            this._setDesignMode();
            success = true;
        } catch (e) {
            // register a function to the timer_instance because 
            // window.setTimeout can't refer to 'this'...
            timer_instance.registerFunction(this, this._setDesignModeWhenReady, 100);
        };
        if (success) {
            // provide an 'afterInit' method on KupuEditor.prototype
            // for additional bootstrapping (after editor init)
            if (this.afterInit) {
                this.afterInit();
            };
        };
    };

    this._setDesignMode = function() {
        this.getInnerDocument().designMode = "On";
        this.execCommand("undo");
        // note the negation: the argument doesn't work as expected...
        this._initialized = true;
    };

    this._saveSelection = function() {
        /* Save the selection, works around a problem with IE where the 
         selection in the iframe gets lost. We only save if the current 
         selection in the document */
        if (this._isDocumentSelected()) {
            var currange = this.getInnerDocument().selection.createRange();
            this._previous_range = currange;
        };
    };

    this._restoreSelection = function() {
        /* re-selects the previous selection in IE. We only restore if the
        current selection is not in the document.*/
        if (this._previous_range && !this._isDocumentSelected()) {
            try {
                this._previous_range.select();
            } catch (e) {
                alert("Error placing back selection");
                this.logMessage(_('Error placing back selection'));
            };
        };
    };
    
    if (this.getBrowserName() != "IE") {
        this._saveSelection = function() {};
        this._restoreSelection = function() {};
    }

    this._isDocumentSelected = function() {
        var editable_body = this.getInnerDocument().getElementsByTagName('body')[0];
        try {
            var selrange = this.getInnerDocument().selection.createRange();
        } catch(e) {
            return false;
        }
        var someelement = selrange.parentElement ? selrange.parentElement() : selrange.item(0);

        while (someelement.nodeName.toLowerCase() != 'body') {
            someelement = someelement.parentNode;
        };
        
        return someelement == editable_body;
    };

    this._clearSelection = function() {
        /* clear the last stored selection */
        this._previous_range = null;
    };

    this._filterContent = function(documentElement) {            
        /* pass the content through all the filters */
        // first copy all nodes to a Sarissa document so it's usable
        var xhtmldoc = Sarissa.getDomDocument();
        var doc = this._convertToSarissaNode(xhtmldoc, documentElement);
        // now pass it through all filters
        for (var i=0; i < this.filters.length; i++) {
            var doc = this.filters[i].filter(xhtmldoc, doc);
        };
        // fix some possible structural problems, such as an empty or missing head, title
        // or script or textarea tags without closing tag...
        this._fixXML(doc, xhtmldoc);
        return doc;
    };

    this.getXMLBody = function(transform) {
        var bodies = transform.getElementsByTagName('body');
        var data = '';
        for (var i = 0; i < bodies.length; i++) {
            data += Sarissa.serialize(bodies[i]);
        }
        return this.escapeEntities(data);
    };

    this.getHTMLBody = function() {
        var doc = this.getInnerDocument();
        var docel = doc.documentElement;
        var bodies = docel.getElementsByTagName('body');
        var data = '';
        for (var i = 0; i < bodies.length; i++) {
            data += bodies[i].innerHTML;
        }
        return this.escapeEntities(data);
    };

    // If we have multiple bodies this needs to remove the extras.
    this.setHTMLBody = function(text) {
        var bodies = this.getInnerDocument().documentElement.getElementsByTagName('body');
        for (var i = 0; i < bodies.length-1; i++) {
            bodies[i].parentNode.removeChild(bodies[i]);
        }
        bodies[bodies.length-1].innerHTML = text;
    };

    this._fixXML = function(doc, document) {
        /* fix some structural problems in the XML that make it invalid XTHML */
        // find if we have a head and title, and if not add them
        var heads = doc.getElementsByTagName('head');
        var titles = doc.getElementsByTagName('title');
        if (!heads.length) {
            // assume we have a body, guess Kupu won't work without one anyway ;)
            var body = doc.getElementsByTagName('body')[0];
            var head = document.createElement('head');
            body.parentNode.insertBefore(head, body);
            var title = document.createElement('title');
            var titletext = document.createTextNode('');
            head.appendChild(title);
            title.appendChild(titletext);
        } else if (!titles.length) {
            var head = heads[0];
            var title = document.createElement('title');
            var titletext = document.createTextNode('');
            head.appendChild(title);
            title.appendChild(titletext);
        };
        // create a closing element for all elements that require one in XHTML
        var dualtons = new Array('a', 'abbr', 'acronym', 'address', 'applet', 
                                    'b', 'bdo', 'big', 'blink', 'blockquote', 
                                    'button', 'caption', 'center', 'cite', 
                                    'comment', 'del', 'dfn', 'dir', 'div',
                                    'dl', 'dt', 'em', 'embed', 'fieldset',
                                    'font', 'form', 'frameset', 'h1', 'h2',
                                    'h3', 'h4', 'h5', 'h6', 'i', 'iframe',
                                    'ins', 'kbd', 'label', 'legend', 'li',
                                    'listing', 'map', 'marquee', 'menu',
                                    'multicol', 'nobr', 'noembed', 'noframes',
                                    'noscript', 'object', 'ol', 'optgroup',
                                    'option', 'p', 'pre', 'q', 's', 'script',
                                    'select', 'small', 'span', 'strike', 
                                    'strong', 'style', 'sub', 'sup', 'table',
                                    'tbody', 'td', 'textarea', 'tfoot',
                                    'th', 'thead', 'title', 'tr', 'tt', 'u',
                                    'ul', 'xmp');
        // XXX I reckon this is *way* slow, can we use XPath instead or
        // something to speed this up?
        for (var i=0; i < dualtons.length; i++) {
            var elname = dualtons[i];
            var els = doc.getElementsByTagName(elname);
            for (var j=0; j < els.length; j++) {
                var el = els[j];
                if (!el.hasChildNodes()) {
                    var child = document.createTextNode('');
                    el.appendChild(child);
                };
            };
        };
    };

    this.xhtmlvalid = new XhtmlValidation(this);

    this._convertToSarissaNode = function(ownerdoc, htmlnode) {
        /* Given a string of non-well-formed HTML, return a string of 
           well-formed XHTML.

           This function works by leveraging the already-excellent HTML 
           parser inside the browser, which generally can turn a pile 
           of crap into a DOM.  We iterate over the HTML DOM, appending 
           new nodes (elements and attributes) into a node.

           The primary problems this tries to solve for crappy HTML: mixed 
           element names, elements that open but don't close, 
           and attributes that aren't in quotes.  This can also be adapted 
           to filter out tags that you don't want and clean up inline styles.

           Inspired by Guido, adapted by Paul from something in usenet.
           Tag and attribute tables added by Duncan
        */
        return this.xhtmlvalid._convertToSarissaNode(ownerdoc, htmlnode);
    };

    this._fixupSingletons = function(xml) {
        return xml.replace(/<([^>]+)\/>/g, "<$1 />");
    }
    this._serializeOutputToString = function(transform) {
        // XXX need to fix this.  Sometimes a spurious "\n\n" text 
        // node appears in the transform, which breaks the Moz 
        // serializer on .xml
            
        if (this.config.strict_output) {
            var contents =  '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' + 
                            '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' + 
                            '<html xmlns="http://www.w3.org/1999/xhtml">' +
                            Sarissa.serialize(transform.getElementsByTagName("head")[0]) +
                            Sarissa.serialize(transform.getElementsByTagName("body")[0]) +
                            '</html>';
        } else {
            var contents = '<html>' + 
                            Sarissa.serialize(transform.getElementsByTagName("head")[0]) +
                            Sarissa.serialize(transform.getElementsByTagName("body")[0]) +
                            '</html>';
        };

        contents = this.escapeEntities(contents);

        if (this.config.compatible_singletons) {
            contents = this._fixupSingletons(contents);
        };
        
        return contents;
    };
    this.escapeEntities = function(xml) {
        // XXX: temporarily disabled
        return xml;
        // Escape non-ascii characters as entities.
        return xml.replace(/[^\r\n -\177]/g,
            function(c) {
            return '&#'+c.charCodeAt(0)+';';
        });
    }

    this.getFullEditor = function() {
        var fulleditor = this.getDocument().getEditable();
        while (!/kupu-fulleditor/.test(fulleditor.className)) {
            fulleditor = fulleditor.parentNode;
        }
        return fulleditor;
    }
    // Control the className and hence the style for the whole editor.
    this.setClass = function(name) {
        this.getFullEditor().className += ' '+name;
    }
    
    this.clearClass = function(name) {
        var fulleditor = this.getFullEditor();
        fulleditor.className = fulleditor.className.replace(' '+name, '');
    }

    this.suspendEditing = function() {
        this._previous_range = this.getSelection().getRange();
        this.setClass('kupu-modal');
        for (var id in this.tools) {
            this.tools[id].disable();
        }
        if (this.getBrowserName() == "IE") {
            var body = this.getInnerDocument().getElementsByTagName('body')[0];
            body.setAttribute('contentEditable', 'false');
        } else {

            this.getInnerDocument().designMode = "Off";
            var iframe = this.getDocument().getEditable();
            iframe.style.position = iframe.style.position?"":"relative"; // Changing this disables designMode!
        }
        this.suspended = true;
    }
    
    this.resumeEditing = function() {
        if (!this.suspended) {
            return;
        }
        this.suspended = false;
        this.clearClass('kupu-modal');
        for (var id in this.tools) {
            this.tools[id].enable();
        }
        if (this.getBrowserName() == "IE") {
            this._restoreSelection();
            var body = this.getInnerDocument().getElementsByTagName('body')[0];
            body.setAttribute('contentEditable', 'true');
        } else {
            var doc = this.getInnerDocument();
            doc.designMode = "On";
            this.getSelection().restoreRange(this._previous_range);
        }
    }
}

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupubasetools.js 27123 2006-05-12 10:19:22Z duncan $


//----------------------------------------------------------------------------
//
// Toolboxes
//
//  These are addons for Kupu, simple plugins that implement a certain 
//  interface to provide functionality and control view aspects.
//
//----------------------------------------------------------------------------

//----------------------------------------------------------------------------
// Superclasses
//----------------------------------------------------------------------------

function KupuTool() {
    /* Superclass (or actually more of an interface) for tools 
    
        Tools must implement at least an initialize method and an 
        updateState method, and can implement other methods to add 
        certain extra functionality (e.g. createContextMenuElements).
    */

    this.toolboxes = {};

    // methods
    this.initialize = function(editor) {
        /* Initialize the tool.

            Obviously this can be overriden but it will do
            for the most simple cases
        */
        this.editor = editor;
    };

    this.registerToolBox = function(id, toolbox) {
        /* register a ui box 
        
            Note that this needs to be called *after* the tool has been 
            registered to the KupuEditor
        */
        this.toolboxes[id] = toolbox;
        toolbox.initialize(this, this.editor);
    };
    
    this.updateState = function(selNode, event) {
        /* Is called when user moves cursor to other element 

            Calls the updateState for all toolboxes and may want perform
            some actions itself
        */
        for (id in this.toolboxes) {
            this.toolboxes[id].updateState(selNode, event);
        };
    };

    this.enable = function() {
        // Called when the tool is enabled after a form is dismissed.
    }

    this.disable = function() {
        // Called when the tool is disabled (e.g. for a modal form)
    }
    // private methods
    addEventHandler = addEventHandler;
    
    this._selectSelectItem = function(select, item) {
        this.editor.logMessage(_('Deprecation warning: KupuTool._selectSelectItem'));
    };
    this._fixTabIndex = function(element) {
        var tabIndex = this.editor.getDocument().getEditable().tabIndex-1;
        if (tabIndex && !element.tabIndex) {
            element.tabIndex = tabIndex;
        }
    }
}

function KupuToolBox() {
    /* Superclass for a user-interface object that controls a tool */

    this.initialize = function(tool, editor) {
        /* store a reference to the tool and the editor */
        this.tool = tool;
        this.editor = editor;
    };

    this.updateState = function(selNode, event) {
        /* update the toolbox according to the current iframe's situation */
    };
    
    this._selectSelectItem = function(select, item) {
        this.editor.logMessage(_('Deprecation warning: KupuToolBox._selectSelectItem'));
    };
};

function NoContextMenu(object) {
    /* Decorator for a tool to suppress the context menu */
    object.createContextMenuElements = function(selNode, event) {
        return [];
    }
    return object;
}

// Helper function for enabling/disabling tools
function KupuButtonDisable(button) {
    button = button || this.button;
    button.disabled = "disabled";
    button.className += ' disabled';
}
function KupuButtonEnable(button) {
    button = button || this.button;
    button.disabled = "";
    button.className = button.className.replace(/ *\bdisabled\b/g, '');
}


//----------------------------------------------------------------------------
// Implementations
//----------------------------------------------------------------------------

function KupuButton(buttonid, commandfunc, tool) {
    /* Base prototype for kupu button tools */
    this.buttonid = buttonid;
    this.button = getFromSelector(buttonid);
    this.commandfunc = commandfunc;
    this.tool = tool;

    this.initialize = function(editor) {
        this.editor = editor;
        this._fixTabIndex(this.button);
        addEventHandler(this.button, 'click', this.execCommand, this);
    };

    this.execCommand = function() {
        /* exec this button's command */
        this.commandfunc(this, this.editor, this.tool);
    };

    this.updateState = function(selNode, event) {
        /* override this in subclasses to determine whether a button should
            look 'pressed in' or not
        */
    };
    this.disable = KupuButtonDisable;
    this.enable = KupuButtonEnable;
};

KupuButton.prototype = new KupuTool;
function KupuStateButton(buttonid, commandfunc, checkfunc, offclass, onclass) {
    /* A button that can have two states (e.g. pressed and
       not-pressed) based on CSS classes */
    this.buttonid = buttonid;
    this.button = getFromSelector(buttonid);
    this.commandfunc = commandfunc;
    this.checkfunc = checkfunc;
    this.offclass = offclass;
    this.onclass = onclass;
    this.pressed = false;

    this.execCommand = function() {
        /* exec this button's command */
        this.button.className = (this.pressed ? this.offclass : this.onclass);
        this.pressed = !this.pressed;
        this.editor.focusDocument();
        this.commandfunc(this, this.editor);
    };

    this.updateState = function(selNode, event) {
        /* check if we need to be clicked or unclicked, and update accordingly 
        
            if the state of the button should be changed, we set the class
        */
        var currclass = this.button.className;
        var newclass = null;
        if (this.checkfunc(selNode, this, this.editor, event)) {
            newclass = this.onclass;
            this.pressed = true;
        } else {
            newclass = this.offclass;
            this.pressed = false;
        };
        if (currclass != newclass) {
            this.button.className = newclass;
        };
    };
};

KupuStateButton.prototype = new KupuButton;

/* Same as the state button, but the focusDocument call is delayed.
 * Mozilla&Firefox have a bug on windows which can cause a crash if you
 * change CSS positioning styles on an element which has focus.
 */
function KupuLateFocusStateButton(buttonid, commandfunc, checkfunc, offclass, onclass) {
    KupuStateButton.apply(this, [buttonid, commandfunc, checkfunc, offclass, onclass]);
    this.execCommand = function() {
        /* exec this button's command */
        this.button.className = (this.pressed ? this.offclass : this.onclass);
        this.pressed = !this.pressed;
        this.commandfunc(this, this.editor);
        this.editor.focusDocument();
    };
}
KupuLateFocusStateButton.prototype = new KupuStateButton;

function KupuRemoveElementButton(buttonid, element_name, cssclass) {
    /* A button specialized in removing elements in the current node
       context. Typical usages include removing links, images, etc. */
    this.button = getFromSelector(buttonid);
    this.onclass = 'invisible';
    this.offclass = cssclass;
    this.pressed = false;

    this.commandfunc = function(button, editor) {
        editor.removeNearestParentOfType(editor.getSelectedNode(), element_name);
    };

    this.checkfunc = function(currnode, button, editor, event) {
        var element = editor.getNearestParentOfType(currnode, element_name);
        return (element ? false : true);
    };
};

KupuRemoveElementButton.prototype = new KupuStateButton;

function KupuUI(textstyleselectid) {
    /* View 
    
        This is the main view, which controls most of the toolbar buttons.
        Even though this is probably never going to be removed from the view,
        it was easier to implement this as a plain tool (plugin) as well.
    */
    
    // attributes
    this.tsselect = getFromSelector(textstyleselectid);
    var paraoptions = [];
    var tableoptions = [];
    this.optionstate = -1;
    this.otherstyle = null;
    this.tablestyles = {};
    this.styles = {}; // use an object here so we can use the 'in' operator later on

    this.initialize = function(editor) {
        /* initialize the ui like tools */
        this.editor = editor;
        this.cleanStyles();
        this.enableOptions(false);
        this._fixTabIndex(this.tsselect);
        this._selectevent = addEventHandler(this.tsselect, 'change', this.setTextStyleHandler, this);
    };

    this.getStyles = function() {
        if (!paraoptions) {
            this.cleanStyles();
        }
        return [ paraoptions, tableoptions ];
    }

    this.setTextStyleHandler = function(event) {
        this.setTextStyle(this.tsselect.options[this.tsselect.selectedIndex].value);
    };
    
    // event handlers
    this.basicButtonHandler = function(action) {
        /* event handler for basic actions (toolbar buttons) */
        this.editor.execCommand(action);
        this.editor.updateState();
    };

    this.saveButtonHandler = function() {
        /* handler for the save button */
        this.editor.saveDocument();
    };

    this.saveAndExitButtonHandler = function(redirect_url) {
        /* save the document and, if successful, redirect */
        this.editor.saveDocument(redirect_url);
    };

    this.cutButtonHandler = function() {
        try {
            this.editor.execCommand('Cut');
        } catch (e) {
            if (this.editor.getBrowserName() == 'Mozilla') {
                alert(_('Cutting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
            } else {
                throw e;
            };
        };
        this.editor.updateState();
    };

    this.copyButtonHandler = function() {
        try {
            this.editor.execCommand('Copy');
        } catch (e) {
            if (this.editor.getBrowserName() == 'Mozilla') {
                alert(_('Copying from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
            } else {
                throw e;
            };
        };
        this.editor.updateState();
    };

    this.pasteButtonHandler = function() {
        try {
            this.editor.execCommand('Paste');
        } catch (e) {
            if (this.editor.getBrowserName() == 'Mozilla') {
                alert(_('Pasting from JavaScript is disabled on your Mozilla due to security settings. For more information, read http://www.mozilla.org/editor/midasdemo/securityprefs.html'));
            } else {
                throw e;
            };
        };
        this.editor.updateState();
    };

    this.cleanStyles = function() {
        var options = this.tsselect.options;
        var parastyles = this.styles;
        var tablestyles = this.tablestyles;

        tableoptions.push([options[0].text, 'td|']);
        tablestyles['td'] = 0;
        paraoptions.push([options[0].text, 'p|']);
        parastyles['p'] = 0;
        while (options.length > 1) {
            opt = options[1];
            var v = opt.value.toLowerCase();
            if (/^thead|tbody|table|t[rdh]\b/.test(v)) {
                var otable = tableoptions;
                var styles = tablestyles;
            } else {
                var otable = paraoptions;
                var styles = parastyles;
            }
            if (v.indexOf('|') > -1) {
                var split = v.split('|');
                v = split[0].toLowerCase() + "|" + split[1];
            } else {
                v = v.toLowerCase()+"|";
            };
            otable.push([opt.text, v]);
            styles[v] = otable.length - 1;
            options[1] = null;
        }
        options[0] = null;
    }

    // Remove otherstyle and switch to appropriate style set.
    this.enableOptions = function(inTable) {
        var select = this.tsselect;
        var options = select.options;
        if (this.otherstyle) {
            options[options.length-1] = null;
            this.otherstyle = null;
        }
        if (this.optionstate == inTable) return; /* No change */

        var valid = inTable ? tableoptions : paraoptions;

        while (options.length) options[0] = null;
        this.otherstyle = null;

        for (var i = 0; i < valid.length; i++) {
            var opt = document.createElement('option');
            opt.text = valid[i][0];
            opt.value = valid[i][1];
            options.add(opt);
        }
        select.selectedIndex = 0;
        this.optionstate = inTable;
    }
    
    this.setIndex = function(currnode, tag, index, styles) {
        var className = currnode.className;
        this.styletag = tag;
        this.classname = className;
        var style = tag+'|'+className;

        if (style in styles) {
            return styles[style];
        } else if (!className && tag in styles) {
            return styles[tag];
        }
        return index;
    }

    this.nodeStyle = function(node) {
        var currnode = node;
        var index = -1;
        var options = this.tsselect.options;
        this.styletag = undefined;
        this.classname = '';
        this.intable = false;

        while (currnode) {
            var tag = currnode.nodeName.toLowerCase();

            if (/^body$/.test(tag)) {
                if (!this.styletag) {
                    // Force style setting
                    //this.setTextStyle(options[0].value, true);
                    // Forced style messes up in Firefox: return -1 to
                    // indicate no style 
                    return -1;
                }
                break;
            }
            if (/^(p|div|h.|ul|ol|dl|menu|dir|pre|blockquote|address|center)$/.test(tag)) {
                index = this.setIndex(currnode, tag, index, this.styles);
            }
            if (/^thead|tbody|table|t[rdh]$/.test(tag)) {
                this.intable = true;
                index = this.setIndex(currnode, tag, index, this.tablestyles);

                if (index > 0 || tag=='table') {
                    return index; // Stop processing if in a table
                }
            }
            currnode = currnode.parentNode;
        }
        return index;
    }

    this.updateState = function(selNode) {
        /* set the text-style pulldown */

        // first get the nearest style
        // search the list of nodes like in the original one, break if we encounter a match,
        // this method does some more than the original one since it can handle commands in
        // the form of '<style>|<classname>' next to the plain
        // '<style>' commands
        var index = undefined;
        var mixed = false;
        var styletag, classname;

        var selection = this.editor.getSelection();

        for (var el=selNode.firstChild; el; el=el.nextSibling) {
            if (el.nodeType==1 && selection.containsNode(el)) {
                var i = this.nodeStyle(el);
                if (index===undefined) {
                    index = i;
                    styletag = this.styletag;
                    classname = this.classname;
                }
                if (index != i || styletag!=this.styletag || classname != this.classname) {
                    mixed = true;
                    break;
                }
            }
        };

        if (index===undefined) {
            index = this.nodeStyle(selNode);
        }

        this.enableOptions(this.intable);

        if (index < 0 || mixed) {
            if (mixed) {
                var caption = 'Mixed styles';
            } else if (this.styletag) {
                var caption = 'Other: ' + this.styletag + ' '+ this.classname;
            } else {
                var caption = '<no style>';
            }

            var opt = document.createElement('option');
            opt.text = caption;
            this.otherstyle = opt;
            this.tsselect.options.add(opt);

            index = this.tsselect.length-1;
        }
        this.tsselect.selectedIndex = Math.max(index,0);
    };

    this._cleanNode = function(node) {
                /* Clean up a block style node (e.g. P, DIV, Hn)
                 * Remove trailing whitespace, then also remove up to one
                 * trailing <br>
                 * If the node is now empty, remove the node itself.
                 */
        var len = node.childNodes.length;
        function stripspace() {
            var c;
            while ((c = node.lastChild) && c.nodeType==3 && /^\s*$/.test(c.data)) {
                node.removeChild(c);
            }
        }
        stripspace();
        var c = node.lastChild;
        if (c && c.nodeType==1 && c.tagName=='BR') {
            node.removeChild(c);
        }
        stripspace();
        if (node.childNodes.length==0) {
            node.parentNode.removeChild(node);
        };
    }

    this._cleanCell = function(eltype, classname) {
        var selNode = this.editor.getSelectedNode();
        var el = this.editor.getNearestParentOfType(selNode, eltype);
        if (!el) {
                // Maybe changing type
            el = this.editor.getNearestParentOfType(selNode, eltype=='TD'?'TH':'TD');
        }
        if (!el) return;

            // Remove formatted div or p from a cell
        var node, nxt, n;
        for (node = el.firstChild; node;) {
            if (/DIV|P/.test(node.nodeName)) {
                for (var n = node.firstChild; n;) {
                    var nxt = n.nextSibling;
                    el.insertBefore(n, node); // Move nodes out of div
                    n = nxt;
                }
                nxt = node.nextSibling;
                el.removeChild(node);
                node = nxt;
            } else {
                node = node.nextSibling;
            }
        }
        if (eltype != el.tagName) {
                // Change node type.
            var node = el.ownerDocument.createElement(eltype);
            var parent = el.parentNode;
            parent.insertBefore(node, el);
            while (el.firstChild) {
                node.appendChild(el.firstChild);
            }
            parent.removeChild(el);
            el = node;
        }
            // now set the classname
        if (classname) {
            el.className = classname;
        } else {
            el.removeAttribute("class");
            el.removeAttribute("className");
        }

    }

    this._setClass = function(el, classname) {
        var parent = el.parentNode;
        if (parent.tagName=='DIV') {
            // fixup buggy formatting
            var gp = parent.parentNode;
            if (el != parent.firstChild) {
                var previous = parent.cloneNode(false);
                while (el != parent.firstChild) {
                    previous.appendChild(parent.firstChild);
                }
                gp.insertBefore(previous, parent);
                this._cleanNode(previous);
            }
            gp.insertBefore(el, parent);
            this._cleanNode(el);
            this._cleanNode(parent);
        } else {
            this._cleanNode(el);
        }
        // now set the classname
        if (classname) {
            el.className = classname;
        } else {
            el.removeAttribute("class");
            el.removeAttribute("className");
        }
    }
    this.setTextStyle = function(style, noupdate) {
            /* parse the argument into a type and classname part
               generate a block element accordingly 
            */
        var classname = '';
        var eltype = style.toUpperCase();
        if (style.indexOf('|') > -1) {
            style = style.split('|');
            eltype = style[0].toUpperCase();
            classname = style[1];
        };

        var command = eltype;
            // first create the element, then find it and set the classname
        if (this.editor.getBrowserName() == 'IE') {
            command = '<' + eltype + '>';
        };
        if (/T[RDH]/.test(eltype)) {
            this._cleanCell(eltype, classname);
        }
        else {
            this.editor.getDocument().execCommand('formatblock', command);

                // now get a reference to the element just added
            var selNode = this.editor.getSelectedNode();
            var el = this.editor.getNearestParentOfType(selNode, eltype);
            if (el) {
                this._setClass(el, classname);
            } else {
                var selection = this.editor.getSelection();
                var elements = selNode.getElementsByTagName(eltype);
                for (var i = 0; i < elements.length; i++) {
                    el = elements[i];
                    if (selection.containsNode(el)) {
                        this._setClass(el, classname);
                    }
                }
            }
        }
        if (el) {
            this.editor.getSelection().selectNodeContents(el);
        }
        if (!noupdate) {
            this.editor.updateState();
        }
    };
  
    this.createContextMenuElements = function(selNode, event) {
        var ret = new Array();
        ret.push(new ContextMenuElement(_('Cut'), 
                    this.cutButtonHandler, this));
        ret.push(new ContextMenuElement(_('Copy'), 
                    this.copyButtonHandler, this));
        ret.push(new ContextMenuElement(_('Paste'), 
                    this.pasteButtonHandler, this));
        return ret;
    };
    this.disable = function() {
        this.tsselect.disabled = "disabled";
    }
    this.enable = function() {
        this.tsselect.disabled = "";
    }
}

KupuUI.prototype = new KupuTool;

function ColorchooserTool(fgcolorbuttonid, hlcolorbuttonid, colorchooserid) {
    /* the colorchooser */
    
    this.fgcolorbutton = getFromSelector(fgcolorbuttonid);
    this.hlcolorbutton = getFromSelector(hlcolorbuttonid);
    this.ccwindow = getFromSelector(colorchooserid);
    this.command = null;

    this.initialize = function(editor) {
        /* attach the event handlers */
        this.editor = editor;
        
        this.createColorchooser(this.ccwindow);

        addEventHandler(this.fgcolorbutton, "click", this.openFgColorChooser, this);
        addEventHandler(this.hlcolorbutton, "click", this.openHlColorChooser, this);
        addEventHandler(this.ccwindow, "click", this.chooseColor, this);

        this.hide();

        this.editor.logMessage(_('Colorchooser tool initialized'));
    };

    this.updateState = function(selNode) {
        /* update state of the colorchooser */
        this.hide();
    };

    this.openFgColorChooser = function() {
        /* event handler for opening the colorchooser */
        this.command = "forecolor";
        this.show();
    };

    this.openHlColorChooser = function() {
        /* event handler for closing the colorchooser */
        if (this.editor.getBrowserName() == "IE") {
            this.command = "backcolor";
        } else {
            this.command = "hilitecolor";
        }
        this.show();
    };

    this.chooseColor = function(event) {
        /* event handler for choosing the color */
        var target = _SARISSA_IS_MOZ ? event.target : event.srcElement;
        var cell = this.editor.getNearestParentOfType(target, 'td');
        this.editor.execCommand(this.command, cell.getAttribute('bgColor'));
        this.hide();
    
        this.editor.logMessage(_('Color chosen'));
    };

    this.show = function(command) {
        /* show the colorchooser */
        this.ccwindow.style.display = "block";
    };

    this.hide = function() {
        /* hide the colorchooser */
        this.command = null;
        this.ccwindow.style.display = "none";
    };

    this.createColorchooser = function(table) {
        /* create the colorchooser table */
        
        var chunks = new Array('00', '33', '66', '99', 'CC', 'FF');
        table.setAttribute('id', 'kupu-colorchooser-table');
        table.style.borderWidth = '2px';
        table.style.borderStyle = 'solid';
        table.style.position = 'absolute';
        table.style.cursor = 'default';
        table.style.display = 'none';

        var tbody = document.createElement('tbody');

        for (var i=0; i < 6; i++) {
            var tr = document.createElement('tr');
            var r = chunks[i];
            for (var j=0; j < 6; j++) {
                var g = chunks[j];
                for (var k=0; k < 6; k++) {
                    var b = chunks[k];
                    var color = '#' + r + g + b;
                    var td = document.createElement('td');
                    td.setAttribute('bgColor', color);
                    td.style.backgroundColor = color;
                    td.style.borderWidth = '1px';
                    td.style.borderStyle = 'solid';
                    td.style.fontSize = '1px';
                    td.style.width = '10px';
                    td.style.height = '10px';
                    var text = document.createTextNode('\u00a0');
                    td.appendChild(text);
                    tr.appendChild(td);
                }
            }
            tbody.appendChild(tr);
        }
        table.appendChild(tbody);

        return table;
    };
    this.enable = function() {
        KupuButtonEnable(this.fgcolorbutton);
        KupuButtonEnable(this.hlcolorbutton);
    }
    this.disable = function() {
        KupuButtonDisable(this.fgcolorbutton);
        KupuButtonDisable(this.hlcolorbutton);
    }
}

ColorchooserTool.prototype = new KupuTool;

function PropertyTool(titlefieldid, descfieldid) {
    /* The property tool */

    this.titlefield = getFromSelector(titlefieldid);
    this.descfield = getFromSelector(descfieldid);

    this.initialize = function(editor) {
        /* attach the event handlers and set the initial values */
        this.editor = editor;
        addEventHandler(this.titlefield, "change", this.updateProperties, this);
        addEventHandler(this.descfield, "change", this.updateProperties, this);
        
        // set the fields
        var heads = this.editor.getInnerDocument().getElementsByTagName('head');
        if (!heads[0]) {
            this.editor.logMessage(_('No head in document!'), 1);
        } else {
            var head = heads[0];
            var titles = head.getElementsByTagName('title');
            if (titles.length) {
                this.titlefield.value = titles[0].text;
            }
            var metas = head.getElementsByTagName('meta');
            if (metas.length) {
                for (var i=0; i < metas.length; i++) {
                    var meta = metas[i];
                    if (meta.getAttribute('name') && 
                            meta.getAttribute('name').toLowerCase() == 
                            'description') {
                        this.descfield.value = meta.getAttribute('content');
                        break;
                    }
                }
            }
        }

        this.editor.logMessage(_('Property tool initialized'));
    };

    this.updateProperties = function() {
        /* event handler for updating the properties form */
        var doc = this.editor.getInnerDocument();
        var heads = doc.getElementsByTagName('HEAD');
        if (!heads) {
            this.editor.logMessage(_('No head in document!'), 1);
            return;
        }

        var head = heads[0];

        // set the title
        var titles = head.getElementsByTagName('title');
        if (!titles) {
            var title = doc.createElement('title');
            var text = doc.createTextNode(this.titlefield.value);
            title.appendChild(text);
            head.appendChild(title);
        } else {
            var title = titles[0];
            // IE6 title has no children, and refuses appendChild.
            // Delete and recreate the title.
            if (title.childNodes.length == 0) {
                title.removeNode(true);
                title = doc.createElement('title');
                title.innerText = this.titlefield.value;
                head.appendChild(title);
            } else {
                title.childNodes[0].nodeValue = this.titlefield.value;
            }
        }
        document.title = this.titlefield.value;

        // let's just fulfill the usecase, not think about more properties
        // set the description
        var metas = doc.getElementsByTagName('meta');
        var descset = 0;
        for (var i=0; i < metas.length; i++) {
            var meta = metas[i];
            if (meta.getAttribute('name') && 
                    meta.getAttribute('name').toLowerCase() == 'description') {
                meta.setAttribute('content', this.descfield.value);
                descset = 1;
            }
        }

        if (!descset) {
            var meta = doc.createElement('meta');
            meta.setAttribute('name', 'description');
            meta.setAttribute('content', this.descfield.value);
            head.appendChild(meta);
        }

        this.editor.logMessage(_('Properties modified'));
    };
}

PropertyTool.prototype = new KupuTool;

function LinkTool() {
    /* Add and update hyperlinks */
    
    this.initialize = function(editor) {
        this.editor = editor;
        this.editor.logMessage(_('Link tool initialized'));
    };
    
    this.createLinkHandler = function(event) {
        /* create a link according to a url entered in a popup */
        var linkWindow = openPopup('kupupopups/link.html', 300, 200);
        linkWindow.linktool = this;
        linkWindow.focus();
    };

    this.updateLink = function (linkel, url, type, name, target, title) {
        if (type && type == 'anchor') {
            linkel.removeAttribute('href');
            linkel.setAttribute('name', name);
        } else {
            linkel.href = url;
            if (linkel.innerHTML == "") {
                var doc = this.editor.getInnerDocument();
                linkel.appendChild(doc.createTextNode(title || url));
            }
            if (title) {
                linkel.title = title;
            } else {
                linkel.removeAttribute('title');
            }
            if (target && target != '') {
                linkel.setAttribute('target', target);
            }
            else {
                linkel.removeAttribute('target');
            };
            linkel.style.color = this.linkcolor;
        };
    };

    this.formatSelectedLink = function(url, type, name, target, title) {
        var currnode = this.editor.getSelectedNode();

        // selection inside link
        var linkel = this.editor.getNearestParentOfType(currnode, 'A');
        if (linkel) {
            this.updateLink(linkel, url, type, name, target, title);
            return true;
        }

        if (currnode.nodeType!=1) return false;

        // selection contains links
        var linkelements = currnode.getElementsByTagName('A');
        var selection = this.editor.getSelection();
        var containsLink = false;
        for (var i = 0; i < linkelements.length; i++) {
            linkel = linkelements[i];
            if (selection.containsNode(linkel)) {
                this.updateLink(linkel, url, type, name, target, title);
                containsLink = true;
            }
        };
        return containsLink;
    }

    // Can create a link in the following circumstances:
    //   The selection is inside a link:
    //      just update the link attributes.
    //   The selection contains links:
    //      update the attributes of the contained links
    //   No links inside or outside the selection:
    //      create a link around the selection
    //   No selection:
    //      insert a link containing the title
    //
    // the order of the arguments is a bit odd here because of backward
    // compatibility
    this.createLink = function(url, type, name, target, title) {
        if (!this.formatSelectedLink(url, type, name, target, title)) {
            // No links inside or outside.
            this.editor.execCommand("CreateLink", url);
            if (!this.formatSelectedLink(url, type, name, target, title)) {
                // Insert link with no text selected, insert the title
                // or URI instead.
                var doc = this.editor.getInnerDocument();
                linkel = doc.createElement("a");
                linkel.setAttribute('href', url);
                linkel.setAttribute('class', 'generated');
                this.editor.getSelection().replaceWithNode(linkel, true);
                this.updateLink(linkel, url, type, name, target, title);
            };
        }
        this.editor.logMessage(_('Link added'));
        this.editor.updateState();
    };
    
    this.deleteLink = function() {
        /* delete the current link */
        var currnode = this.editor.getSelectedNode();
        var linkel = this.editor.getNearestParentOfType(currnode, 'a');
        if (!linkel) {
            this.editor.logMessage(_('Not inside link'));
            return;
        };
        while (linkel.childNodes.length) {
            linkel.parentNode.insertBefore(linkel.childNodes[0], linkel);
        };
        linkel.parentNode.removeChild(linkel);
        
        this.editor.logMessage(_('Link removed'));
        this.editor.updateState();
    };
    
    this.createContextMenuElements = function(selNode, event) {
        /* create the 'Create link' or 'Remove link' menu elements */
        var ret = new Array();
        var link = this.editor.getNearestParentOfType(selNode, 'a');
        if (link) {
            ret.push(new ContextMenuElement(_('Delete link'), this.deleteLink, this));
        } else {
            ret.push(new ContextMenuElement(_('Create link'), this.createLinkHandler, this));
        };
        return ret;
    };
}

LinkTool.prototype = new KupuTool;

function LinkToolBox(inputid, buttonid, toolboxid, plainclass, activeclass) {
    /* create and edit links */
    
    this.input = getFromSelector(inputid);
    this.button = getFromSelector(buttonid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;
    
    this.initialize = function(tool, editor) {
        /* attach the event handlers */
        this.tool = tool;
        this.editor = editor;
        addEventHandler(this.input, "blur", this.updateLink, this);
        addEventHandler(this.button, "click", this.addLink, this);
    };

    this.updateState = function(selNode) {
        /* if we're inside a link, update the input, else empty it */
        var linkel = this.editor.getNearestParentOfType(selNode, 'a');
        if (linkel) {
            // check first before setting a class for backward compatibility
            if (this.toolboxel) {
                this.toolboxel.className = this.activeclass;
            };
            this.input.value = linkel.getAttribute('href');
        } else {
            // check first before setting a class for backward compatibility
            if (this.toolboxel) {
                this.toolboxel.className = this.plainclass;
            };
            this.input.value = '';
        }
    };
    
    this.addLink = function(event) {
        /* add a link */
        var url = this.input.value;
        this.tool.createLink(url);
    };
    
    this.updateLink = function() {
        /* update the current link */
        var currnode = this.editor.getSelectedNode();
        var linkel = this.editor.getNearestParentOfType(currnode, 'A');
        if (!linkel) {
            return;
        }

        var url = this.input.value;
        linkel.setAttribute('href', url);

        this.editor.logMessage(_('Link modified'));
    };
};

LinkToolBox.prototype = new LinkToolBox;

function ImageTool() {
    /* Image tool to add images */
    
    this.initialize = function(editor) {
        /* attach the event handlers */
        this.editor = editor;
        this.editor.logMessage(_('Image tool initialized'));
    };

    this.createImageHandler = function(event) {
        /* create an image according to a url entered in a popup */
        var imageWindow = openPopup('kupupopups/image.html', 300, 200);
        imageWindow.imagetool = this;
        imageWindow.focus();
    };

    this.createImage = function(url, alttext, imgclass) {
        /* create an image */
        var img = this.editor.getInnerDocument().createElement('img');
        img.src = url;
        img.removeAttribute('height');
        img.removeAttribute('width');
        if (alttext) {
            img.alt = alttext;
        };
        if (imgclass) {
            img.className = imgclass;
        };
        img = this.editor.insertNodeAtSelection(img, 1);
        this.editor.logMessage(_('Image inserted'));
        this.editor.updateState();
        return img;
    };

    this.setImageClass = function(imgclass) {
        /* set the class of the selected image */
        var currnode = this.editor.getSelectedNode();
        var currimg = this.editor.getNearestParentOfType(currnode, 'IMG');
        if (currimg) {
            currimg.className = imgclass;
        };
    };

    this.createContextMenuElements = function(selNode, event) {
        return new Array(new ContextMenuElement(_('Create image'), this.createImageHandler, this));
    };
}

ImageTool.prototype = new KupuTool;

function ImageToolBox(inputfieldid, insertbuttonid, classselectid, toolboxid, plainclass, activeclass) {
    /* toolbox for adding images */

    this.inputfield = getFromSelector(inputfieldid);
    this.insertbutton = getFromSelector(insertbuttonid);
    this.classselect = getFromSelector(classselectid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;

    this.initialize = function(tool, editor) {
        this.tool = tool;
        this.editor = editor;
        addEventHandler(this.classselect, "change", this.setImageClass, this);
        addEventHandler(this.insertbutton, "click", this.addImage, this);
    };

    this.updateState = function(selNode, event) {
        /* update the state of the toolbox element */
        var imageel = this.editor.getNearestParentOfType(selNode, 'img');
        if (imageel) {
            // check first before setting a class for backward compatibility
            if (this.toolboxel) {
                this.toolboxel.className = this.activeclass;
                this.inputfield.value = imageel.getAttribute('src');
                var imgclass = imageel.className ? imageel.className : 'image-inline';
                selectSelectItem(this.classselect, imgclass);
            };
        } else {
            if (this.toolboxel) {
                this.toolboxel.className = this.plainclass;
            };
        };
    };

    this.addImage = function() {
        /* add an image */
        var url = this.inputfield.value;
        var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
        this.tool.createImage(url, null, sel_class);
        this.editor.focusDocument();
    };

    this.setImageClass = function() {
        /* set the class for the current image */
        var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
        this.tool.setImageClass(sel_class);
        this.editor.focusDocument();
    };
};

ImageToolBox.prototype = new KupuToolBox;

function TableTool() {
    /* The table tool */

    // XXX There are some awfully long methods in here!!
    this.createContextMenuElements = function(selNode, event) {
        var table =  this.editor.getNearestParentOfType(selNode, 'table');
        if (!table) {
            ret = new Array();
            var el = new ContextMenuElement(_('Add table'), this.addPlainTable, this);
            ret.push(el);
            return ret;
        } else {
            var ret = new Array();
            ret.push(new ContextMenuElement(_('Add row'), this.addTableRow, this));
            ret.push(new ContextMenuElement(_('Delete row'), this.delTableRow, this));
            ret.push(new ContextMenuElement(_('Add column'), this.addTableColumn, this));
            ret.push(new ContextMenuElement(_('Delete column'), this.delTableColumn, this));
            ret.push(new ContextMenuElement(_('Delete Table'), this.delTable, this));
            return ret;
        };
    };

    this.addPlainTable = function() {
        /* event handler for the context menu */
        this.createTable(2, 3, 1, 'plain');
    };

    this.createTable = function(rows, cols, makeHeader, tableclass) {
        /* add a table */
        if (rows < 1 || rows > 99 || cols < 1 || cols > 99) {
            this.editor.logMessage(_('Invalid table size'), 1);
            return;
        };

        var doc = this.editor.getInnerDocument();

        table = doc.createElement("table");
        table.className = tableclass;

        // If the user wants a row of headings, make them
        if (makeHeader) {
            var tr = doc.createElement("tr");
            var thead = doc.createElement("thead");
            for (i=0; i < cols; i++) {
                var th = doc.createElement("th");
                th.appendChild(doc.createTextNode("Col " + i+1));
                tr.appendChild(th);
            }
            thead.appendChild(tr);
            table.appendChild(thead);
        }

        tbody = doc.createElement("tbody");
        for (var i=0; i < rows; i++) {
            var tr = doc.createElement("tr");
            for (var j=0; j < cols; j++) {
                var td = doc.createElement("td");
                var content = doc.createTextNode('\u00a0');
                td.appendChild(content);
                tr.appendChild(td);
            }
            tbody.appendChild(tr);
        }
        table.appendChild(tbody);
        this.editor.insertNodeAtSelection(table);

        this._setTableCellHandlers(table);

        this.editor.logMessage(_('Table added'));
        this.editor.updateState();
        return table;
    };

    this._setTableCellHandlers = function(table) {
        // make each cell select its full contents if it's clicked
        addEventHandler(table, 'click', this._selectContentIfEmpty, this);

        var cells = table.getElementsByTagName('td');
        for (var i=0; i < cells.length; i++) {
            addEventHandler(cells[i], 'click', this._selectContentIfEmpty, this);
        };
        
        // select the nbsp in the first cell
        var firstcell = cells[0];
        if (firstcell) {
            var children = firstcell.childNodes;
            if (children.length == 1 && children[0].nodeType == 3 && 
                    children[0].nodeValue == '\xa0') {
                var selection = this.editor.getSelection();
                selection.selectNodeContents(firstcell);
            };
        };
    };
    
    this._selectContentIfEmpty = function() {
        var selNode = this.editor.getSelectedNode();
        var cell = this.editor.getNearestParentOfType(selNode, 'td');
        if (!cell) {
            return;
        };
        var children = cell.childNodes;
        if (children.length == 1 && children[0].nodeType == 3 && 
                children[0].nodeValue == '\xa0') {
            var selection = this.editor.getSelection();
            selection.selectNodeContents(cell);
        };
    };

    this.addTableRow = function() {
        /* Find the current row and add a row after it */
        var currnode = this.editor.getSelectedNode();
        var currtbody = this.editor.getNearestParentOfType(currnode, "TBODY");
        var bodytype = "tbody";
        if (!currtbody) {
            currtbody = this.editor.getNearestParentOfType(currnode, "THEAD");
            bodytype = "thead";
        }
        var parentrow = this.editor.getNearestParentOfType(currnode, "TR");
        var nextrow = parentrow.nextSibling;

        // get the number of cells we should place
        var colcount = 0;
        for (var i=0; i < currtbody.childNodes.length; i++) {
            var el = currtbody.childNodes[i];
            if (el.nodeType != 1) {
                continue;
            }
            if (el.nodeName.toLowerCase() == 'tr') {
                var cols = 0;
                for (var j=0; j < el.childNodes.length; j++) {
                    if (el.childNodes[j].nodeType == 1) {
                        cols++;
                    }
                }
                if (cols > colcount) {
                    colcount = cols;
                }
            }
        }

        var newrow = this.editor.getInnerDocument().createElement("TR");

        for (var i = 0; i < colcount; i++) {
            var newcell;
            if (bodytype == 'tbody') {
                newcell = this.editor.getInnerDocument().createElement("TD");
            } else {
                newcell = this.editor.getInnerDocument().createElement("TH");
            }
            var newcellvalue = this.editor.getInnerDocument().createTextNode("\u00a0");
            newcell.appendChild(newcellvalue);
            newrow.appendChild(newcell);
        }

        if (!nextrow) {
            currtbody.appendChild(newrow);
        } else {
            currtbody.insertBefore(newrow, nextrow);
        }
        
        this.editor.focusDocument();
        this.editor.logMessage(_('Table row added'));
    };

    this.delTableRow = function() {
        /* Find the current row and delete it */
        var currnode = this.editor.getSelectedNode();
        var parentrow = this.editor.getNearestParentOfType(currnode, "TR");
        if (!parentrow) {
            this.editor.logMessage(_('No row to delete'), 1);
            return;
        }

        // move selection aside
        // XXX: doesn't work if parentrow is the only row of thead/tbody/tfoot
        // XXX: doesn't preserve the colindex
        var selection = this.editor.getSelection();
        if (parentrow.nextSibling) {
            selection.selectNodeContents(parentrow.nextSibling.firstChild);
        } else if (parentrow.previousSibling) {
            selection.selectNodeContents(parentrow.previousSibling.firstChild);
        };

        // remove the row
        parentrow.parentNode.removeChild(parentrow);

        this.editor.focusDocument();
        this.editor.logMessage(_('Table row removed'));
    };

    this.addTableColumn = function() {
        /* Add a new column after the current column */
        var currnode = this.editor.getSelectedNode();
        var currtd = this.editor.getNearestParentOfType(currnode, 'TD');
        if (!currtd) {
            currtd = this.editor.getNearestParentOfType(currnode, 'TH');
        }
        if (!currtd) {
            this.editor.logMessage(_('No parentcolumn found!'), 1);
            return;
        }
        var currtr = this.editor.getNearestParentOfType(currnode, 'TR');
        var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');
        
        // get the current index
        var tdindex = this._getColIndex(currtd);
        // XXX this looks like a debug message, remove
        this.editor.logMessage(_('tdindex: ${tdindex}'));

        // now add a column to all rows
        // first the thead
        var theads = currtable.getElementsByTagName('THEAD');
        if (theads) {
            for (var i=0; i < theads.length; i++) {
                // let's assume table heads only have ths
                var currthead = theads[i];
                for (var j=0; j < currthead.childNodes.length; j++) {
                    var tr = currthead.childNodes[j];
                    if (tr.nodeType != 1) {
                        continue;
                    }
                    var currindex = 0;
                    for (var k=0; k < tr.childNodes.length; k++) {
                        var th = tr.childNodes[k];
                        if (th.nodeType != 1) {
                            continue;
                        }
                        if (currindex == tdindex) {
                            var doc = this.editor.getInnerDocument();
                            var newth = doc.createElement('th');
                            var text = doc.createTextNode('\u00a0');
                            newth.appendChild(text);
                            if (tr.childNodes.length == k+1) {
                                // the column will be on the end of the row
                                tr.appendChild(newth);
                            } else {
                                tr.insertBefore(newth, tr.childNodes[k + 1]);
                            }
                            break;
                        }
                        currindex++;
                    }
                }
            }
        }

        // then the tbody
        var tbodies = currtable.getElementsByTagName('TBODY');
        if (tbodies) {
            for (var i=0; i < tbodies.length; i++) {
                // let's assume table heads only have ths
                var currtbody = tbodies[i];
                for (var j=0; j < currtbody.childNodes.length; j++) {
                    var tr = currtbody.childNodes[j];
                    if (tr.nodeType != 1) {
                        continue;
                    }
                    var currindex = 0;
                    for (var k=0; k < tr.childNodes.length; k++) {
                        var td = tr.childNodes[k];
                        if (td.nodeType != 1) {
                            continue;
                        }
                        if (currindex == tdindex) {
                            var doc = this.editor.getInnerDocument();
                            var newtd = doc.createElement('td');
                            var text = doc.createTextNode('\u00a0');
                            newtd.appendChild(text);
                            if (tr.childNodes.length == k+1) {
                                // the column will be on the end of the row
                                tr.appendChild(newtd);
                            } else {
                                tr.insertBefore(newtd, tr.childNodes[k + 1]);
                            }
                            break;
                        }
                        currindex++;
                    }
                }
            }
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('Table column added'));
    };

    this.delTableColumn = function() {
        /* remove a column */
        var currnode = this.editor.getSelectedNode();
        var currtd = this.editor.getNearestParentOfType(currnode, 'TD');
        if (!currtd) {
            currtd = this.editor.getNearestParentOfType(currnode, 'TH');
        }
        var currcolindex = this._getColIndex(currtd);
        var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');

        // move selection aside
        var selection = this.editor.getSelection();
        if (currtd.nextSibling) {
            selection.selectNodeContents(currtd.nextSibling);
        } else if (currtd.previousSibling) {
            selection.selectNodeContents(currtd.previousSibling);
        };

        // remove the theaders
        var heads = currtable.getElementsByTagName('THEAD');
        if (heads.length) {
            for (var i=0; i < heads.length; i++) {
                var thead = heads[i];
                for (var j=0; j < thead.childNodes.length; j++) {
                    var tr = thead.childNodes[j];
                    if (tr.nodeType != 1) {
                        continue;
                    }
                    var currindex = 0;
                    for (var k=0; k < tr.childNodes.length; k++) {
                        var th = tr.childNodes[k];
                        if (th.nodeType != 1) {
                            continue;
                        }
                        if (currindex == currcolindex) {
                            tr.removeChild(th);
                            break;
                        }
                        currindex++;
                    }
                }
            }
        }

        // now we remove the column field, a bit harder since we need to take 
        // colspan and rowspan into account XXX Not right, fix theads as well
        var bodies = currtable.getElementsByTagName('TBODY');
        for (var i=0; i < bodies.length; i++) {
            var currtbody = bodies[i];
            var relevant_rowspan = 0;
            for (var j=0; j < currtbody.childNodes.length; j++) {
                var tr = currtbody.childNodes[j];
                if (tr.nodeType != 1) {
                    continue;
                }
                var currindex = 0
                for (var k=0; k < tr.childNodes.length; k++) {
                    var cell = tr.childNodes[k];
                    if (cell.nodeType != 1) {
                        continue;
                    }
                    var colspan = cell.colSpan;
                    if (currindex == currcolindex) {
                        tr.removeChild(cell);
                        break;
                    }
                    currindex++;
                }
            }
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('Table column deleted'));
    };

    this.delTable = function() {
        /* delete the current table */
        var currnode = this.editor.getSelectedNode();
        var table = this.editor.getNearestParentOfType(currnode, 'table');
        if (!table) {
            this.editor.logMessage(_('Not inside a table!'));
            return;
        };
        table.parentNode.removeChild(table);
        this.editor.logMessage(_('Table removed'));
    };

    this.setColumnAlign = function(newalign) {
        /* change the alignment of a full column */
        var currnode = this.editor.getSelectedNode();
        var currtd = this.editor.getNearestParentOfType(currnode, "TD");
        var bodytype = 'tbody';
        if (!currtd) {
            currtd = this.editor.getNearestParentOfType(currnode, "TH");
            bodytype = 'thead';
        }
        var currcolindex = this._getColIndex(currtd);
        var currtable = this.editor.getNearestParentOfType(currnode, "TABLE");

        // unfortunately this is not enough to make the browsers display
        // the align, we need to set it on individual cells as well and
        // mind the rowspan...
        for (var i=0; i < currtable.childNodes.length; i++) {
            var currtbody = currtable.childNodes[i];
            if (currtbody.nodeType != 1 || 
                    (currtbody.nodeName.toUpperCase() != "THEAD" &&
                        currtbody.nodeName.toUpperCase() != "TBODY")) {
                continue;
            }
            for (var j=0; j < currtbody.childNodes.length; j++) {
                var row = currtbody.childNodes[j];
                if (row.nodeType != 1) {
                    continue;
                }
                var index = 0;
                for (var k=0; k < row.childNodes.length; k++) {
                    var cell = row.childNodes[k];
                    if (cell.nodeType != 1) {
                        continue;
                    }
                    if (index == currcolindex) {
                        if (this.editor.config.use_css) {
                            cell.style.textAlign = newalign;
                        } else {
                            cell.setAttribute('align', newalign);
                        }
                        cell.className = 'align-' + newalign;
                    }
                    index++;
                }
            }
        }
    };

    this.setTableClass = function(sel_class) {
        /* set the class for the table */
        var currnode = this.editor.getSelectedNode();
        var currtable = this.editor.getNearestParentOfType(currnode, 'TABLE');

        if (currtable) {
            currtable.className = sel_class;
        }
    };

    this._getColIndex = function(currcell) {
        /* Given a node, return an integer for which column it is */
        var prevsib = currcell.previousSibling;
        var currcolindex = 0;
        while (prevsib) {
            if (prevsib.nodeType == 1 && 
                    (prevsib.tagName.toUpperCase() == "TD" || 
                        prevsib.tagName.toUpperCase() == "TH")) {
                var colspan = prevsib.colSpan;
                if (colspan) {
                    currcolindex += parseInt(colspan);
                } else {
                    currcolindex++;
                }
            }
            prevsib = prevsib.previousSibling;
            if (currcolindex > 30) {
                alert(_("Recursion detected when counting column position"));
                return;
            }
        }

        return currcolindex;
    };

    this._getColumnAlign = function(selNode) {
        /* return the alignment setting of the current column */
        var align;
        var td = this.editor.getNearestParentOfType(selNode, 'td');
        if (!td) {
            td = this.editor.getNearestParentOfType(selNode, 'th');
        };
        if (td) {
            align = td.getAttribute('align');
            if (this.editor.config.use_css) {
                align = td.style.textAlign;
            };
        };
        return align;
    };

    this.fixTable = function(event) {
        /* fix the table so it can be processed by Kupu */
        // since this can be quite a nasty creature we can't just use the
        // helper methods
        
        // first we create a new tbody element
        var currnode = this.editor.getSelectedNode();
        var table = this.editor.getNearestParentOfType(currnode, 'TABLE');
        if (!table) {
            this.editor.logMessage(_('Not inside a table!'));
            return;
        };
        this._fixTableHelper(table);
    };

    this._isBodyRow = function(row) {
        for (var node = row.firstChild; node; node=node.nextSibling) {
            if (/TD/.test(node.nodeName)) {
                return true;
            }
        }
        return false;
    }

    this._cleanCell = function(el) {
        dump('_cleanCell('+el.innerHTML+')\n');
        // Remove formatted div or p from a cell
        var node, nxt, n;
        for (node = el.firstChild; node;) {
            if (/DIV|P/.test(node.nodeName)) {
                for (var n = node.firstChild; n;) {
                    var nxt = n.nextSibling;
                    el.insertBefore(n, node); // Move nodes out of div
                    n = nxt;
                }
                nxt = node.nextSibling;
                el.removeChild(node);
                node = nxt;
            } else {
                node = node.nextSibling;
            }
        }
        var c;
        while (el.firstChild && (c = el.firstChild).nodeType==3 && /^\s+/.test(c.data)) {
            c.data = c.data.replace(/^\s+/, '');
            if (!c.data) {
                el.removeChild(c);
            } else {
                break;
            };
        };
        while (el.lastChild && (c = el.lastChild).nodeType==3 && /\s+$/.test(c.data)) {
            c.data = c.data.replace(/\s+$/, '');
            if (!c.data) {
                el.removeChild(c);
            } else {
                break;
            };
        };
        el.removeAttribute('colSpan');
        el.removeAttribute('rowSpan');
    }
    this._countCols = function(rows, numcols) {
        for (var i=0; i < rows.length; i++) {
            var row = rows[i];
            var currnumcols = 0;
            for (var node = row.firstChild; node; node=node.nextSibling) {
                if (/td|th/i.test(node.nodeName)) {
                    currnumcols += parseInt(node.getAttribute('colSpan') || '1');
                };
            };
            if (currnumcols > numcols) {
                numcols = currnumcols;
            };
        };
        return numcols;
    }

    this._cleanRows = function(rows, container, numcols) {
        // now walk through all rows to clean them up
        for (var i=0; i < rows.length; i++) {
            dump("row "+i+'\n');
            var row = rows[i];
            var doc = this.editor.getInnerDocument();
            var newrow = doc.createElement('tr');
            if (row.className) {
                newrow.className = row.className;
            }
            for (var node = row.firstChild; node;) {
                dump("child\n");
                var nxt = node.nextSibling;
                if (/TD|TH/.test(node.nodeName)) {
                    this._cleanCell(node);
                    newrow.appendChild(node);
                };
                node = nxt;
            };
            if (newrow.childNodes.length) {
                container.appendChild(newrow);
            };
        };
        // now make sure all rows have the correct length
        for (row = container.firstChild; row; row=row.nextSibling) {
            var cellname = row.lastChild.nodeName;
            while (row.childNodes.length < numcols) {
                var cell = doc.createElement(cellname);
                var nbsp = doc.createTextNode('\u00a0');
                cell.appendChild(nbsp);
                row.appendChild(cell);
            };
        };
    };

    this._fixTableHelper = function(table) {
        /* the code to actually fix tables */
        var doc = this.editor.getInnerDocument();
        var thead = doc.createElement('thead');
        var tbody = doc.createElement('tbody');
        var tfoot = doc.createElement('tfoot');

        var table_classes = this.editor.config.table_classes;
        function cleanClassName(name) {
            var allowed_classes = table_classes['class'];
            for (var i = 0; i < allowed_classes.length; i++) {
                var classname = allowed_classes[i];
                classname = classname.classname || classname;
                if (classname==name) return name;
            };
            return allowed_classes[0];
        }
        if (table_classes) {
            table.className = cleanClassName(table.className);
        } else {
            table.removeAttribute('class');
            table.removeAttribute('className');
        };
        table.removeAttribute('border');
        table.removeAttribute('cellpadding');
        table.removeAttribute('cellPadding');
        table.removeAttribute('cellspacing');
        table.removeAttribute('cellSpacing');

        // now get all the rows of the table, the rows can either be
        // direct descendants of the table or inside a 'tbody', 'thead'
        // or 'tfoot' element

        var hrows = [], brows = [], frows = [];
        for (var node = table.firstChild; node; node = node.nextSibling) {
            var nodeName = node.nodeName;
            if (/TR/.test(node.nodeName)) {
                brows.push(node);
            } else if (/THEAD|TBODY|TFOOT/.test(node.nodeName)) {
                var rows = nodeName=='THEAD' ? hrows : nodeName=='TFOOT' ? frows : brows;
                for (var inode = node.firstChild; inode; inode = inode.nextSibling) {
                    if (/TR/.test(inode.nodeName)) {
                        rows.push(inode);
                    };
                };
            };
        };
        /* Extract thead and tfoot from tbody */
        dump('extract head and foot\n');
        while (brows.length && !this._isBodyRow(brows[0])) {
            hrows.push(brows[0]);
            brows.shift();
        }
        while (brows.length && !this._isBodyRow(brows[brows.length-1])) {
            var last = brows[brows.length-1];
            brows.length -= 1;
            frows.unshift(last);
        }
        dump('count cols\n');
        // now find out how many cells our rows should have
        var numcols = this._countCols(hrows, 0);
        numcols = this._countCols(brows, numcols);
        numcols = this._countCols(frows, numcols);

        dump('clean rows\n');
        // now walk through all rows to clean them up
        this._cleanRows(hrows, thead);
        this._cleanRows(brows, tbody);
        this._cleanRows(frows, tfoot);

        // now remove all the old stuff from the table and add the new
        // tbody
        dump('remove old\n');
        while (table.firstChild) {
            table.removeChild(table.firstChild);
        }
        if (hrows.length)
            table.appendChild(thead);
        if (brows.length)
            table.appendChild(tbody);
        if (frows.length)
            table.appendChild(tfoot);
        dump('finish up\n');

        this.editor.focusDocument();
        this.editor.logMessage(_('Table cleaned up'));
    };

    this.fixAllTables = function() {
        /* fix all the tables in the document at once */
        var tables = this.editor.getInnerDocument().getElementsByTagName('table');
        for (var i=0; i < tables.length; i++) {
            this._fixTableHelper(tables[i]);
        };
    };
};

TableTool.prototype = new KupuTool;

function TableToolBox(addtabledivid, edittabledivid, newrowsinputid, 
                    newcolsinputid, makeheaderinputid, classselectid, alignselectid, addtablebuttonid,
                    addrowbuttonid, delrowbuttonid, addcolbuttonid, delcolbuttonid, fixbuttonid,
                    fixallbuttonid, toolboxid, plainclass, activeclass) {
    /* The table tool */

    // XXX There are some awfully long methods in here!!
    

    // a lot of dependencies on html elements here, but most implementations
    // will use them all I guess
    this.addtablediv = getFromSelector(addtabledivid);
    this.edittablediv = getFromSelector(edittabledivid);
    this.newrowsinput = getFromSelector(newrowsinputid);
    this.newcolsinput = getFromSelector(newcolsinputid);
    this.makeheaderinput = getFromSelector(makeheaderinputid);
    this.classselect = getFromSelector(classselectid);
    this.alignselect = getFromSelector(alignselectid);
    this.addtablebutton = getFromSelector(addtablebuttonid);
    this.addrowbutton = getFromSelector(addrowbuttonid);
    this.delrowbutton = getFromSelector(delrowbuttonid);
    this.addcolbutton = getFromSelector(addcolbuttonid);
    this.delcolbutton = getFromSelector(delcolbuttonid);
    this.fixbutton = getFromSelector(fixbuttonid);
    this.fixallbutton = getFromSelector(fixallbuttonid);
    this.toolboxel = getFromSelector(toolboxid);
    this.plainclass = plainclass;
    this.activeclass = activeclass;

    // register event handlers
    this.initialize = function(tool, editor) {
        /* attach the event handlers */
        this.tool = tool;
        this.editor = editor;
        // build the select list of table classes if configured
        if (this.editor.config.table_classes) {
            var classes = this.editor.config.table_classes['class'];
            while (this.classselect.hasChildNodes()) {
                this.classselect.removeChild(this.classselect.firstChild);
            };
            for (var i=0; i < classes.length; i++) {
                var classname = classes[i];
                classname = classname.classname || classname;
                var option = document.createElement('option');
                var content = document.createTextNode(classname);
                option.appendChild(content);
                option.setAttribute('value', classname);
                this.classselect.appendChild(option);
            };
        };
        addEventHandler(this.addtablebutton, "click", this.addTable, this);
        addEventHandler(this.addrowbutton, "click", this.tool.addTableRow, this.tool);
        addEventHandler(this.delrowbutton, "click", this.tool.delTableRow, this.tool);
        addEventHandler(this.addcolbutton, "click", this.tool.addTableColumn, this.tool);
        addEventHandler(this.delcolbutton, "click", this.tool.delTableColumn, this.tool);
        addEventHandler(this.alignselect, "change", this.setColumnAlign, this);
        addEventHandler(this.classselect, "change", this.setTableClass, this);
        addEventHandler(this.fixbutton, "click", this.tool.fixTable, this.tool);
        addEventHandler(this.fixallbutton, "click", this.tool.fixAllTables, this.tool);
        this.addtablediv.style.display = "block";
        this.edittablediv.style.display = "none";
        this.editor.logMessage(_('Table tool initialized'));
    };

    this.updateState = function(selNode) {
        /* update the state (add/edit) and update the pulldowns (if required) */
        var table = this.editor.getNearestParentOfType(selNode, 'table');
        if (table) {
            this.addtablediv.style.display = "none";
            this.edittablediv.style.display = "block";

            var align = this.tool._getColumnAlign(selNode);
            selectSelectItem(this.alignselect, align);
            selectSelectItem(this.classselect, table.className);
            if (this.toolboxel) {
                this.toolboxel.className = this.activeclass;
            };
        } else {
            this.edittablediv.style.display = "none";
            this.addtablediv.style.display = "block";
            this.alignselect.selectedIndex = 0;
            this.classselect.selectedIndex = 0;
            if (this.toolboxel) {
                this.toolboxel.className = this.plainclass;
            };
        };
    };

    this.addTable = function() {
        /* add a table */
        var rows = this.newrowsinput.value;
        var cols = this.newcolsinput.value;
        var makeHeader = this.makeheaderinput.checked;
        // XXX getFromSelector
        var classchooser = getFromSelector("kupu-table-classchooser-add");
        var tableclass = this.classselect.options[this.classselect.selectedIndex].value;
        
        this.tool.createTable(rows, cols, makeHeader, tableclass);
    };

    this.setColumnAlign = function() {
        /* set the alignment of the current column */
        var newalign = this.alignselect.options[this.alignselect.selectedIndex].value;
        this.tool.setColumnAlign(newalign);
    };

    this.setTableClass = function() {
        /* set the class for the current table */
        var sel_class = this.classselect.options[this.classselect.selectedIndex].value;
        if (sel_class) {
            this.tool.setTableClass(sel_class);
        };
    };
};

TableToolBox.prototype = new KupuToolBox;

function ListTool(addulbuttonid, addolbuttonid, ulstyleselectid, olstyleselectid) {
    /* tool to set list styles */

    this.addulbutton = getFromSelector(addulbuttonid);
    this.addolbutton = getFromSelector(addolbuttonid);
    this.ulselect = getFromSelector(ulstyleselectid);
    this.olselect = getFromSelector(olstyleselectid);

    this.style_to_type = {'decimal': '1',
                            'lower-alpha': 'a',
                            'upper-alpha': 'A',
                            'lower-roman': 'i',
                            'upper-roman': 'I',
                            'disc': 'disc',
                            'square': 'square',
                            'circle': 'circle',
                            'none': 'none'
                            };
    this.type_to_style = {'1': 'decimal',
                            'a': 'lower-alpha',
                            'A': 'upper-alpha',
                            'i': 'lower-roman',
                            'I': 'upper-roman',
                            'disc': 'disc',
                            'square': 'square',
                            'circle': 'circle',
                            'none': 'none'
                            };
    
    this.initialize = function(editor) {
        /* attach event handlers */
        this.editor = editor;
        this._fixTabIndex(this.addulbutton);
        this._fixTabIndex(this.addolbutton);
        this._fixTabIndex(this.ulselect);
        this._fixTabIndex(this.olselect);

        addEventHandler(this.addulbutton, "click", this.addUnorderedList, this);
        addEventHandler(this.addolbutton, "click", this.addOrderedList, this);
        addEventHandler(this.ulselect, "change", this.setUnorderedListStyle, this);
        addEventHandler(this.olselect, "change", this.setOrderedListStyle, this);
        this.ulselect.style.display = "none";
        this.olselect.style.display = "none";

        this.editor.logMessage(_('List style tool initialized'));
    };

    this._handleStyles = function(currnode, onselect, offselect) {
        if (this.editor.config.use_css) {
            var currstyle = currnode.style.listStyleType;
        } else {
            var currstyle = this.type_to_style[currnode.getAttribute('type')];
        }
        selectSelectItem(onselect, currstyle);
        offselect.style.display = "none";
        onselect.style.display = "inline";
        offselect.selectedIndex = 0;
    };

    this.updateState = function(selNode) {
        /* update the visibility and selection of the list type pulldowns */
        // we're going to walk through the tree manually since we want to 
        // check on 2 items at the same time
        for (var currnode=selNode; currnode; currnode=currnode.parentNode) {
            var tag = currnode.nodeName.toLowerCase();
            if (tag == 'ul') {
                this._handleStyles(currnode, this.ulselect, this.olselect);
                return;
            } else if (tag == 'ol') {
                this._handleStyles(currnode, this.olselect, this.ulselect);
                return;
            }
        }
        with(this.ulselect) {
            selectedIndex = 0;
            style.display = "none";
        };
        with(this.olselect) {
            selectedIndex = 0;
            style.display = "none";
        };
    };

    this.addList = function(command) {
        this.ulselect.style.display = "inline";
        this.olselect.style.display = "none";
        this.editor.execCommand(command);
        this.editor.focusDocument();
    };
    this.addUnorderedList = function() {
        /* add an unordered list */
        this.addList("insertunorderedlist");
    };

    this.addOrderedList = function() {
        /* add an ordered list */
        this.addList("insertorderedlist");
    };

    this.setListStyle = function(tag, select) {
        /* set the type of an ul */
        var currnode = this.editor.getSelectedNode();
        var l = this.editor.getNearestParentOfType(currnode, tag);
        var style = select.options[select.selectedIndex].value;
        if (this.editor.config.use_css) {
            l.style.listStyleType = style;
        } else {
            l.setAttribute('type', this.style_to_type[style]);
        }
        this.editor.focusDocument();
        this.editor.logMessage(_('List style changed'));
    };

    this.setUnorderedListStyle = function() {
        /* set the type of an ul */
        this.setListStyle('ul', this.ulselect);
    };

    this.setOrderedListStyle = function() {
        /* set the type of an ol */
        this.setListStyle('ol', this.olselect);
    };

    this.enable = function() {
        KupuButtonEnable(this.addulbutton);
        KupuButtonEnable(this.addolbutton);
        this.ulselect.disabled = "";
        this.olselect.disabled = "";
    }
    this.disable = function() {
        KupuButtonDisable(this.addulbutton);
        KupuButtonDisable(this.addolbutton);
        this.ulselect.disabled = "disabled";
        this.olselect.disabled = "disabled";
    }
};

ListTool.prototype = new KupuTool;

function ShowPathTool() {
    /* shows the path to the current element in the status bar */

    this.updateState = function(selNode) {
        /* calculate and display the path */
        var path = '';
        var url = null; // for links we want to display the url too
        var currnode = selNode;
        while (currnode != null && currnode.nodeName != '#document') {
            if (currnode.nodeName.toLowerCase() == 'a') {
                url = currnode.getAttribute('href');
            };
            path = '/' + currnode.nodeName.toLowerCase() + path;
            currnode = currnode.parentNode;
        }
        
        try {
            window.status = url ? 
                    (path.toString() + ' - contains link to \'' + 
                        url.toString() + '\'') :
                    path;
        } catch (e) {
            this.editor.logMessage(_('Could not set status bar message, ' +
                                    'check your browser\'s security settings.'
                                    ), 1);
        };
    };
};

ShowPathTool.prototype = new KupuTool;

function ViewSourceTool() {
    /* tool to provide a 'show source' context menu option */
    this.sourceWindow = null;
    
    this.viewSource = function() {
        /* open a window and write the current contents of the iframe to it */
        if (this.sourceWindow) {
            this.sourceWindow.close();
        };
        this.sourceWindow = window.open('#', 'sourceWindow');
        
        //var transform = this.editor._filterContent(this.editor.getInnerDocument().documentElement);
        //var contents = transform.xml; 
        var contents = '<html>\n' + this.editor.getInnerDocument().documentElement.innerHTML + '\n</html>';
        
        var doc = this.sourceWindow.document;
        doc.write('\xa0');
        doc.close();
        var body = doc.getElementsByTagName("body")[0];
        while (body.hasChildNodes()) {
            body.removeChild(body.firstChild);
        };
        var pre = doc.createElement('pre');
        var textNode = doc.createTextNode(contents);
        body.appendChild(pre);
        pre.appendChild(textNode);
    };
    
    this.createContextMenuElements = function(selNode, event) {
        /* create the context menu element */
        return new Array(new ContextMenuElement(_('View source'), this.viewSource, this));
    };
};

ViewSourceTool.prototype = new KupuTool;

function DefinitionListTool(dlbuttonid) {
    /* a tool for managing definition lists

        the dl elements should behave much like plain lists, and the keypress
        behaviour should be similar
    */

    this.dlbutton = getFromSelector(dlbuttonid);
    
    this.initialize = function(editor) {
        /* initialize the tool */
        this.editor = editor;
        this._fixTabIndex(this.dlbutton);
        addEventHandler(this.dlbutton, 'click', this.createDefinitionList, this);
        addEventHandler(editor.getInnerDocument(), 'keyup', this._keyDownHandler, this);
        addEventHandler(editor.getInnerDocument(), 'keypress', this._keyPressHandler, this);
    };

    // even though the following methods may seem view related, they belong 
    // here, since they describe core functionality rather then view-specific
    // stuff
    this.handleEnterPress = function(selNode) {
        var dl = this.editor.getNearestParentOfType(selNode, 'dl');
        if (dl) {
            var dt = this.editor.getNearestParentOfType(selNode, 'dt');
            if (dt) {
                if (dt.childNodes.length == 1 && dt.childNodes[0].nodeValue == '\xa0') {
                    this.escapeFromDefinitionList(dl, dt, selNode);
                    return;
                };

                var selection = this.editor.getSelection();
                var startoffset = selection.startOffset();
                var endoffset = selection.endOffset(); 
                if (endoffset > startoffset) {
                    // throw away any selected stuff
                    selection.cutChunk(startoffset, endoffset);
                    selection = this.editor.getSelection();
                    startoffset = selection.startOffset();
                };
                
                var ellength = selection.getElementLength(selection.parentElement());
                if (startoffset >= ellength - 1) {
                    // create a new element
                    this.createDefinition(dl, dt);
                } else {
                    var doc = this.editor.getInnerDocument();
                    var newdt = selection.splitNodeAtSelection(dt);
                    var newdd = doc.createElement('dd');
                    while (newdt.hasChildNodes()) {
                        if (newdt.firstChild != newdt.lastChild || newdt.firstChild.nodeName.toLowerCase() != 'br') {
                            newdd.appendChild(newdt.firstChild);
                        };
                    };
                    newdt.parentNode.replaceChild(newdd, newdt);
                    selection.selectNodeContents(newdd);
                    selection.collapse();
                };
            } else {
                var dd = this.editor.getNearestParentOfType(selNode, 'dd');
                if (!dd) {
                    this.editor.logMessage(_('Not inside a definition list element!'));
                    return;
                };
                if (dd.childNodes.length == 1 && dd.childNodes[0].nodeValue == '\xa0') {
                    this.escapeFromDefinitionList(dl, dd, selNode);
                    return;
                };
                var selection = this.editor.getSelection();
                var startoffset = selection.startOffset();
                var endoffset = selection.endOffset();
                if (endoffset > startoffset) {
                    // throw away any selected stuff
                    selection.cutChunk(startoffset, endoffset);
                    selection = this.editor.getSelection();
                    startoffset = selection.startOffset();
                };
                var ellength = selection.getElementLength(selection.parentElement());
                if (startoffset >= ellength - 1) {
                    // create a new element
                    this.createDefinitionTerm(dl, dd);
                } else {
                    // add a break and continue in this element
                    var br = this.editor.getInnerDocument().createElement('br');
                    this.editor.insertNodeAtSelection(br, 1);
                    //var selection = this.editor.getSelection();
                    //selection.moveStart(1);
                    selection.collapse(true);
                };
            };
        };
    };

    this.handleTabPress = function(selNode) {
    };

    this._keyDownHandler = function(event) {
        var selNode = this.editor.getSelectedNode();
        var dl = this.editor.getNearestParentOfType(selNode, 'dl');
        if (!dl) {
            return;
        };
        switch (event.keyCode) {
            case 13:
                if (event.preventDefault) {
                    event.preventDefault();
                } else {
                    event.returnValue = false;
                };
                break;
        };
    };

    this._keyPressHandler = function(event) {
        var selNode = this.editor.getSelectedNode();
        var dl = this.editor.getNearestParentOfType(selNode, 'dl');
        if (!dl) {
            return;
        };
        switch (event.keyCode) {
            case 13:
                this.handleEnterPress(selNode);
                if (event.preventDefault) {
                    event.preventDefault();
                } else {
                    event.returnValue = false;
                };
                break;
            case 9:
                if (event.preventDefault) {
                    event.preventDefault();
                } else {
                    event.returnValue = false;
                };
                this.handleTabPress(selNode);
        };
    };

    this.createDefinitionList = function() {
        /* create a new definition list (dl) */
        var selection = this.editor.getSelection();
        var doc = this.editor.getInnerDocument();

        var selection = this.editor.getSelection();
        var cloned = selection.cloneContents();
        // first get the 'first line' (until the first break) and use it
        // as the dt's content
        var iterator = new NodeIterator(cloned);
        var currnode = null;
        var remove = false;
        while (currnode = iterator.next()) {
            if (currnode.nodeName.toLowerCase() == 'br') {
                remove = true;
            };
            if (remove) {
                var next = currnode;
                while (!next.nextSibling) {
                    next = next.parentNode;
                };
                next = next.nextSibling;
                iterator.setCurrent(next);
                currnode.parentNode.removeChild(currnode);
            };
        };

        var dtcontentcontainer = cloned;
        var collapsetoend = false;
        
        var dl = doc.createElement('dl');
        this.editor.insertNodeAtSelection(dl);
        var dt = this.createDefinitionTerm(dl);
        if (dtcontentcontainer.hasChildNodes()) {
            collapsetoend = true;
            while (dt.hasChildNodes()) {
                dt.removeChild(dt.firstChild);
            };
            while (dtcontentcontainer.hasChildNodes()) {
                dt.appendChild(dtcontentcontainer.firstChild);
            };
        };

        var selection = this.editor.getSelection();
        selection.selectNodeContents(dt);
        selection.collapse(collapsetoend);
    };

    this.createDefinitionTerm = function(dl, dd) {
        /* create a new definition term inside the current dl */
        var doc = this.editor.getInnerDocument();
        var dt = doc.createElement('dt');
        // somehow Mozilla seems to add breaks to all elements...
        if (dd) {
            if (dd.lastChild.nodeName.toLowerCase() == 'br') {
                dd.removeChild(dd.lastChild);
            };
        };
        // dd may be null here, if so we assume this is the first element in 
        // the dl
        if (!dd || dl == dd.lastChild) {
            dl.appendChild(dt);
        } else {
            var nextsibling = dd.nextSibling;
            if (nextsibling) {
                dl.insertBefore(dt, nextsibling);
            } else {
                dl.appendChild(dt);
            };
        };
        var nbsp = doc.createTextNode('\xa0');
        dt.appendChild(nbsp);
        var selection = this.editor.getSelection();
        selection.selectNodeContents(dt);
        selection.collapse();

        this.editor.focusDocument();
        return dt;
    };

    this.createDefinition = function(dl, dt, initial_content) {
        var doc = this.editor.getInnerDocument();
        var dd = doc.createElement('dd');
        var nextsibling = dt.nextSibling;
        // somehow Mozilla seems to add breaks to all elements...
        if (dt) {
            if (dt.lastChild.nodeName.toLowerCase() == 'br') {
                dt.removeChild(dt.lastChild);
            };
        };
        while (nextsibling) {
            var name = nextsibling.nodeName.toLowerCase();
            if (name == 'dd' || name == 'dt') {
                break;
            } else {
                nextsibling = nextsibling.nextSibling;
            };
        };
        if (nextsibling) {
            dl.insertBefore(dd, nextsibling);
            //this._fixStructure(doc, dl, nextsibling);
        } else {
            dl.appendChild(dd);
        };
        if (initial_content) {
            for (var i=0; i < initial_content.length; i++) {
                dd.appendChild(initial_content[i]);
            };
        };
        var nbsp = doc.createTextNode('\xa0');
        dd.appendChild(nbsp);
        var selection = this.editor.getSelection();
        selection.selectNodeContents(dd);
        selection.collapse();
    };

    this.escapeFromDefinitionList = function(dl, currel, selNode) {
        var doc = this.editor.getInnerDocument();
        var p = doc.createElement('p');
        var nbsp = doc.createTextNode('\xa0');
        p.appendChild(nbsp);

        if (dl.lastChild == currel) {
            dl.parentNode.insertBefore(p, dl.nextSibling);
        } else {
            for (var i=0; i < dl.childNodes.length; i++) {
                var child = dl.childNodes[i];
                if (child == currel) {
                    var newdl = this.editor.getInnerDocument().createElement('dl');
                    while (currel.nextSibling) {
                        newdl.appendChild(currel.nextSibling);
                    };
                    dl.parentNode.insertBefore(newdl, dl.nextSibling);
                    dl.parentNode.insertBefore(p, dl.nextSibling);
                };
            };
        };
        currel.parentNode.removeChild(currel);
        var selection = this.editor.getSelection();
        selection.selectNodeContents(p);
        selection.collapse();
        this.editor.focusDocument();
    };

    this._fixStructure = function(doc, dl, offsetnode) {
        /* makes sure the order of the elements is correct */
        var currname = offsetnode.nodeName.toLowerCase();
        var currnode = offsetnode.nextSibling;
        while (currnode) {
            if (currnode.nodeType == 1) {
                var nodename = currnode.nodeName.toLowerCase();
                if (currname == 'dt' && nodename == 'dt') {
                    var dd = doc.createElement('dd');
                    while (currnode.hasChildNodes()) {
                        dd.appendChild(currnode.childNodes[0]);
                    };
                    currnode.parentNode.replaceChild(dd, currnode);
                } else if (currname == 'dd' && nodename == 'dd') {
                    var dt = doc.createElement('dt');
                    while (currnode.hasChildNodes()) {
                        dt.appendChild(currnode.childNodes[0]);
                    };
                    currnode.parentNode.replaceChild(dt, currnode);
                };
            };
            currnode = currnode.nextSibling;
        };
    };
};

DefinitionListTool.prototype = new KupuTool;

function KupuZoomTool(buttonid, firsttab, lasttab) {
    this.button = getFromSelector(buttonid);
    firsttab = firsttab || 'kupu-tb-styles';
    lasttab = lasttab || 'kupu-logo-button';

    this.initialize = function(editor) {
        this.offclass = 'kupu-zoom';
        this.onclass = 'kupu-zoom-pressed';
        this.pressed = false;

        this.baseinitialize(editor);
        this.button.tabIndex = this.editor.document.editable.tabIndex;
        addEventHandler(window, "resize", this.onresize, this);
        addEventHandler(window, "scroll", this.onscroll, this);

        /* Toolbar tabbing */
        var lastbutton = getFromSelector(lasttab);
        var firstbutton = getFromSelector(firsttab);
        var iframe = editor.getInnerDocument();
        this.setTabbing(iframe, firstbutton, lastbutton);
        this.setTabbing(firstbutton, null, editor.getDocument().getWindow());

        this.editor.logMessage(_('Zoom tool initialized'));
    };
};

KupuZoomTool.prototype = new KupuLateFocusStateButton;
KupuZoomTool.prototype.baseinitialize = KupuZoomTool.prototype.initialize;

KupuZoomTool.prototype.onscroll = function() {
    if (!this.zoomed) return;
    /* XXX Problem here: Mozilla doesn't generate onscroll when window is
     * scrolled by focus move or selection. */
    var top = window.pageYOffset!=undefined ? window.pageYOffset : document.documentElement.scrollTop;
    var left = window.pageXOffset!=undefined ? window.pageXOffset : document.documentElement.scrollLeft;
    if (top || left) window.scrollTo(0, 0);
}

// Handle tab pressed from a control.
KupuZoomTool.prototype.setTabbing = function(control, forward, backward) {
    function TabDown(event) {
        if (event.keyCode != 9 || !this.zoomed) return;

        var target = event.shiftKey ? backward : forward;
        if (!target) return;

        if (event.stopPropogation) event.stopPropogation();
        event.cancelBubble = true;
        event.returnValue = false;

        target.focus();
        return false;
    }
    addEventHandler(control, "keydown", TabDown, this);
}

KupuZoomTool.prototype.onresize = function() {
    if (!this.zoomed) return;

    var editor = this.editor;
    var iframe = editor.getDocument().editable;
    var sourcetool = editor.getTool('sourceedittool');
    var sourceArea = sourcetool?sourcetool.getSourceArea():null;

    var fulleditor = iframe.parentNode;
    var body = document.body;

    if (window.innerWidth) {
        var width = window.innerWidth;
        var height = window.innerHeight;
    } else if (document.documentElement) {
        var width = document.documentElement.offsetWidth-5;
        var height = document.documentElement.offsetHeight-5;
    } else {
        var width = document.body.offsetWidth-5;
        var height = document.body.offsetHeight-5;
    }
    width = width + 'px';
    var offset = iframe.offsetTop;
    if (sourceArea && sourceArea.offsetTop) offset = sourceArea.offsetTop-1;
    var nheight = Math.max(height - offset -1/*top border*/, 10);
    nheight = nheight + 'px';
    fulleditor.style.width = width; /*IE needs this*/
    iframe.style.width = width;
    iframe.style.height = nheight;
    if (sourceArea) {
        sourceArea.style.width = width;
        sourceArea.style.height = nheight;
    }
}

KupuZoomTool.prototype.checkfunc = function(selNode, button, editor, event) {
    return this.zoomed;
}

KupuZoomTool.prototype.commandfunc = function(button, editor) {
    /* Toggle zoom state */
    var zoom = button.pressed;
    this.zoomed = zoom;

    var zoomClass = 'kupu-fulleditor-zoomed';
    var iframe = editor.getDocument().getEditable();

    var body = document.body;
    var html = document.getElementsByTagName('html')[0];
    if (zoom) {
        html.style.overflow = 'hidden';
        window.scrollTo(0, 0);
        editor.setClass(zoomClass);
        body.className += ' '+zoomClass;
        this.onresize();
    } else {
        html.style.overflow = '';
        var fulleditor = iframe.parentNode;
        fulleditor.style.width = '';
        body.className = body.className.replace(/ *kupu-fulleditor-zoomed/, '');
        editor.clearClass(zoomClass);

        iframe.style.width = '';
        iframe.style.height = '';

        var sourcetool = editor.getTool('sourceedittool');
        var sourceArea = sourcetool?sourcetool.getSourceArea():null;
        if (sourceArea) {
            sourceArea.style.width = '';
            sourceArea.style.height = '';
        };
    }
    var doc = editor.getInnerDocument();
    // Mozilla needs this. Yes, really!
    doc.designMode=doc.designMode;

    window.scrollTo(0, iframe.offsetTop);
    editor.focusDocument();
}

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupuloggers.js 9879 2005-03-18 12:04:00Z yuppie $


//----------------------------------------------------------------------------
// Loggers
//
//  Loggers are pretty simple classes, that should have 1 method, called 
//  'log'. This is called with 2 arguments, the first one is the message to
//  log and the second is the severity, which can be 0 or some other false
//  value for debug messages, 1 for warnings and 2 for errors (the loggers
//  are allowed to raise an exception if that happens).
//
//----------------------------------------------------------------------------

function DebugLogger() {
    /* Alert all messages */
    
    this.log = function(message, severity) {
        /* log a message */
        if (severity > 1) {
            alert("Error: " + message);
        } else if (severity == 1) {
            alert("Warning: " + message);
        } else {
            alert("Log message: " + message);
        }
    };
}

function PlainLogger(debugelid, maxlength) {
    /* writes messages to a debug tool and throws errors */

    this.debugel = getFromSelector(debugelid);
    this.maxlength = maxlength;
    
    this.log = function(message, severity) {
        /* log a message */
        if (severity > 1) {
            throw message;
        } else {
            if (this.maxlength) {
                if (this.debugel.childNodes.length > this.maxlength - 1) {
                    this.debugel.removeChild(this.debugel.childNodes[0]);
                }
            }
            var now = new Date();
            var time = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
            
            var div = document.createElement('div');
            var text = document.createTextNode(time + ' - ' + message);
            div.appendChild(text);
            this.debugel.appendChild(div);
        }
    };
}

function DummyLogger() {
    this.log = function(message, severity) {
        if (severity > 1) {
            throw message;
        }
    };
};

/*****************************************************************************
 *
 * Copyright (c) 2003-2004 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupucontentfilters.js 27713 2006-05-26 10:32:29Z duncan $


//----------------------------------------------------------------------------
// 
// ContentFilters
//
//  These are (or currently 'this is') filters for HTML cleanup and 
//  conversion. Kupu filters should be classes that should get registered to
//  the editor using the registerFilter method with 2 methods: 'initialize'
//  and 'filter'. The first will be called with the editor as its only
//  argument and the latter with a reference to the ownerdoc (always use 
//  that to create new nodes and such) and the root node of the HTML DOM as 
//  its arguments.
//
//----------------------------------------------------------------------------

function NonXHTMLTagFilter() {
    /* filter out non-XHTML tags*/
    
    // A mapping from element name to whether it should be left out of the 
    // document entirely. If you want an element to reappear in the resulting 
    // document *including* it's contents, add it to the mapping with a 1 value.
    // If you want an element not to appear but want to leave it's contents in 
    // tact, add it to the mapping with a 0 value. If you want an element and
    // it's contents to be removed from the document, don't add it.
    if (arguments.length) {
        // allow an optional filterdata argument
        this.filterdata = arguments[0];
    } else {
        // provide a default filterdata dict
        this.filterdata = {'html': 1,
                            'body': 1,
                            'head': 1,
                            'title': 1,
                            
                            'a': 1,
                            'abbr': 1,
                            'acronym': 1,
                            'address': 1,
                            'b': 1,
                            'base': 1,
                            'blockquote': 1,
                            'br': 1,
                            'caption': 1,
                            'cite': 1,
                            'code': 1,
                            'col': 1,
                            'colgroup': 1,
                            'dd': 1,
                            'dfn': 1,
                            'div': 1,
                            'dl': 1,
                            'dt': 1,
                            'em': 1,
                            'h1': 1,
                            'h2': 1,
                            'h3': 1,
                            'h4': 1,
                            'h5': 1,
                            'h6': 1,
                            'h7': 1,
                            'i': 1,
                            'img': 1,
                            'kbd': 1,
                            'li': 1,
                            'link': 1,
                            'meta': 1,
                            'ol': 1,
                            'p': 1,
                            'pre': 1,
                            'q': 1,
                            'samp': 1,
                            'script': 1,
                            'span': 1,
                            'strong': 1,
                            'style': 1,
                            'sub': 1,
                            'sup': 1,
                            'table': 1,
                            'tbody': 1,
                            'td': 1,
                            'tfoot': 1,
                            'th': 1,
                            'thead': 1,
                            'tr': 1,
                            'ul': 1,
                            'u': 1,
                            'var': 1,

                            // even though they're deprecated we should leave
                            // font tags as they are, since Kupu sometimes
                            // produces them itself.
                            'font': 1,
                            'center': 0
                            };
    };
                        
    this.initialize = function(editor) {
        /* init */
        this.editor = editor;
    };

    this.filter = function(ownerdoc, htmlnode) {
        return this._filterHelper(ownerdoc, htmlnode);
    };

    this._filterHelper = function(ownerdoc, node) {
        /* filter unwanted elements */
        if (node.nodeType == 3) {
            return ownerdoc.createTextNode(node.nodeValue);
        } else if (node.nodeType == 4) {
            return ownerdoc.createCDATASection(node.nodeValue);
        };
        // create a new node to place the result into
        // XXX this can be severely optimized by doing stuff inline rather 
        // than on creating new elements all the time!
        var newnode = ownerdoc.createElement(node.nodeName);
        // copy the attributes
        for (var i=0; i < node.attributes.length; i++) {
            var attr = node.attributes[i];
            newnode.setAttribute(attr.nodeName, attr.nodeValue);
        };
        for (var i=0; i < node.childNodes.length; i++) {
            var child = node.childNodes[i];
            var nodeType = child.nodeType;
            var nodeName = child.nodeName.toLowerCase();
            if (nodeType == 3 || nodeType == 4) {
                newnode.appendChild(this._filterHelper(ownerdoc, child));
            };
            if (nodeName in this.filterdata && this.filterdata[nodeName]) {
                newnode.appendChild(this._filterHelper(ownerdoc, child));
            } else if (nodeName in this.filterdata) {
                for (var j=0; j < child.childNodes.length; j++) {
                    newnode.appendChild(this._filterHelper(ownerdoc, 
                        child.childNodes[j]));
                };
            };
        };
        return newnode;
    };
};

//-----------------------------------------------------------------------------
//
// XHTML validation support
//
// This class is the XHTML 1.0 transitional DTD expressed as Javascript
// data structures.
//
function XhtmlValidation(editor) {
    // Support functions
    this.Set = function(ary) {
        if (typeof(ary)==typeof('')) ary = [ary];
        if (ary instanceof Array) {
            for (var i = 0; i < ary.length; i++) {
                this[ary[i]] = 1;
            }
        }
        else {
            for (var v in ary) { // already a set?
                this[v] = 1;
            }
        }
    }

    this._exclude = function(array, exceptions) {
        var ex;
        if (exceptions.split) {
            ex = exceptions.split("|");
        } else {
            ex = exceptions;
        }
        var exclude = new this.Set(ex);
        var res = [];
        for (var k=0; k < array.length;k++) {
            if (!exclude[array[k]]) res.push(array[k]);
        }
        return res;
    }
    this.setAttrFilter = function(attributes, filter) {
        for (var j = 0; j < attributes.length; j++) {
            var attr = attributes[j];
            this.attrFilters[attr] = filter || this._defaultCopyAttribute;
        }
    }

    this.setTagAttributes = function(tags, attributes) {
        for (var j = 0; j < tags.length; j++) {
            this.tagAttributes[tags[j]] = attributes;
        }
    }

    // define some new attributes for existing tags
    this.includeTagAttributes = function(tags, attributes) {
        for (var j = 0; j < tags.length; j++) {
            var tag = tags[j];
            this.tagAttributes[tag] = this.tagAttributes[tag].concat(attributes);
        }
    }

    this.excludeTagAttributes = function(tags, attributes) {
        var bad = new this.Set(attributes);
        var tagset = new this.Set(tags);
        for (var tag in tagset) {
            var val = this.tagAttributes[tag];
            if (val) {
                for (var i = val.length; i >= 0; i--) {
                    if (bad[val[i]]) {
                        val = val.concat(); // Copy
                        val.splice(i,1);
                    }
                }
            }
            this.tagAttributes[tag] = val;
            // have to store this to allow filtering for nodes on which
            // '*' is set as allowed, this allows using '*' for the attributes
            // but also filtering some out
            this.badTagAttributes[tag] = attributes;
        }
    }

    this.excludeTags = function(badtags) {
        if (typeof(badtags)==typeof('')) badtags = [badtags];
        for (var i = 0; i < badtags.length; i++) {
            delete this.tagAttributes[badtags[i]];
        }
    }

    this.excludeAttributes = function(badattrs) {
        this.excludeTagAttributes(this.tagAttributes, badattrs);
        for (var i = 0; i < badattrs.length; i++) {
            delete this.attrFilters[badattrs[i]];
        }
    }
    if (editor.getBrowserName()=="IE") {
        this._getTagName = function(htmlnode) {
            var nodename = htmlnode.nodeName.toLowerCase();
            if (htmlnode.scopeName && htmlnode.scopeName != "HTML") {
                nodename = htmlnode.scopeName+':'+nodename;
            }
            return nodename;
        }
    } else {
        this._getTagName = function(htmlnode) {
            return htmlnode.nodeName.toLowerCase();
        }
    };

    // Supporting declarations
    this.elements = new function(validation) {
        // A list of all attributes
        this.attributes = [
            'abbr','accept','accept-charset','accesskey','action','align','alink',
            'alt','archive','axis','background','bgcolor','border','cellpadding',
            'cellspacing','char','charoff','charset','checked','cite','class',
            'classid','clear','code','codebase','codetype','color','cols','colspan',
            'compact','content','coords','data','datetime','declare','defer','dir',
            'disabled','enctype','face','for','frame','frameborder','headers',
            'height','href','hreflang','hspace','http-equiv','id','ismap','label',
            'lang','language','link','longdesc','marginheight','marginwidth',
            'maxlength','media','method','multiple','name','nohref','noshade','nowrap',
            'object','onblur','onchange','onclick','ondblclick','onfocus','onkeydown',
            'onkeypress','onkeyup','onload','onmousedown','onmousemove','onmouseout',
            'onmouseover','onmouseup','onreset','onselect','onsubmit','onunload',
            'profile','prompt','readonly','rel','rev','rows','rowspan','rules',
            'scheme','scope','scrolling','selected','shape','size','span','src',
            'standby','start','style','summary','tabindex','target','text','title',
            'type','usemap','valign','value','valuetype','vlink','vspace','width',
            'xml:lang','xml:space','xmlns'];

        // Core attributes
        this.coreattrs = ['id', 'title', 'style', 'class'];
        this.i18n = ['lang', 'dir', 'xml:lang'];
        // All event attributes are here but commented out so we don't
        // have to remove them later.
        this.events = []; // 'onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup'.split('|');
        this.focusevents = []; // ['onfocus','onblur']
        this.loadevents = []; // ['onload', 'onunload']
        this.formevents = []; // ['onsubmit','onreset']
        this.inputevents = [] ; // ['onselect', 'onchange']
        this.focus = ['accesskey', 'tabindex'].concat(this.focusevents);
        this.attrs = [].concat(this.coreattrs, this.i18n, this.events);

        // entities
        this.special_extra = ['object','applet','img','map','iframe'];
        this.special_basic=['br','span','bdo'];
        this.special = [].concat(this.special_basic, this.special_extra);
        this.fontstyle_extra = ['big','small','font','basefont'];
        this.fontstyle_basic = ['tt','i','b','u','s','strike'];
        this.fontstyle = [].concat(this.fontstyle_basic, this.fontstyle_extra);
        this.phrase_extra = ['sub','sup'];
        this.phrase_basic=[
                          'em','strong','dfn','code','q',
                          'samp','kbd','var', 'cite','abbr','acronym'];
        this.inline_forms = ['input','select','textarea','label','button'];
        this.misc_inline = ['ins','del'];
        this.misc = ['noscript'].concat(this.misc_inline);
        this.inline = ['a'].concat(this.special, this.fontstyle, this.phrase, this.inline_forms);

        this.Inline = ['#PCDATA'].concat(this.inline, this.misc_inline);

        this.heading = ['h1','h2','h3','h4','h5','h6'];
        this.lists = ['ul','ol','dl','menu','dir'];
        this.blocktext = ['pre','hr','blockquote','address','center','noframes'];
        this.block = ['p','div','isindex','fieldset','table'].concat(
                     this.heading, this.lists, this.blocktext);

        this.Flow = ['#PCDATA','form'].concat(this.block, this.inline);
    }(this);

    this._commonsetting = function(self, names, value) {
        for (var n = 0; n < names.length; n++) {
            self[names[n]] = value;
        }
    }
    
    // The tagAttributes class returns all valid attributes for a tag,
    // e.g. a = this.tagAttributes.head
    // a.head -> [ 'lang', 'xml:lang', 'dir', 'id', 'profile' ]
    this.tagAttributes = new function(el, validation) {
        this.title = el.i18n.concat('id');
        this.html = this.title.concat('xmlns');
        this.head = this.title.concat('profile');
        this.base = ['id', 'href', 'target'];
        this.meta =  this.title.concat('http-equiv','name','content', 'scheme');
        this.link = el.attrs.concat('charset','href','hreflang','type', 'rel','rev','media','target');
        this.style = this.title.concat('type','media','title', 'xml:space');
        this.script = ['id','charset','type','language','src','defer', 'xml:space'];
        this.iframe = [
                      'longdesc','name','src','frameborder','marginwidth',
                      'marginheight','scrolling','align','height','width'].concat(el.coreattrs);
        this.body = ['background','bgcolor','text','link','vlink','alink'].concat(el.attrs, el.loadevents);
        validation._commonsetting(this,
                                  ['p','div'].concat(el.heading),
                                  ['align'].concat(el.attrs));
        this.dl = this.dir = this.menu = el.attrs.concat('compact');
        this.ul = this.menu.concat('type');
        this.ol = this.ul.concat('start');
        this.li = el.attrs.concat('type','value');
        this.hr = el.attrs.concat('align','noshade','size','width');
        this.pre = el.attrs.concat('width','xml:space');
        this.blockquote = this.q = el.attrs.concat('cite');
        this.ins = this.del = this.blockquote.concat('datetime');
        this.a = el.attrs.concat(el.focus,'charset','type','name','href','hreflang','rel','rev','shape','coords','target');
        this.bdo = el.coreattrs.concat(el.events, 'lang','xml:lang','dir');
        this.br = el.coreattrs.concat('clear');
        validation._commonsetting(this,
                                  ['noscript','noframes','dt', 'dd', 'address','center','span','em', 'strong', 'dfn','code',
                                  'samp','kbd','var','cite','abbr','acronym','sub','sup','tt',
                                  'i','b','big','small','u','s','strike', 'fieldset'],
                                  el.attrs);

        this.basefont = ['id','size','color','face'];
        this.font = el.coreattrs.concat(el.i18n, 'size','color','face');
        this.object = el.attrs.concat('declare','classid','codebase','data','type','codetype','archive','standby','height','width','usemap','name','tabindex','align','border','hspace','vspace');
        this.param = ['id','name','value','valuetype','type'];
        this.applet = el.coreattrs.concat('codebase','archive','code','object','alt','name','width','height','align','hspace','vspace');
        this.img = el.attrs.concat('src','alt','name','longdesc','height','width','usemap','ismap','align','border','hspace','vspace');
        this.map = this.title.concat('title','name', 'style', 'class', el.events);
        this.area = el.attrs.concat('shape','coords','href','nohref','alt','target', el.focus);
        this.form = el.attrs.concat('action','method','name','enctype',el.formevents,'accept','accept-charset','target');
        this.label = el.attrs.concat('for','accesskey', el.focusevents);
        this.input = el.attrs.concat('type','name','value','checked','disabled','readonly','size','maxlength','src','alt','usemap',el.input,'accept','align', el.focus);
        this.select = el.attrs.concat('name','size','multiple','disabled','tabindex', el.focusevents,el.input);
        this.optgroup = el.attrs.concat('disabled','label');
        this.option = el.attrs.concat('selected','disabled','label','value');
        this.textarea = el.attrs.concat('name','rows','cols','disabled','readonly', el.inputevents, el.focus);
        this.legend = el.attrs.concat('accesskey','align');
        this.button = el.attrs.concat('name','value','type','disabled',el.focus);
        this.isindex = el.coreattrs.concat('prompt', el.i18n);
        this.table = el.attrs.concat('summary','width','border','frame','rules','cellspacing','cellpadding','align','bgcolor');
        this.caption = el.attrs.concat('align');
        this.col = this.colgroup = el.attrs.concat('span','width','align','char','charoff','valign');
        this.thead =  el.attrs.concat('align','char','charoff','valign');
        this.tfoot = this.tbody = this.thead;
        this.tr = this.thead.concat('bgcolor');
        this.td = this.th = this.tr.concat('abbr','axis','headers','scope','rowspan','colspan','nowrap','width','height');
    }(this.elements, this);

    this.badTagAttributes = new this.Set({});

    // State array. For each tag identifies what it can contain.
    // I'm not attempting to check the order or number of contained
    // tags (yet).
    this.States = new function(el, validation) {

        var here = this;
        function setStates(tags, value) {
            var valset = new validation.Set(value);

            for (var i = 0; i < tags.length; i++) {
                here[tags[i]] = valset;
            }
        }
        
        setStates(['html'], ['head','body']);
        setStates(['head'], ['title','base','script','style', 'meta','link','object','isindex']);
        setStates([
            'base', 'meta', 'link', 'hr', 'param', 'img', 'area', 'input',
            'br', 'basefont', 'isindex', 'col',
            ], []);

        setStates(['title','style','script','option','textarea'], ['#PCDATA']);
        setStates([ 'noscript', 'iframe', 'noframes', 'body', 'div',
            'li', 'dd', 'blockquote', 'center', 'ins', 'del', 'td', 'th',
            ], el.Flow);

        setStates(el.heading, el.Inline);
        setStates([ 'p', 'dt', 'address', 'span', 'bdo', 'caption',
            'em', 'strong', 'dfn','code','samp','kbd','var',
            'cite','abbr','acronym','q','sub','sup','tt','i',
            'b','big','small','u','s','strike','font','label',
            'legend'], el.Inline);

        setStates(['ul', 'ol', 'menu', 'dir', 'ul', ], ['li']);
        setStates(['dl'], ['dt','dd']);
        setStates(['pre'], validation._exclude(el.Inline, "img|object|applet|big|small|sub|sup|font|basefont"));
        setStates(['a'], validation._exclude(el.Inline, "a"));
        setStates(['applet', 'object'], ['#PCDATA', 'param','form'].concat(el.block, el.inline, el.misc));
        setStates(['map'], ['form', 'area'].concat(el.block, el.misc));
        setStates(['form'], validation._exclude(el.Flow, ['form']));
        setStates(['select'], ['optgroup','option']);
        setStates(['optgroup'], ['option']);
        setStates(['fieldset'], ['#PCDATA','legend','form'].concat(el.block,el.inline,el.misc));
        setStates(['button'], validation._exclude(el.Flow, ['a','form','iframe'].concat(el.inline_forms)));
        setStates(['table'], ['caption','col','colgroup','thead','tfoot','tbody','tr']);
        setStates(['thead', 'tfoot', 'tbody'], ['tr']);
        setStates(['colgroup'], ['col']);
        setStates(['tr'], ['th','td']);
    }(this.elements, this);

    // Permitted elements for style.
    this.styleWhitelist = new this.Set(['text-align', 'list-style-type', 'float']);
    this.classBlacklist = new this.Set(['MsoNormal', 'MsoTitle', 'MsoHeader', 'MsoFootnoteText',
        'Bullet1', 'Bullet2']);

    this.classFilter = function(value) {
        var classes = value.split(' ');
        var filtered = [];
        for (var i = 0; i < classes.length; i++) {
            var c = classes[i];
            if (c && !this.classBlacklist[c]) {
                filtered.push(c);
            }
        }
        return filtered.join(' ');
    }
    this._defaultCopyAttribute = function(name, htmlnode, xhtmlnode) {
        var val = htmlnode.getAttribute(name);
        if (val) xhtmlnode.setAttribute(name, val);
    }
    // Set up filters for attributes.
    var filter = this;
    this.attrFilters = new function(validation, editor) {
        var attrs = validation.elements.attributes;
        for (var i=0; i < attrs.length; i++) {
            this[attrs[i]] = validation._defaultCopyAttribute;
        }
        this['class'] = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.getAttribute('class');
            if (val) val = validation.classFilter(val);
            if (val) xhtmlnode.setAttribute('class', val);
        }
        // allow a * wildcard to make all attributes valid in the filter
        // note that this is pretty slow on IE
        this['*'] = function(name, htmlnode, xhtmlnode) {
            var nodeName = filter._getTagName(htmlnode);
            var bad = filter.badTagAttributes[nodeName];
            for (var i=0; i < htmlnode.attributes.length; i++) {
                var attr = htmlnode.attributes[i];
                if (bad && bad.contains(attr.name)) {
                    continue;
                };
                if (attr.value !== null && attr.value !== undefined) {
                    xhtmlnode.setAttribute(attr.name, attr.value);
                };
            };
        }
        if (editor.getBrowserName()=="IE") {
            this['class'] = function(name, htmlnode, xhtmlnode) {
                var val = htmlnode.className;
                if (val) val = validation.classFilter(val);
                if (val) xhtmlnode.setAttribute('class', val);
            }
            this['http-equiv'] = function(name, htmlnode, xhtmlnode) {
                var val = htmlnode.httpEquiv;
                if (val) xhtmlnode.setAttribute('http-equiv', val);
            }
            this['xml:lang'] = this['xml:space'] = function(name, htmlnode, xhtmlnode) {
                try {
                    var val = htmlnode.getAttribute(name);
                    if (val) xhtmlnode.setAttribute(name, val);
                } catch(e) {
                }
            }
        }
        this.rowspan = this.colspan = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.getAttribute(name);
            if (val && val != '1') xhtmlnode.setAttribute(name, val);
        }
        this.style = function(name, htmlnode, xhtmlnode) {
            var val = htmlnode.style.cssText;
            if (val) {
                var styles = val.split(/; */);
                for (var i = styles.length; i >= 0; i--) if (styles[i]) {
                    var parts = /^([^:]+): *(.*)$/.exec(styles[i]);
                    var name = parts[1].toLowerCase();
                    if (validation.styleWhitelist[name]) {
                        styles[i] = name+': '+parts[2];
                    } else {
                        styles.splice(i,1); // delete
                    }
                }
                if (styles[styles.length-1]) styles.push('');
                val = styles.join('; ').strip();
            }
            if (val) xhtmlnode.setAttribute('style', val);
        }
    }(this, editor);

    // Exclude unwanted tags.
    this.excludeTags(['center']);

    if (editor.config && editor.config.htmlfilter) {
        this.filterStructure = editor.config.htmlfilter.filterstructure;
        
        var exclude = editor.config.htmlfilter;
        if (exclude.a)
            this.excludeAttributes(exclude.a);
        if (exclude.t)
            this.excludeTags(exclude.t);
        if (exclude.c) {
            var c = exclude.c;
            if (!c.length) c = [c];
            for (var i = 0; i < c.length; i++) {
                this.excludeTagAttributes(c[i].t, c[i].a);
            }
        }
        if (exclude.xstyle) {
            var s = exclude.xstyle;
            for (var i = 0; i < s.length; i++) {
                this.styleWhitelist[s[i]] = 1;
            }
        }
        if (exclude['class']) {
            var c = exclude['class'];
            for (var i = 0; i < c.length; i++) {
                this.classBlacklist[c[i]] = 1;
            }
        }
    };

    // Copy all valid attributes from htmlnode to xhtmlnode.
    this._copyAttributes = function(htmlnode, xhtmlnode, valid) {
        if (valid.contains('*')) {
            // allow all attributes on this tag
            this.attrFilters['*'](name, htmlnode, xhtmlnode);
            return;
        };
        for (var i = 0; i < valid.length; i++) {
            var name = valid[i];
            var filter = this.attrFilters[name];
            if (filter) filter(name, htmlnode, xhtmlnode);
        }
    }

    this._convertToSarissaNode = function(ownerdoc, htmlnode, xhtmlparent) {
        return this._convertNodes(ownerdoc, htmlnode, xhtmlparent, new this.Set(['html']));
    };
    
    this._convertNodes = function(ownerdoc, htmlnode, xhtmlparent, permitted) {
        var name, parentnode = xhtmlparent;
        var nodename = this._getTagName(htmlnode);
        var nostructure = !this.filterstructure;

        // TODO: This permits valid tags anywhere. it should use the state
        // table in xhtmlvalid to only permit tags where the XHTML DTD
        // says they are valid.
        var validattrs = this.tagAttributes[nodename];
        if (validattrs && (nostructure || permitted[nodename])) {
            try {
                var xhtmlnode = ownerdoc.createElement(nodename);
                parentnode = xhtmlnode;
            } catch (e) { };

            if (validattrs && xhtmlnode)
                this._copyAttributes(htmlnode, xhtmlnode, validattrs);
        }

        var kids = htmlnode.childNodes;
        var permittedChildren = this.States[parentnode.tagName] || permitted;

        if (kids.length == 0) {
            if (htmlnode.text && htmlnode.text != "" &&
                (nostructure || permittedChildren['#PCDATA'])) {
                var text = htmlnode.text;
                var tnode = ownerdoc.createTextNode(text);
                parentnode.appendChild(tnode);
            }
        } else {
            for (var i = 0; i < kids.length; i++) {
                var kid = kids[i];

                if (kid.parentNode !== htmlnode) {
                    if (kid.tagName == 'BODY') {
                        if (nodename != 'html') continue;
                    } else if (kid.parentNode.tagName === htmlnode.tagName) {
                        continue; // IE bug: nodes appear multiple places
                    }
                }
                
                if (kid.nodeType == 1) {
                    var newkid = this._convertNodes(ownerdoc, kid, parentnode, permittedChildren);
                    if (newkid != null) {
                        parentnode.appendChild(newkid);
                    };
                } else if (kid.nodeType == 3) {
                    if (nostructure || permittedChildren['#PCDATA'])
                        parentnode.appendChild(ownerdoc.createTextNode(kid.nodeValue));
                } else if (kid.nodeType == 4) {
                    if (nostructure || permittedChildren['#PCDATA'])
                        parentnode.appendChild(ownerdoc.createCDATASection(kid.nodeValue));
                }
            }
        } 
        return xhtmlnode;
    };
}


/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupucontextmenu.js 9879 2005-03-18 12:04:00Z yuppie $


//----------------------------------------------------------------------------
// ContextMenu
//----------------------------------------------------------------------------

function ContextMenu() {
    /* the contextmenu */
    this.contextmenu = null;
    this.seperator = 1;

    this.initialize = function(editor) {
        /* set the event handlers and such */
        this.editor = editor;
        // needs some work since it won't work for more than one editor
        addEventHandler(this.editor.getInnerDocument(), "contextmenu", this.createContextMenu, this);
        //addEventHandler(editor.getInnerDocument(), "focus", this.hideContextMenu, this);
        addEventHandler(document, "focus", this.hideContextMenu, this);
        addEventHandler(editor.getInnerDocument(), "mousedown", this.hideContextMenu, this);
        addEventHandler(document, "mousedown", this.hideContextMenu, this);
    };

    this.createContextMenu = function(event) {
        /* Create and show the context menu 
        
            The method will ask all tools for any (optional) elements they
            want to add the menu and when done render it
        */
        if (event.stopPropagation) {
            event.stopPropagation();
        };
        event.returnValue = false;
        if (this.editor.getBrowserName() == 'IE') {
            this.editor._saveSelection();
        };
        // somehow Mozilla on Windows seems to generate the oncontextmenu event
        // several times on each rightclick, here's a workaround
        if (this.editor.getBrowserName() == 'Mozilla' && this.contextmenu) {
            return false;
        };
        this.hideContextMenu();
        var selNode = this.editor.getSelectedNode();
        var elements = new Array();
        for (var id in this.editor.tools) {
            var tool = this.editor.tools[id];
            // alas, some people seem to want backward compatibility ;)
            if (tool.createContextMenuElements) {
                var els = tool.createContextMenuElements(selNode, event);
                elements = elements.concat(els);
            };
        };
        // remove the last seperator
        this._createNewContextMenu(elements, event);
        this.last_event = event;
        return false;
    };

    this.hideContextMenu = function(event) {
        /* remove the context menu from view */
        if (this.contextmenu) {
            try {
                window.document.getElementsByTagName('body')[0].removeChild(this.contextmenu);
            } catch (e) {
                // after some commands, the contextmenu will be removed by 
                // the browser, ignore those cases
            };
            this.contextmenu = null;
        };
    };

    this._createNewContextMenu = function(elements, event) {
        /* add the elements to the contextmenu and show it */
        var doc = window.document;
        var menu = doc.createElement('div');
        menu.contentEditable = false;
        menu.designMode = 'Off';
        this._setMenuStyle(menu);
        for (var i=0; i < elements.length; i++) {
            var element = elements[i];
            if (element !== this.seperator) {
                var div = doc.createElement('div');
                div.style.width = '100%';
                var label = doc.createTextNode('\u00a0' + element.label);
                div.appendChild(label);
                menu.appendChild(div);
                // set a reference to the div on the element
                element.element = div;
                addEventHandler(div, "mousedown", element.action, element.context);
                addEventHandler(div, "mouseover", element.changeOverStyle, element);
                addEventHandler(div, "mouseout", element.changeNormalStyle, element);
                addEventHandler(div, "mouseup", this.hideContextMenu, this);
            } else {
                var hr = doc.createElement('hr');
                menu.appendChild(hr);
            };
        };
        // now move the menu to the right position
        var iframe = this.editor.getDocument().getEditable();
        var left = event.clientX;
        var top = event.clientY;
        var currnode = iframe;
        if (this.editor.getBrowserName() == 'IE') {
            while (currnode) {
                left += currnode.offsetLeft + currnode.clientLeft;
                top += currnode.offsetTop + currnode.clientTop;
                currnode = currnode.offsetParent;
            };
        } else {
            while (currnode) {
                left += currnode.offsetLeft;
                top += currnode.offsetTop;
                currnode = currnode.offsetParent;
            };
        };
        menu.style.left = left + 'px';
        menu.style.top = top + 'px';
        menu.style.visibility = 'visible';
        addEventHandler(menu, 'focus', function() {this.blur}, menu)
        doc.getElementsByTagName('body')[0].appendChild(menu);
        this.contextmenu = menu;
    };
    
    this._setMenuStyle = function(menu) {
        /* set the styles for the menu

            to change the menu style, override this method
        */
        menu.style.position = 'absolute';
        menu.style.backgroundColor = 'white';
        menu.style.fontFamily = 'Verdana, Arial, Helvetica, sans-serif';
        menu.style.fontSize = '12px';
        menu.style.lineHeight = '16px';
        menu.style.borderWidth = '1px';
        menu.style.borderStyle = 'solid';
        menu.style.borderColor = 'black';
        menu.style.cursor = 'default';
        menu.style.width = "8em";
    };

    this._showOriginalMenu = function(event) {
        window.document.dispatchEvent(this._last_event);
    };
};

function ContextMenuElement(label, action, context) {
    /* context menu element struct
    
        should be returned (optionally in a list) by the tools' 
        createContextMenuElements methods
    */
    this.label = label; // the text shown in the menu
    this.action = action; // a reference to the method that should be called
    this.context = context; // a reference to the object on which the method
                            //  is defined
    this.element = null; // the contextmenu machinery will add a reference 
                            // to the element here

    this.changeOverStyle = function(event) {
        /* set the background of the element to 'mouseover' style

            override only for the prototype, not for individual elements
            so every element looks the same
        */
        this.element.style.backgroundColor = 'blue';
    };

    this.changeNormalStyle = function(event) {
        /* set the background of the element back to 'normal' style

            override only for the prototype, not for individual elements
            so every element looks the same
        */
        this.element.style.backgroundColor = 'white';
    };
};

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

KupuEditor.prototype._getBase = function(dom) {
    var base = dom.getElementsByTagName('base');
    if (base.length) {
        return base[0].getAttribute('href');
    } else {
        return '';
    }
}

// $Id: kupuploneeditor.js 9879 2005-03-18 12:04:00Z yuppie $
KupuEditor.prototype.makeLinksRelative = function(contents,base,debug) {
    // After extracting text from Internet Explorer, all the links in
    // the document are absolute.
    // we can't use the DOM to convert them to relative links, since
    // its the DOM that corrupts them to absolute to begin with.
    // Instead we can find the base from the DOM and do replace on the
    // text until all our links are relative.

    var href = base.replace(/\/[^\/]*$/, '/');
    var hrefparts = href.split('/');
    return contents.replace(/(<[^>]* (?:src|href)=")([^"]*)"/g,
        function(str, tag, url, offset, contents) {
            var resolveuid = url.indexOf('/resolveuid/');
            if (resolveuid != -1) {
                str = tag + url.substr(resolveuid+1)+'"';
                return str;
            }
            var urlparts = url.split('#');
            var anchor = urlparts[1] || '';
            url = urlparts[0];
            var urlparts = url.split('/');
            var common = 0;
            while (common < urlparts.length &&
                   common < hrefparts.length &&
                   urlparts[common]==hrefparts[common])
                common++;
            var last = urlparts[common];
            if (common+1 == urlparts.length && last=='emptypage') {
                urlparts[common] = '';
            }
            // The base and the url have 'common' parts in common.
            // First two are the protocol, so only do stuff if more
            // than two match.
            if (common > 2) {
                var path = new Array();
                var i = 0;
                for (; i+common < hrefparts.length-1; i++) {
                    path[i] = '..';
                };
                while (common < urlparts.length) {
                    path[i++] = urlparts[common++];
                };
                if (i==0) {
                    path[i++] = '.';
                }
                str = path.join('/');
                if (anchor) {
                    str = [str,anchor].join('#');
                }
                str = tag + str+'"';
            };
            return str;
        });
};

KupuEditor.prototype.saveDataToField = function(form, field) {
    var sourcetool = this.getTool('sourceedittool');
    if (sourcetool) {sourcetool.cancelSourceMode();};

    if (!this._initialized) {
        return;
    };
    this._initialized = false;

    // set the window status so people can see we're actually saving
    window.status= "Please wait while saving document...";

    // pass the content through the filters
    this.logMessage("Starting HTML cleanup");

    var transform = this._filterContent(this.getInnerDocument().documentElement);

    // We need to get the contents of the body node as xml, but we don't
    // want the body node itself, so we use a regex to remove it
    var contents = kupu.getXMLBody(transform);
    if (/^<body[^>]*>(<\/?(p|br)[^>]*>|\&nbsp;)*<\/body>$/.test(contents)) {
        contents = ''; /* Ignore nearly empty contents */
    }
    var base = this._getBase(transform);
    contents = this._fixupSingletons(contents);
    contents = this.makeLinksRelative(contents, base).replace(/<\/?body[^>]*>/g, "");
    this.logMessage("Cleanup done, sending document to server");

    // now create the form input
    var document = form.ownerDocument;

    field.value = contents;
    
    kupu.content_changed = false;
};

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/
// This file deliberately left blank

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id$


function SourceEditTool(sourcebuttonid, sourceareaid) {
    /* Source edit tool to edit document's html source */
    this.sourceButton = getFromSelector(sourcebuttonid);
    this.sourcemode = false;
    this._currently_editing = null;

    // method defined inline to support closure
    // XXX would be nice to have this defined on the prototype too, because
    // of subclassing issues?
    this.getSourceArea = function() {
        return getFromSelector(sourceareaid);
    };
};

SourceEditTool.prototype = new KupuTool;

SourceEditTool.prototype.cancelSourceMode = function() {
    if (this._currently_editing) {
        this.switchSourceEdit(null, true);
    };
};

SourceEditTool.prototype.updateState = 
        SourceEditTool.prototype.cancelSourceMode;

SourceEditTool.prototype.initialize = function(editor) {
    /* attach the event handlers */
    this.editor = editor;
    this._fixTabIndex(this.sourceButton);
    addEventHandler(this.sourceButton, "click", this.switchSourceEdit, this);
    this.editor.logMessage(_('Source edit tool initialized'));
};

SourceEditTool.prototype.switchSourceEdit = function(event, nograb) {
    var kupu = this.editor;
    var docobj = this._currently_editing||kupu.getDocument();
    var editorframe = docobj.getEditable();
    var sourcearea = this.getSourceArea();
    var kupudoc = docobj.getDocument();
    var sourceClass = 'kupu-sourcemode';

    if (!this.sourcemode) {
        if (window.drawertool) {
            window.drawertool.closeDrawer();
        };
        if (/on/i.test(kupudoc.designMode)) {
            kupudoc.designMode = 'Off';
        };
        kupu._initialized = false;

        var data='';
        if(kupu.config.filtersourceedit) {
            window.status = _('Cleaning up HTML...');
            var transform = kupu._filterContent(
                                kupu.getInnerDocument().documentElement);
            data = kupu.getXMLBody(transform);
            data = kupu._fixupSingletons(data).replace(/<\/?body[^>]*>/g, "");
            window.status = '';
        } else {
            data = kupu.getHTMLBody();
        };
        sourcearea.value = data;
        kupu.setClass(sourceClass);
        editorframe.style.display = 'none';
        sourcearea.style.display = 'block';
        if (!nograb) {
            sourcearea.focus();
        };
        this._currently_editing = docobj;
      } else {
        kupu.setHTMLBody(sourcearea.value);
        kupu.clearClass(sourceClass);
        sourcearea.style.display = 'none';
        editorframe.style.display = 'block';
        if (/off/i.test(kupudoc.designMode)) {
            kupudoc.designMode = 'On';
        };
        if (!nograb) {
            docobj.getWindow().focus();
            var selection = this.editor.getSelection();
            selection.collapse();
        };

        kupu._initialized = true;
        this._currently_editing = null;
        this.editor.updateState();
    };
    this.sourcemode = !this.sourcemode;
};

SourceEditTool.prototype.enable = function() {
    KupuButtonEnable(this.sourceButton);
};

SourceEditTool.prototype.disable = function() {
    KupuButtonDisable(this.sourceButton);
};

function MultiSourceEditTool(sourcebuttonid, textareaprefix) {
    /* Source edit tool to edit document's html source */
    this.sourceButton = getFromSelector(sourcebuttonid);
    this.textareaprefix = textareaprefix;

    this._currently_editing = null;
};

MultiSourceEditTool.prototype = new SourceEditTool;

MultiSourceEditTool.prototype.getSourceArea = function() {
    var docobj = this._currently_editing||kupu.getDocument();
    var sourceareaid = this.textareaprefix + docobj.getEditable().id;
    return getFromSelector(sourceareaid);
};


/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 * 
 *****************************************************************************/

// $Id: kupudrawers.js 25188 2006-03-31 14:48:42Z fschulze $

function DrawerTool() {
    /* a tool to open and fill drawers

        this tool has to (and should!) only be instantiated once
    */
    this.drawers = {};
    this.current_drawer = null;
    
    this.initialize = function(editor) {
        this.editor = editor;
        this.isIE = this.editor.getBrowserName() == 'IE';
        // this essentially makes the drawertool a singleton
        window.drawertool = this;
    };

    this.registerDrawer = function(id, drawer, editor) {
        this.drawers[id] = drawer;
        drawer.initialize(editor || this.editor, this);
    };

    this.openDrawer = function(id) {
        /* open a drawer */
        if (this.current_drawer) {
            this.closeDrawer();
        };
        var drawer = this.drawers[id];
        if (this.isIE) {
            drawer.editor._saveSelection();
        }
        drawer.createContent();
        drawer.editor.suspendEditing();
        this.current_drawer = drawer;
    };

    this.updateState = function(selNode) {
    };

    this.closeDrawer = function(button) {
        if (!this.current_drawer) {
            return;
        };
        this.current_drawer.hide();
        this.current_drawer.editor.resumeEditing();
        this.current_drawer = null;
    };

//     this.getDrawerEnv = function(iframe_win) {
//         var drawer = null;
//         for (var id in this.drawers) {
//             var ldrawer = this.drawers[id];
//             // Note that we require drawers to provide us with an
//             // element property!
//             if (ldrawer.element.contentWindow == iframe_win) {
//                 drawer = ldrawer;
//             };
//         };
//         if (!drawer) {
//             this.editor.logMessage("Drawer not found", 1);
//             return;
//         };
//         return {
//             'drawer': drawer,
//             'drawertool': this,
//             'tool': drawer.tool
//         };
//     };
};

DrawerTool.prototype = new KupuTool;

function Drawer(elementid, tool) {
    /* base prototype for drawers */

    this.element = getFromSelector(elementid);
    this.tool = tool;
    
    this.initialize = function(editor, drawertool) {
        this.editor = editor;
        this.drawertool = drawertool;
    };
    
    this.createContent = function() {
        /* fill the drawer with some content */
        // here's where any intelligence and XSLT transformation and such 
        // is done
        this.element.style.display = 'block';
        this.focusElement();
    };

    this.hide = function() {
        this.element.style.display = 'none';
        this.focussed = false;
    };

    this.focusElement = function() {
        // IE can focus the drawer element, but Mozilla needs more help
        this.focussed = false;
        var iterator = new NodeIterator(this.element);
        var currnode = iterator.next();
        while (currnode) {
            if (currnode.tagName && (currnode.tagName.toUpperCase()=='BUTTON' ||
                (currnode.tagName.toUpperCase()=='INPUT' && !(/nofocus/.test(currnode.className)))
                )) {
                this.focussed = true;
                function focusit() {
                    currnode.focus();
                }
                timer_instance.registerFunction(this, focusit, 100);
                return;
            }
            currnode = iterator.next();
        }
    }
};

function LinkDrawer(elementid, tool, wrap) {
    /* Link drawer */
    this.element = getFromSelector(elementid);
    this.tool = tool;
    function wrap(id, tag) {
        return '#'+this.element.id+' '+tag+'.'+id;
    }
    var input = getBaseTagClass(this.element, 'input', 'kupu-linkdrawer-input');
    var preview = getBaseTagClass(this.element, 'iframe', 'kupu-linkdrawer-preview');

    this.createContent = function() {
        /* display the drawer */
        var currnode = this.editor.getSelectedNode();
        var linkel = this.editor.getNearestParentOfType(currnode, 'a');
        input.value = "";
        this.preview();
        if (linkel) {
            input.value = linkel.getAttribute('href');
        } else {
            input.value = 'http://';
        };
        this.element.style.display = 'block';
        this.focusElement();
    };

    this.save = function() {
        /* add or modify a link */
        this.editor.resumeEditing();
        var url = input.value;
        var target = '_self';
        if (this.target) target = this.target;
        this.tool.createLink(url, null, null, target);
        input.value = '';

        // XXX when reediting a link, the drawer does not close for
        // some weird reason. BUG! Close the drawer manually until we
        // find a fix:
        this.drawertool.closeDrawer();
    };
    
    this.preview = function() {
        preview.src = input.value;
        if (this.editor.getBrowserName() == 'IE') {
            preview.width = "800";
            preview.height = "365";
            preview.style.zoom = "60%";
        };
    }
    this.preview_loaded = function() {
        if (input.value  != preview.src) {
            input.value = preview.src;
        }
    }
};

LinkDrawer.prototype = new Drawer;

function TableDrawer(elementid, tool) {
    /* Table drawer */
    this.element = getFromSelector(elementid);
    this.tool = tool;

    this.addpanel = getBaseTagClass(this.element, 'div', 'kupu-tabledrawer-addtable');
    this.editpanel = getBaseTagClass(this.element, 'div', 'kupu-tabledrawer-edittable');
    var editclassselect = getBaseTagClass(this.element, 'select', 'kupu-tabledrawer-editclasschooser');
    var addclassselect = getBaseTagClass(this.element, 'select', 'kupu-tabledrawer-addclasschooser');
    var alignselect = getBaseTagClass(this.element, 'select', 'kupu-tabledrawer-alignchooser');
    var newrowsinput = getBaseTagClass(this.element, 'input', 'kupu-tabledrawer-newrows');
    var newcolsinput = getBaseTagClass(this.element, 'input', 'kupu-tabledrawer-newcols');
    var makeheadercheck = getBaseTagClass(this.element, 'input', 'kupu-tabledrawer-makeheader');

    this.createContent = function() {
        var editor = this.editor;
        var selNode = editor.getSelectedNode();

        function fixClasses(classselect) {
            if (editor.config.table_classes) {
                var classes = editor.config.table_classes['class'];
                while (classselect.hasChildNodes()) {
                    classselect.removeChild(classselect.firstChild);
                };
                for (var i=0; i < classes.length; i++) {
                    var classinfo = classes[i];
                    var caption = classinfo.xcaption || classinfo;
                    var classname = classinfo.classname || classinfo;

                    var option = document.createElement('option');
                    var content = document.createTextNode(caption);
                    option.appendChild(content);
                    option.setAttribute('value', classname);
                    classselect.appendChild(option);
                };
            };
        };
        fixClasses(addclassselect);
        fixClasses(editclassselect);
        
        var table = editor.getNearestParentOfType(selNode, 'table');

        if (!table) {
            // show add table drawer
            show = this.addpanel;
            hide = this.editpanel;
        } else {
            // show edit table drawer
            show = this.editpanel;
            hide = this.addpanel;
            var align = this.tool._getColumnAlign(selNode);
            selectSelectItem(alignselect, align);
            selectSelectItem(editclassselect, table.className);
        };
        hide.style.display = 'none';
        show.style.display = 'block';
        this.element.style.display = 'block';
        this.focusElement();
    };

    this.createTable = function() {
        this.editor.resumeEditing();
        var rows = newrowsinput.value;
        var cols = newcolsinput.value;
        var style = addclassselect.value;
        var add_header = makeheadercheck.checked;
        this.tool.createTable(parseInt(rows), parseInt(cols), add_header, style);
        this.drawertool.closeDrawer();
    };
    this.delTableRow = function() {
        this.editor.resumeEditing();
        this.tool.delTableRow();
        this.editor.suspendEditing();
    };
    this.addTableRow = function() {
        this.editor.resumeEditing();
        this.tool.addTableRow();
        this.editor.suspendEditing();
    };
    this.delTableColumn = function() {
        this.editor.resumeEditing();
        this.tool.delTableColumn();
        this.editor.suspendEditing();
    };
    this.addTableColumn = function() {
        this.editor.resumeEditing();
        this.tool.addTableColumn();
        this.editor.suspendEditing();
    };
    this.fixTable = function() {
        this.editor.resumeEditing();
        this.tool.fixTable();
        this.editor.suspendEditing();
    };
    this.fixAllTables = function() {
        this.editor.resumeEditing();
        this.tool.fixAllTables();
        this.editor.suspendEditing();
    };
    this.setTableClass = function(className) {
        this.editor.resumeEditing();
        this.tool.setTableClass(className);
        this.editor.suspendEditing();
    };
    this.setColumnAlign = function(align) {
        this.editor.resumeEditing();
        this.tool.setColumnAlign(align);
        this.editor.suspendEditing();
    };
};

TableDrawer.prototype = new Drawer;

function LibraryDrawer(tool, xsluri, libsuri, searchuri, baseelement) {
    /* a drawer that loads XSLT and XML from the server 
       and converts the XML to XHTML for the drawer using the XSLT

       there are 2 types of XML file loaded from the server: the first
       contains a list of 'libraries', partitions for the data items, 
       and the second a list of data items for a certain library

       all XML loading is done async, since sync loading can freeze Mozilla
    */

    this.init = function(tool, xsluri, libsuri, searchuri, baseelement) {
        /* This method is there to thin out the constructor and to be
           able to inherit it in sub-prototypes. Don't confuse this
           method with the component initializer (initialize()).
        */
        // these are used in the XSLT. Maybe they should be
        // parameterized or something, but we depend on so many other
        // things implicitly anyway...
        this.drawerid = 'kupu-librarydrawer';
        this.librariespanelid = 'kupu-librariespanel';
        this.resourcespanelid = 'kupu-resourcespanel';
        this.propertiespanelid = 'kupu-propertiespanel';

        if (baseelement) {
            this.baseelement = getFromSelector(baseelement);
        } else {
            this.baseelement = getBaseTagClass(document.body, 'div', 'kupu-librarydrawer-parent');
        }

        this.tool = tool;
        this.element = document.getElementById(this.drawerid);
        if (!this.element) {
            var e = document.createElement('div');
            e.id = this.drawerid;
            e.className = 'kupu-drawer '+this.drawerid;
            this.baseelement.appendChild(e);
            this.element = e;
        }
        this.shared.xsluri = xsluri;
        this.shared.libsuri = libsuri;
        this.shared.searchuri = searchuri;
        
        // marker that gets set when a new image has been uploaded
        this.shared.newimages = null;

        // the following vars will be available after this.initialize()
        // has been called
    
        // this will be filled by this._libXslCallback()
        this.shared.xsl = null;
        // this will be filled by this.loadLibraries(), which is called 
        // somewhere further down the chain starting with 
        // this._libsXslCallback()
        this.shared.xmldata = null;

    };
    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri);
    }

    this.initialize = function(editor, drawertool) {
        this.editor = editor;
        this.drawertool = drawertool;
        this.selecteditemid = '';

        // load the xsl and the initial xml
        var wrapped_callback = new ContextFixer(this._libsXslCallback, this);
        this._loadXML(this.shared.xsluri, wrapped_callback.execute);
    };

    /*** bootstrapping ***/

    this._libsXslCallback = function(dom) {
        /* callback for when the xsl for the libs is loaded
        
            this is called on init and since the initial libs need
            to be loaded as well (and everything is async with callbacks
            so there's no way to wait until the XSL is loaded) this
            will also make the first loadLibraries call
        */
        this.shared.xsl = dom;

        // Change by Paul to have cached xslt transformers for reuse of 
        // multiple transforms and also xslt params
        try {
            var xsltproc =  new XSLTProcessor();
            this.shared.xsltproc = xsltproc;
            xsltproc.importStylesheet(dom);
            xsltproc.setParameter("", "drawertype", this.drawertype);
            xsltproc.setParameter("", "drawertitle", this.drawertitle);
            xsltproc.setParameter("", "showupload", this.showupload);
            if (this.editor.config.captions) {
                xsltproc.setParameter("", "usecaptions", 'yes');
            }
        } catch(e) {
            return; // No XSLT Processor, maybe IE 5.5?
        }
    };

    this.createContent = function() {
        // Make sure the drawer XML is in the current Kupu instance
        if (this.element.parentNode != this.baseelement) {
            this.baseelement.appendChild(this.element);
        }
        // load the initial XML
        if(!this.shared.xmldata) {
            // Do a meaningful test to see if this is IE5.5 or some other 
            // editor-enabled version whose XML support isn't good enough 
            // for the drawers
            if (!window.XSLTProcessor) {
               alert("This function requires better XML support in your browser.");
               return;
            }
            this.loadLibraries();
        } else {
            if (this.shared.newimages) {
                this.reloadCurrent();
                this.shared.newimages = null;
            };
            this.updateDisplay();
            this.initialSelection();
        };

        // display the drawer div
        this.element.style.display = 'block';
    };

    this._singleLibsXslCallback = function(dom) {
        /* callback for then the xsl for single libs (items) is loaded

            nothing special needs to be called here, since initially the
            items pane will be empty
        */
        this.singlelibxsl = dom;
    };

    this.loadLibraries = function() {
        /* load the libraries and display them in a redrawn drawer */
        var wrapped_callback = new ContextFixer(this._libsContentCallback, this);
        this._loadXML(this.shared.libsuri, wrapped_callback.execute);
    };

    this._libsContentCallback = function(dom) {
        /* this is called when the libs xml is loaded

            does the xslt transformation to set up or renew the drawer's full
            content and adds the content to the drawer
        */
        this.shared.xmldata = dom;
        this.shared.xmldata.setProperty("SelectionLanguage", "XPath");

        // replace whatever is in there with our stuff
        this.updateDisplay(this.drawerid);
        this.initialSelection();
    };

    this.initialSelection = function() {
        var libnode_path = '/libraries/library[@selected]';
        var libnode = this.shared.xmldata.selectSingleNode(libnode_path);
        if (libnode) {
            var id = libnode.getAttribute('id');
            this.selectLibrary(id);
        }
    }

    this.updateDisplay = function(id) {
      /* (re-)transform XML and (re-)display the necessary part
       */
        if(!id) {
            id = this.drawerid;
        };
        try {
            this.shared.xsltproc.setParameter("", "showupload", this.showupload);
        } catch(e) {};
        var doc = this._transformXml();
        var sourcenode = doc.selectSingleNode('//*[@id="'+id+'"]');
        var targetnode = document.getElementById(id);
        sourcenode = document.importNode(sourcenode, true);
        Sarissa.copyChildNodes(sourcenode, targetnode);
        if (!this.focussed) {
            this.focusElement();
        }

        if (this.editor.getBrowserName() == 'IE' && id == this.resourcespanelid) {
            this.updateDisplay(this.drawerid);
        };
    };

    this.deselectActiveCollection = function() {
        /* Deselect the currently active collection or library */
        while (1) {
            // deselect selected DOM node
            var selected = this.shared.xmldata.selectSingleNode('//*[@selected]');
            if (!selected) {
                return;
            };
            selected.removeAttribute('selected');
        };
    };

    /*** Load a library ***/

    this.selectLibrary = function(id) {
        /* unselect the currently selected lib and select a new one

            the selected lib (libraries pane) will have a specific CSS class 
            (selected)
        */
        // remove selection in the DOM
        this.deselectActiveCollection();
        // as well as visual selection in CSS
        // XXX this is slow, but we can't do XPath, unfortunately
        var divs = this.element.getElementsByTagName('div');
        for (var i=0; i<divs.length; i++ ) {
          if (divs[i].className == 'kupu-libsource-selected') {
            divs[i].className = 'kupu-libsource';
          };
        };

        var libnode_path = '/libraries/library[@id="' + id + '"]';
        var libnode = this.shared.xmldata.selectSingleNode(libnode_path);
        libnode.setAttribute('selected', '1');

        var items_xpath = "items";
        var items_node = libnode.selectSingleNode(items_xpath);
        
        if (items_node && !this.shared.newimages) {
            // The library has already been loaded before or was
            // already provided with an items list. No need to do
            // anything except for displaying the contents in the
            // middle pane. Newimages is set if we've lately
            // added an image.
            this.updateDisplay(this.resourcespanelid);
            this.updateDisplay(this.propertiespanelid);
        } else {
            // We have to load the library from XML first.
            var src_uri = libnode.selectSingleNode('src/text()').nodeValue;
            src_uri = src_uri.strip(); // needs kupuhelpers.js
            // Now load the library into the items pane. Since we have
            // to load the XML, do this via a call back
            var wrapped_callback = new ContextFixer(this._libraryContentCallback, this);
            this._loadXML(src_uri, wrapped_callback.execute, null);
            this.shared.newimages = null;
        };
        // instead of running the full transformations again we get a 
        // reference to the element and set the classname...
        var newseldiv = document.getElementById(id);
        newseldiv.className = 'kupu-libsource-selected';
    };

    this._libraryContentCallback = function(dom, src_uri) {
        /* callback for when a library's contents (item list) is loaded

        This is also used as he handler for reloading a standard
        collection.
        */
        var libnode = this.shared.xmldata.selectSingleNode('//*[@selected]');
        var itemsnode = libnode.selectSingleNode("items");
        var newitemsnode = dom.selectSingleNode("//items");

        // IE does not support importNode on XML document nodes. As an
        // evil hack, clonde the node instead.

        if (this.editor.getBrowserName() == 'IE') {
            newitemsnode = newitemsnode.cloneNode(true);
        } else {
            newitemsnode = this.shared.xmldata.importNode(newitemsnode, true);
        }
        if (!itemsnode) {
            // We're loading this for the first time
            libnode.appendChild(newitemsnode);
        } else {
            // User has clicked reload
            libnode.replaceChild(newitemsnode, itemsnode);
        };
        this.updateDisplay(this.resourcespanelid);
        this.updateDisplay(this.propertiespanelid);
    };

    /*** Load a collection ***/

    this.selectCollection = function(id) {
        this.deselectActiveCollection();

        // First turn off current selection, if any
        this.removeSelection();
        
        var leafnode_path = "//collection[@id='" + id + "']";
        var leafnode = this.shared.xmldata.selectSingleNode(leafnode_path);

        // Case 1: We've already loaded the data, so we just need to
        // refer to the data by id.
        var loadedInNode = leafnode.getAttribute('loadedInNode');
        if (loadedInNode) {
            var collnode_path = "/libraries/collection[@id='" + loadedInNode + "']";
            var collnode = this.shared.xmldata.selectSingleNode(collnode_path);
            if (collnode) {
                collnode.setAttribute('selected', '1');
                this.updateDisplay(this.resourcespanelid);
                this.updateDisplay(this.propertiespanelid);
                return;
            };
        };

        // Case 2: We've already loaded the data, but there hasn't
        // been a reference made yet. So, make one :)
        uri = leafnode.selectSingleNode('uri/text()').nodeValue;
        uri = (new String(uri)).strip(); // needs kupuhelpers.js
        var collnode_path = "/libraries/collection/uri[text()='" + uri + "']/..";
        var collnode = this.shared.xmldata.selectSingleNode(collnode_path);
        if (collnode) {
            id = collnode.getAttribute('id');
            leafnode.setAttribute('loadedInNode', id);
            collnode.setAttribute('selected', '1');
            this.updateDisplay(this.resourcespanelid);
            this.updateDisplay(this.propertiespanelid);
            return;
        };

        // Case 3: We've not loaded the data yet, so we need to load it
        // this is just so we can find the leafnode much easier in the
        // callback.
        leafnode.setAttribute('selected', '1');
        var src_uri = leafnode.selectSingleNode('src/text()').nodeValue;
        src_uri = src_uri.strip(); // needs kupuhelpers.js
        var wrapped_callback = new ContextFixer(this._collectionContentCallback, this);
        this._loadXML(src_uri, wrapped_callback.execute, null);
    };

    this._collectionContentCallback = function(dom, src_uri) {
        // Unlike with libraries, we don't have to find a node to hook
        // our results into (UNLESS we've hit the reload button, but
        // that is handled in _libraryContentCallback anyway).
        // We need to give the newly retrieved data a unique ID, we
        // just use the time.
        date = new Date();
        time = date.getTime();

        // attach 'loadedInNode' attribute to leaf node so Case 1
        // applies next time.
        var leafnode = this.shared.xmldata.selectSingleNode('//*[@selected]');
        leafnode.setAttribute('loadedInNode', time);
        this.deselectActiveCollection()

        var collnode = dom.selectSingleNode('/collection');
        collnode.setAttribute('id', time);
        collnode.setAttribute('selected', '1');

        var libraries = this.shared.xmldata.selectSingleNode('/libraries');

        // IE does not support importNode on XML documet nodes
        if (this.editor.getBrowserName() == 'IE') {
            collnode = collnode.cloneNode(true);
        } else {
            collnode = this.shared.xmldata.importNode(collnode, true);
        }
        libraries.appendChild(collnode);
        this.updateDisplay(this.resourcespanelid);
        this.updateDisplay(this.propertiespanelid);
    };

    /*** Reloading a collection or library ***/

    this.reloadCurrent = function() {
        // Reload current collection or library
        this.showupload = '';
        var current = this.shared.xmldata.selectSingleNode('//*[@selected]');
        // make sure we're dealing with a collection even though a
        // resource might be selected
        if (current.tagName == "resource") {
            current.removeAttribute("selected");
            current = current.parentNode;
            current.setAttribute("selected", "1");
        };
        var src_node = current.selectSingleNode('src');
        if (!src_node) {
            // simply do nothing if the library cannot be reloaded. This
            // is currently the case w/ search result libraries.
            return;
        };

        var src_uri = src_node.selectSingleNode('text()').nodeValue;
        
        src_uri = src_uri.strip(); // needs kupuhelpers.js

        var wrapped_callback = new ContextFixer(this._libraryContentCallback, this);
        this._loadXML(src_uri, wrapped_callback.execute);
    };

    this.removeSelection = function() {
        // turn off current selection, if any
        var oldselxpath = '/libraries/*[@selected]//resource[@selected]';
        var oldselitem = this.shared.xmldata.selectSingleNode(oldselxpath);
        if (oldselitem) {
            oldselitem.removeAttribute("selected");
        };
        if (this.selecteditemid) {
            var item = document.getElementById(this.selecteditemid);
            if (item) {
                var span = item.getElementsByTagName('span');
                if (span.length > 0) {
                    span = span[0];
                    span.className = span.className.replace(' selected-item', '');
                }
            }
            this.selecteditemid = '';
        }
        this.showupload = '';
    }

    this.selectUpload = function() {
        this.removeSelection();
        this.showupload = 'yes';
        this.updateDisplay(this.resourcespanelid);
        this.updateDisplay(this.propertiespanelid);
    }
    /*** Selecting a resource ***/

    this.selectItem = function (item, id) {
        /* select an item in the item pane, show the item's metadata */

        // First turn off current selection, if any
        this.removeSelection();
        
        // Grab XML DOM node for clicked "resource" and mark it selected
        var newselxpath = '/libraries/*[@selected]//resource[@id="' + id + '"]';
        var newselitem = this.shared.xmldata.selectSingleNode(newselxpath);
        newselitem.setAttribute("selected", "1");
        //this.updateDisplay(this.resourcespanelid);
        this.updateDisplay(this.propertiespanelid);

        // Don't want to reload the resource panel xml as it scrolls to
        // the top.
        var span = item.getElementsByTagName('span');
        if (span.length > 0) {
            span = span[0];
            span.className += ' selected-item';
        }
        this.selecteditemid = id;
        if (this.editor.getBrowserName() == 'IE') {
            var ppanel = document.getElementById(this.propertiespanelid)
            var height = ppanel.clientHeight;
            if (height > ppanel.scrollHeight) height = ppanel.scrollHeight;
            if (height < 260) height = 260;
            document.getElementById(this.resourcespanelid).style.height = height+'px';
        }
        return;
    }


    this.search = function() {
        /* search */
        var searchvalue = getFromSelector('kupu-searchbox-input').value;
        //XXX make search variable configurable
        var body = 'SearchableText=' + escape(searchvalue);

        // the search uri might contain query parameters in HTTP GET
        // style. We want to do a POST though, so find any possible
        // parameters, trim them from the URI and append them to the
        // POST body instead.
        var chunks = this.shared.searchuri.split('?');
        var searchuri = chunks[0];
        if (chunks[1]) {
            body += "&" + chunks[1];
        };
        var wrapped_callback = new ContextFixer(this._searchCallback, this);
        this._loadXML(searchuri, wrapped_callback.execute, body);
    };

    this._searchCallback = function(dom) {
        var resultlib = dom.selectSingleNode("/library");

        var items = resultlib.selectNodes("items/*");
        if (!items.length) {
            alert("No results found.");
            return;
        };

        // we need to give the newly retrieved data a unique ID, we
        // just use the time.
        date = new Date();
        time = date.getTime();
        resultlib.setAttribute("id", time);

        // deselect the previous collection and mark the result
        // library as selected
        this.deselectActiveCollection();
        resultlib.setAttribute("selected", "1");

        // now hook the result library into our DOM
        if (this.editor.getBrowserName() == 'IE') {
            resultlib = resultlib.cloneNode(true);
        } else {
            this.shared.xmldata.importNode(resultlib, true);
        }
        var libraries = this.shared.xmldata.selectSingleNode("/libraries");
        libraries.appendChild(resultlib);

        this.updateDisplay(this.drawerid);
        var newseldiv = getFromSelector(time);
        newseldiv.className = 'selected';
    };

    this.save = function() {
        /* save the element, should be implemented on subclasses */
        throw "Not yet implemented";
    };

    /*** Auxiliary methods ***/

    this._transformXml = function() {
        /* transform this.shared.xmldata to HTML using this.shared.xsl and return it */
        var doc = Sarissa.getDomDocument();
	var result = this.shared.xsltproc.transformToDocument(this.shared.xmldata);
        return result;
    };

    this._loadXML = function(uri, callback, body) {
        /* load the XML from a uri
        
            calls callback with one arg (the XML DOM) when done
            the (optional) body arg should contain the body for the request
*/
	var xmlhttp = new XMLHttpRequest();
        var method = 'GET';
        if (body) {
          method = 'POST';
        } else {
          // be sure that body is null and not an empty string or
          // something
          body = null;
        };
        xmlhttp.open(method, uri, true);
        // use ContextFixer to wrap the Sarissa callback, both for isolating 
        // the 'this' problem and to be able to pass in an extra argument 
        // (callback)
        var wrapped_callback = new ContextFixer(this._sarissaCallback, xmlhttp,
                                                callback, uri);
        xmlhttp.onreadystatechange = wrapped_callback.execute;
        if (method == "POST") {
            // by default, we would send a 'text/xml' request, which
            // is a dirty lie; explicitly set the content type to what
            // a web server expects from a POST.
            xmlhttp.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
        };
        xmlhttp.send(body);
    };

    this._sarissaCallback = function(user_callback, uri) {
        /* callback for Sarissa
            when the callback is called because the data's ready it
            will get the responseXML DOM and call user_callback
            with the DOM as the first argument and the uri loaded
            as the second
            
            note that this method should be called in the context of an 
            xmlhttp object
        */
        var errmessage = 'Error loading XML: ';
        if (uri) {
            errmessage = 'Error loading ' + uri + ':';
        };
        if (this.readyState == 4) {
            if (this.status && this.status != 200) {
                alert(errmessage + this.status);
                throw "Error loading XML";
            };
            var dom = this.responseXML;
            user_callback(dom, uri);
        };
    };
};

LibraryDrawer.prototype = new Drawer;
LibraryDrawer.prototype.shared = {}; // Shared data

function ImageLibraryDrawer(tool, xsluri, libsuri, searchuri, baseelement) {
    /* a specific LibraryDrawer for images */

    this.drawertitle = "Insert Image";
    this.drawertype = "image";
    this.showupload = '';
    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri, baseelement);
    }
 
    
    // upload, on submit/insert press
    this.uploadImage = function() {
        var form = document.kupu_upload_form;
        if (!form || form.node_prop_image.value=='') return;

        if (form.node_prop_caption.value == "") {
            alert("Please enter a title for the image you are uploading");
            return;        
        };
        
        var targeturi =  this.shared.xmldata.selectSingleNode('/libraries/*[@selected]/uri/text()').nodeValue
        document.kupu_upload_form.action =  targeturi + "/kupuUploadImage";
        document.kupu_upload_form.submit();
    };
    
    // called for example when no permission to upload for some reason
    this.cancelUpload = function(msg) {
        var s = this.shared.xmldata.selectSingleNode('/libraries/*[@selected]');     
        s.removeAttribute("selected");
        this.updateDisplay();
        if (msg != '') {
            alert(msg);
        };
    };
    
    // called by onLoad within document sent by server
    this.finishUpload = function(url) {
        this.editor.resumeEditing();
        var imgclass = 'image-inline';
        if (this.editor.config.captions) {
            imgclass += " captioned";
        };
        this.tool.createImage(url, null, imgclass);
        this.shared.newimages = 1;
        this.drawertool.closeDrawer();
    };
    

    this.save = function() {
        this.editor.resumeEditing();
        /* create an image in the iframe according to collected data
           from the drawer */
        var selxpath = '//resource[@selected]';
        var selnode = this.shared.xmldata.selectSingleNode(selxpath);
        
        // If no image resource is selected, check for upload
        if (!selnode) {
            var uploadbutton = this.shared.xmldata.selectSingleNode("/libraries/*[@selected]//uploadbutton");
            if (uploadbutton) {
                this.uploadImage();
            };
            return;
        };

        var sizeselector = document.getElementsByName('image-size-selector');
        if (sizeselector && sizeselector.length > 0) {
            sizeselector = sizeselector[0];
            var uri = sizeselector.options[sizeselector.selectedIndex].value;
        } else {
            var uri = selnode.selectSingleNode('uri/text()').nodeValue;
        }
        uri = uri.strip();  // needs kupuhelpers.js
        var alt = getFromSelector('image_alt').value;

        var radios = document.getElementsByName('image-align');
        for (var i = 0; i < radios.length; i++) {
            if (radios[i].checked) {
                var imgclass = radios[i].value;
            };
        };

        var caption = document.getElementsByName('image-caption');
        if (caption && caption.length>0 && caption[0].checked) {
            imgclass += " captioned";
        };

        this.tool.createImage(uri, alt, imgclass);
        this.drawertool.closeDrawer();
    };
};

ImageLibraryDrawer.prototype = new LibraryDrawer;
ImageLibraryDrawer.prototype.shared = {}; // Shared data

function LinkLibraryDrawer(tool, xsluri, libsuri, searchuri, baseelement) {
    /* a specific LibraryDrawer for links */

    this.drawertitle = "Insert Link";
    this.drawertype = "link";
    this.showupload = '';
    if (tool) {
        this.init(tool, xsluri, libsuri, searchuri, baseelement);
    }

    this.save = function() {
        this.editor.resumeEditing();
        /* create a link in the iframe according to collected data
           from the drawer */
        var selxpath = '//resource[@selected]';
        var selnode = this.shared.xmldata.selectSingleNode(selxpath);
        if (!selnode) {
            return;
        };

        var uri = selnode.selectSingleNode('uri/text()').nodeValue;
        uri = uri.strip();  // needs kupuhelpers.js
        var title = '';
        title = selnode.selectSingleNode('title/text()').nodeValue;
        title = title.strip();

        // XXX requiring the user to know what link type to enter is a
        // little too much I think. (philiKON)
        var type = null;
        var name = getFromSelector('link_name').value;
        var target = null;
        if (getFromSelector('link_target') && getFromSelector('link_target').value != '')
            target = getFromSelector('link_target').value;
        
        this.tool.createLink(uri, type, name, target, title);
        this.drawertool.closeDrawer();
    };
};

LinkLibraryDrawer.prototype = new LibraryDrawer;
LinkLibraryDrawer.prototype.shared = {}; // Shared data

/* Function to suppress enter key in drawers */
function HandleDrawerEnter(event, clickid) {
    var key;
    event = event || window.event;
    key = event.which || event.keyCode;

    if (key==13) {
        if (clickid) {
            var button = document.getElementById(clickid);
            if (button) {
                button.click();
            }
        }
        event.cancelBubble = true;
        if (event.stopPropogation) event.stopPropogation();

        return false;
    }
    return true;
}

/*****************************************************************************
 *
 * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the Kupu
 * License. See LICENSE.txt for license text. For a list of Kupu
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: kupuploneinit.js 27127 2006-05-12 12:14:12Z duncan $

function initPloneKupu(editorId) {
    var topnode = getFromSelector(editorId);
    var prefix = '#'+editorId+' ';

    var iframe = getFromSelector(prefix+'iframe.kupu-editor-iframe');
    var textarea = getFromSelector(prefix+'textarea.kupu-editor-textarea');
    var l = new DummyLogger();

    // XXX this should be fixed in stylesheets, but I don't know how to do 
    // that without applying this change to the outter document. Damn iframes.
    var ibody = iframe.contentWindow.document.body;
    var form = textarea.form;
    var initialtext = textarea.value || (_SARISSA_IS_IE?'<p></p>':'<p><br></p>');

    // now some config values
    var conf = loadDictFromXML(document, prefix+'xml.kupuconfig');

    // the we create the document, hand it over the id of the iframe
    var doc = new KupuDocument(iframe);

    // now we can create the controller
    var kupu = new KupuEditor(doc, conf, l);
    kupu.setHTMLBody(initialtext);

    // add the contextmenu
    var cm = new ContextMenu();
    kupu.setContextMenu(cm);

    // now we can create a UI object which we can use from the UI
    var ui = new KupuUI(prefix+'select.kupu-tb-styles');
    kupu.registerTool('ui', ui);

    // function that returns a function to execute a button command
    var execCommand = function(cmd) {
        return function(button, editor) {
            editor.execCommand(cmd);
        };
    };

    var boldchecker = ParentWithStyleChecker(new Array('b', 'strong'),
					     'font-weight', 'bold');
    var boldbutton = new KupuStateButton(prefix+'button.kupu-bold', 
                                         execCommand('bold'),
                                         boldchecker,
                                         'kupu-bold',
                                         'kupu-bold-pressed');
    kupu.registerTool('boldbutton', boldbutton);

    var italicschecker = ParentWithStyleChecker(new Array('i', 'em'),
						'font-style', 'italic');
    var italicsbutton = new KupuStateButton(prefix+'button.kupu-italic', 
                                           execCommand('italic'),
                                           italicschecker, 
                                           'kupu-italic', 
                                           'kupu-italic-pressed');
    kupu.registerTool('italicsbutton', italicsbutton);

    /* disabled
    var underlinechecker = ParentWithStyleChecker(new Array('u'));
    var underlinebutton = new KupuStateButton(prefix+'button.kupu-underline', 
                                              execCommand('underline'),
                                              underlinechecker,
                                              'kupu-underline', 
                                              'kupu-underline-pressed');
    kupu.registerTool('underlinebutton', underlinebutton);
    */

    var subscriptchecker = ParentWithStyleChecker(new Array('sub'));
    var subscriptbutton = new KupuStateButton(prefix+'button.kupu-subscript',
                                              execCommand('subscript'),
                                              subscriptchecker,
                                              'kupu-subscript',
                                              'kupu-subscript-pressed');
    kupu.registerTool('subscriptbutton', subscriptbutton);

    var superscriptchecker = ParentWithStyleChecker(new Array('super', 'sup'));
    var superscriptbutton = new KupuStateButton(prefix+'button.kupu-superscript', 
                                                execCommand('superscript'),
                                                superscriptchecker,
                                                'kupu-superscript', 
                                                'kupu-superscript-pressed');
    kupu.registerTool('superscriptbutton', superscriptbutton);

    var justifyleftbutton = new KupuButton(prefix+'button.kupu-justifyleft',
                                           execCommand('justifyleft'));
    kupu.registerTool('justifyleftbutton', justifyleftbutton);

    var justifycenterbutton = new KupuButton(prefix+'button.kupu-justifycenter',
                                             execCommand('justifycenter'));
    kupu.registerTool('justifycenterbutton', justifycenterbutton);

    var justifyrightbutton = new KupuButton(prefix+'button.kupu-justifyright',
                                            execCommand('justifyright'));
    kupu.registerTool('justifyrightbutton', justifyrightbutton);

    var outdentbutton = new KupuButton(prefix+'button.kupu-outdent', execCommand('outdent'));
    kupu.registerTool('outdentbutton', outdentbutton);

    var indentbutton = new KupuButton(prefix+'button.kupu-indent', execCommand('indent'));
    kupu.registerTool('indentbutton', indentbutton);

    var undobutton = new KupuButton(prefix+'button.kupu-undo', execCommand('undo'));
    kupu.registerTool('undobutton', undobutton);

    var redobutton = new KupuButton(prefix+'button.kupu-redo', execCommand('redo'));
    kupu.registerTool('redobutton', redobutton);

    var removeimagebutton = new KupuRemoveElementButton(prefix+'button.kupu-removeimage',
							'img',
							'kupu-removeimage');
    kupu.registerTool('removeimagebutton', removeimagebutton);
    var removelinkbutton = new KupuRemoveElementButton(prefix+'button.kupu-removelink',
						       'a',
						       'kupu-removelink');
    kupu.registerTool('removelinkbutton', removelinkbutton);

    // add some tools

    var listtool = new ListTool(prefix+'button.kupu-insertunorderedlist',
                                prefix+'button.kupu-insertorderedlist',
                                prefix+'select.kupu-ulstyles',
                                prefix+'select.kupu-olstyles');
    kupu.registerTool('listtool', listtool);

    var definitionlisttool = new DefinitionListTool(prefix+'button.kupu-insertdefinitionlist');
    kupu.registerTool('definitionlisttool', definitionlisttool);
    
    var tabletool = new TableTool();
    kupu.registerTool('tabletool', tabletool);

    var showpathtool = new ShowPathTool('kupu-showpath-field');
    kupu.registerTool('showpathtool', showpathtool);

    var sourceedittool = new SourceEditTool(prefix+'button.kupu-source',
                                            prefix+'textarea.kupu-editor-textarea');
    kupu.registerTool('sourceedittool', sourceedittool);

    var imagetool = NoContextMenu(new ImageTool());
    kupu.registerTool('imagetool', imagetool);

    var linktool = NoContextMenu(new LinkTool());
    kupu.registerTool('linktool', linktool);

    var zoom = new KupuZoomTool(prefix+'button.kupu-zoom',
        prefix+'select.kupu-tb-styles',
        prefix+'button.kupu-logo');
    kupu.registerTool('zoomtool', zoom);

    // XXX  - Needs prefix here for multi area support, but also 
    // added to the template
    if (typeof KupuSpellChecker != 'undefined') {
        var spellchecker = new KupuSpellChecker('kupu-spellchecker-button',
                                                'kupu_library_tool/spellcheck');
        kupu.registerTool('spellchecker', spellchecker);
    } else {
        // hide the button when not available
        var spellchecker_tool = document.getElementById('kupu-spellchecker');
        spellchecker_tool.style.display = 'none';
    }

    // Use the generic beforeUnload handler if we have it:
    var beforeunloadTool = window.onbeforeunload && window.onbeforeunload.tool;
    if (beforeunloadTool) {
        var initialBody = ibody.innerHTML;
        beforeunloadTool.addHandler(function() {
            return ibody.innerHTML != initialBody;
        });
        beforeunloadTool.chkId[textarea.id] = function() { return false; }
        beforeunloadTool.addForm(form);
    }
    // Patch for bad AT format pulldown.
    var fmtname = textarea.name+'_text_format';
    var pulldown = form[fmtname];
    if (pulldown && pulldown.type=='select-one') {
        for (var i=0 ; i < pulldown.length; i++) {
            var opt = pulldown.options[i];
            opt.selected = opt.defaultSelected = (opt.value=='text/html');
        }
        pulldown.disabled = true;
        var hidden = document.createElement('input');
        hidden.type = 'hidden';
        hidden.name = fmtname;
        hidden.value = 'text/html';
        pulldown.parentNode.appendChild(hidden);
    };

    // Drawers...

    // Function that returns function to open a drawer
    var opendrawer = function(drawerid) {
        return function(button, editor) {
            drawertool.openDrawer(prefix+drawerid);
        };
    };

    var imagelibdrawerbutton = new KupuButton(prefix+'button.kupu-image',
                                              opendrawer('imagelibdrawer'));
    kupu.registerTool('imagelibdrawerbutton', imagelibdrawerbutton);

    var linklibdrawerbutton = new KupuButton(prefix+'button.kupu-inthyperlink',
                                             opendrawer('linklibdrawer'));
    kupu.registerTool('linklibdrawerbutton', linklibdrawerbutton);

    var linkdrawerbutton = new KupuButton(prefix+'button.kupu-exthyperlink',
                                          opendrawer('linkdrawer'));
    kupu.registerTool('linkdrawerbutton', linkdrawerbutton);

    var tabledrawerbutton = new KupuButton(prefix+'button.kupu-table',
                                           opendrawer('tabledrawer'));
    kupu.registerTool('tabledrawerbutton', tabledrawerbutton);

    // create some drawers, drawers are some sort of popups that appear when a 
    // toolbar button is clicked
    var drawertool = window.drawertool || new DrawerTool();
    kupu.registerTool('drawertool', drawertool);

    var drawerparent = prefix+'div.kupu-librarydrawer-parent';
    var linklibdrawer = new LinkLibraryDrawer(linktool, conf['link_xsl_uri'],
                                              conf['link_libraries_uri'],
                                              conf['search_links_uri'], drawerparent);
    drawertool.registerDrawer(prefix+'linklibdrawer', linklibdrawer, kupu);

    var imagelibdrawer = new ImageLibraryDrawer(imagetool, conf['image_xsl_uri'],
                                                conf['image_libraries_uri'],
                                                conf['search_images_uri'], drawerparent);
    drawertool.registerDrawer(prefix+'imagelibdrawer', imagelibdrawer, kupu);

    var linkdrawer = new LinkDrawer(prefix+'div.kupu-linkdrawer', linktool);
    drawertool.registerDrawer(prefix+'linkdrawer', linkdrawer, kupu);

    var tabledrawer = new TableDrawer(prefix+'div.kupu-tabledrawer', tabletool);
    drawertool.registerDrawer(prefix+'tabledrawer', tabledrawer, kupu);

    // register form submit handler, remove the drawer's contents before submitting 
    // the form since it seems to crash IE if we leave them alone
    function prepareForm(event) {
        kupu.saveDataToField(this.form, this);
        var drawer = window.document.getElementById('kupu-librarydrawer');
        if (drawer) {
            drawer.parentNode.removeChild(drawer);
        }
    };
    addEventHandler(textarea.form, 'submit', prepareForm, textarea);

    return kupu;
};

// modify LinkDrawer so all links have a target
// defaults to _self, override here if reqd.
//LinkDrawer.prototype.target = '_blank';


