Annotation of kupuMPIWG/common/kupuhelpers.js, revision 1.1

1.1     ! dwinter     1: /*****************************************************************************
        !             2:  *
        !             3:  * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved.
        !             4:  *
        !             5:  * This software is distributed under the terms of the Kupu
        !             6:  * License. See LICENSE.txt for license text. For a list of Kupu
        !             7:  * Contributors see CREDITS.txt.
        !             8:  *
        !             9:  *****************************************************************************/
        !            10: 
        !            11: // $Id: kupuhelpers.js 12353 2005-05-16 12:29:05Z duncan $
        !            12: 
        !            13: /*
        !            14: 
        !            15: Some notes about the scripts:
        !            16: 
        !            17: - Problem with bound event handlers:
        !            18:     
        !            19:     When a method on an object is used as an event handler, the method uses 
        !            20:     its reference to the object it is defined on. The 'this' keyword no longer
        !            21:     points to the class, but instead refers to the element on which the event
        !            22:     is bound. To overcome this problem, you can wrap the method in a class that
        !            23:     holds a reference to the object and have a method on the wrapper that calls
        !            24:     the input method in the input object's context. This wrapped method can be
        !            25:     used as the event handler. An example:
        !            26: 
        !            27:     class Foo() {
        !            28:         this.foo = function() {
        !            29:             // the method used as an event handler
        !            30:             // using this here wouldn't work if the method
        !            31:             // was passed to addEventListener directly
        !            32:             this.baz();
        !            33:         };
        !            34:         this.baz = function() {
        !            35:             // some method on the same object
        !            36:         };
        !            37:     };
        !            38: 
        !            39:     f = new Foo();
        !            40: 
        !            41:     // create the wrapper for the function, args are func, context
        !            42:     wrapper = new ContextFixer(f.foo, f);
        !            43: 
        !            44:     // the wrapper can be passed to addEventListener, 'this' in the method
        !            45:     // will be pointing to the right context.
        !            46:     some_element.addEventListener("click", wrapper.execute, false);
        !            47: 
        !            48: - Problem with window.setTimeout:
        !            49: 
        !            50:     The window.setTimeout function has a couple of problems in usage, all 
        !            51:     caused by the fact that it expects a *string* argument that will be
        !            52:     evalled in the global namespace rather than a function reference with
        !            53:     plain variables as arguments. This makes that the methods on 'this' can
        !            54:     not be called (the 'this' variable doesn't exist in the global namespace)
        !            55:     and references to variables in the argument list aren't allowed (since
        !            56:     they don't exist in the global namespace). To overcome these problems, 
        !            57:     there's now a singleton instance of a class called Timer, which has one 
        !            58:     public method called registerFunction. This can be called with a function
        !            59:     reference and a variable number of extra arguments to pass on to the 
        !            60:     function.
        !            61: 
        !            62:     Usage:
        !            63: 
        !            64:         timer_instance.registerFunction(this, this.myFunc, 10, 'foo', bar);
        !            65: 
        !            66:         will call this.myFunc('foo', bar); in 10 milliseconds (with 'this'
        !            67:         as its context).
        !            68: 
        !            69: */
        !            70: 
        !            71: //----------------------------------------------------------------------------
        !            72: // Helper classes and functions
        !            73: //----------------------------------------------------------------------------
        !            74: 
        !            75: function addEventHandler(element, event, method, context) {
        !            76:     /* method to add an event handler for both IE and Mozilla */
        !            77:     var wrappedmethod = new ContextFixer(method, context);
        !            78:     var args = new Array(null, null);
        !            79:     for (var i=4; i < arguments.length; i++) {
        !            80:         args.push(arguments[i]);
        !            81:     };
        !            82:     wrappedmethod.args = args;
        !            83:     try {
        !            84:         if (_SARISSA_IS_MOZ) {
        !            85:             element.addEventListener(event, wrappedmethod.execute, false);
        !            86:         } else if (_SARISSA_IS_IE) {
        !            87:             element.attachEvent("on" + event, wrappedmethod.execute);
        !            88:         } else {
        !            89:             throw _("Unsupported browser!");
        !            90:         };
        !            91:         return wrappedmethod.execute;
        !            92:     } catch(e) {
        !            93:         alert(_('exception ${message} while registering an event handler ' +
        !            94:                 'for element ${element}, event ${event}, method ${method}',
        !            95:                 {'message': e.message, 'element': element,
        !            96:                     'event': event,
        !            97:                     'method': method}));
        !            98:     };
        !            99: };
        !           100: 
        !           101: function removeEventHandler(element, event, method) {
        !           102:     /* method to remove an event handler for both IE and Mozilla */
        !           103:     if (_SARISSA_IS_MOZ) {
        !           104:         window.removeEventListener(event, method, false);
        !           105:     } else if (_SARISSA_IS_IE) {
        !           106:         element.detachEvent("on" + event, method);
        !           107:     } else {
        !           108:         throw _("Unsupported browser!");
        !           109:     };
        !           110: };
        !           111: 
        !           112: /* Replacement for window.document.getElementById()
        !           113:  * selector can be an Id (so we maintain backwards compatability)
        !           114:  * but is intended to be a subset of valid CSS selectors.
        !           115:  * For now we only support the format: "#id tag.class"
        !           116:  */
        !           117: function getFromSelector(selector) {
        !           118:     var match = /#(\S+)\s*([^ .]+)\.(\S+)/.exec(selector);
        !           119:     if (!match) {
        !           120:         return window.document.getElementById(selector);
        !           121:     }
        !           122:     var id=match[1], tag=match[2], className=match[3];
        !           123:     var base = window.document.getElementById(id);
        !           124:     return getBaseTagClass(base, tag, className);
        !           125: }
        !           126: 
        !           127: function getBaseTagClass(base, tag, className) {
        !           128:     var classPat = new RegExp('\\b'+className+'\\b');
        !           129: 
        !           130:     var nodes = base.getElementsByTagName(tag);
        !           131:     for (var i = 0; i < nodes.length; i++) {
        !           132:         if (classPat.test(nodes[i].className)) {
        !           133:             return nodes[i];
        !           134:         }
        !           135:     }
        !           136:     return null;
        !           137: }
        !           138: 
        !           139: function openPopup(url, width, height) {
        !           140:     /* open and center a popup window */
        !           141:     var sw = screen.width;
        !           142:     var sh = screen.height;
        !           143:     var left = sw / 2 - width / 2;
        !           144:     var top = sh / 2 - height / 2;
        !           145:     var win = window.open(url, 'someWindow', 
        !           146:                 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top);
        !           147:     return win;
        !           148: };
        !           149: 
        !           150: function selectSelectItem(select, item) {
        !           151:     /* select a certain item from a select */
        !           152:     for (var i=0; i < select.options.length; i++) {
        !           153:         var option = select.options[i];
        !           154:         if (option.value == item) {
        !           155:             select.selectedIndex = i;
        !           156:             return;
        !           157:         }
        !           158:     }
        !           159:     select.selectedIndex = 0;
        !           160: };
        !           161: 
        !           162: function ParentWithStyleChecker(tagnames, style, stylevalue, command) {
        !           163:     /* small wrapper that provides a generic function to check if a
        !           164:        button should look pressed in */
        !           165:     return function(selNode, button, editor, event) {
        !           166:         /* check if the button needs to look pressed in */
        !           167:         if (command) {
        !           168:             var result = editor.getInnerDocument().queryCommandState(command)
        !           169:             if (result || editor.getSelection().getContentLength() == 0) {
        !           170:                 return result;
        !           171:             };
        !           172:         };
        !           173:         var currnode = selNode;
        !           174:         while (currnode && currnode.style) {
        !           175:             for (var i=0; i < tagnames.length; i++) {
        !           176:                 if (currnode.nodeName.toLowerCase() == tagnames[i].toLowerCase()) {
        !           177:                     return true;
        !           178:                 };
        !           179:             };
        !           180:             if (style && currnode.style[style] == stylevalue) {
        !           181:                 return true;
        !           182:             };
        !           183:             currnode = currnode.parentNode;
        !           184:         };
        !           185:         return false;
        !           186:     };
        !           187: };
        !           188: 
        !           189: function _load_dict_helper(element) {
        !           190:     /* walks through a set of XML nodes and builds a nested tree of objects */
        !           191:     var dict = {};
        !           192:     for (var i=0; i < element.childNodes.length; i++) {
        !           193:         var child = element.childNodes[i];
        !           194:         if (child.nodeType == 1) {
        !           195:             var value = '';
        !           196:             for (var j=0; j < child.childNodes.length; j++) {
        !           197:                 // test if we can recurse, if so ditch the string (probably
        !           198:                 // ignorable whitespace) and dive into the node
        !           199:                 if (child.childNodes[j].nodeType == 1) {
        !           200:                     value = _load_dict_helper(child);
        !           201:                     break;
        !           202:                 } else if (typeof(value) == typeof('')) {
        !           203:                     value += child.childNodes[j].nodeValue;
        !           204:                 };
        !           205:             };
        !           206:             if (typeof(value) == typeof('') && !isNaN(parseInt(value)) && 
        !           207:                     parseInt(value).toString().length == value.length) {
        !           208:                 value = parseInt(value);
        !           209:             } else if (typeof(value) != typeof('')) {
        !           210:                 if (value.length == 1) {
        !           211:                     value = value[0];
        !           212:                 };
        !           213:             };
        !           214:             var name = child.nodeName.toLowerCase();
        !           215:             if (dict[name] != undefined) {
        !           216:                 if (!dict[name].push) {
        !           217:                     dict[name] = new Array(dict[name], value);
        !           218:                 } else {
        !           219:                     dict[name].push(value);
        !           220:                 };
        !           221:             } else {
        !           222:                 dict[name] = value;
        !           223:             };
        !           224:         };
        !           225:     };
        !           226:     return dict;
        !           227: };
        !           228: 
        !           229: function loadDictFromXML(document, islandid) {
        !           230:     /* load configuration values from an XML chunk
        !           231: 
        !           232:         this is quite generic, it just reads data from a chunk of XML into
        !           233:         an object, checking if the object is complete should be done in the
        !           234:         calling context.
        !           235:     */
        !           236:     var dict = {};
        !           237:     var confnode = getFromSelector(islandid);
        !           238:     var root = null;
        !           239:     for (var i=0; i < confnode.childNodes.length; i++) {
        !           240:         if (confnode.childNodes[i].nodeType == 1) {
        !           241:             root = confnode.childNodes[i];
        !           242:             break;
        !           243:         };
        !           244:     };
        !           245:     if (!root) {
        !           246:         throw(_('No element found in the config island!'));
        !           247:     };
        !           248:     dict = _load_dict_helper(root);
        !           249:     return dict;
        !           250: };
        !           251: 
        !           252: function NodeIterator(node, continueatnextsibling) {
        !           253:     /* simple node iterator
        !           254: 
        !           255:         can be used to recursively walk through all children of a node,
        !           256:         the next() method will return the next node until either the next
        !           257:         sibling of the startnode is reached (when continueatnextsibling is 
        !           258:         false, the default) or when there's no node left (when 
        !           259:         continueatnextsibling is true)
        !           260: 
        !           261:         returns false if no nodes are left
        !           262:     */
        !           263:     this.node = node;
        !           264:     this.current = node;
        !           265:     this.terminator = continueatnextsibling ? null : node;
        !           266:     
        !           267:     this.next = function() {
        !           268:         /* return the next node */
        !           269:         if (this.current === false) {
        !           270:             // restart
        !           271:             this.current = this.node;
        !           272:         };
        !           273:         var current = this.current;
        !           274:         if (current.firstChild) {
        !           275:             this.current = current.firstChild;
        !           276:         } else {
        !           277:             // walk up parents until we finish or find one with a nextSibling
        !           278:             while (current != this.terminator && !current.nextSibling) {
        !           279:                 current = current.parentNode;
        !           280:             };
        !           281:             if (current == this.terminator) {
        !           282:                 this.current = false;
        !           283:             } else {
        !           284:                 this.current = current.nextSibling;
        !           285:             };
        !           286:         };
        !           287:         return this.current;
        !           288:     };
        !           289: 
        !           290:     this.reset = function() {
        !           291:         /* reset the iterator so it starts at the first node */
        !           292:         this.current = this.node;
        !           293:     };
        !           294: 
        !           295:     this.setCurrent = function(node) {
        !           296:         /* change the current node
        !           297:             
        !           298:             can be really useful for specific hacks, the user must take
        !           299:             care that the node is inside the iterator's scope or it will
        !           300:             go wild
        !           301:         */
        !           302:         this.current = node;
        !           303:     };
        !           304: };
        !           305: 
        !           306: /* selection classes, these are wrappers around the browser-specific
        !           307:     selection objects to provide a generic abstraction layer
        !           308: */
        !           309: function BaseSelection() {
        !           310:     /* superclass for the Selection objects
        !           311:     
        !           312:         this will contain higher level methods that don't contain 
        !           313:         browser-specific code
        !           314:     */
        !           315:     this.splitNodeAtSelection = function(node) {
        !           316:         /* split the node at the current selection
        !           317: 
        !           318:             remove any selected text, then split the node on the location
        !           319:             of the selection, thus creating a new node, this is attached to
        !           320:             the node's parent after the node
        !           321: 
        !           322:             this will fail if the selection is not inside the node
        !           323:         */
        !           324:         if (!this.selectionInsideNode(node)) {
        !           325:             throw(_('Selection not inside the node!'));
        !           326:         };
        !           327:         // a bit sneaky: what we'll do is insert a new br node to replace
        !           328:         // the current selection, then we'll walk up to that node in both
        !           329:         // the original and the cloned node, in the original we'll remove
        !           330:         // the br node and everything that's behind it, on the cloned one
        !           331:         // we'll remove the br and everything before it
        !           332:         // anyway, we'll end up with 2 nodes, the first already in the 
        !           333:         // document (the original node) and the second we can just attach
        !           334:         // to the doc after the first one
        !           335:         var doc = this.document.getDocument();
        !           336:         var br = doc.createElement('br');
        !           337:         br.setAttribute('node_splitter', 'indeed');
        !           338:         this.replaceWithNode(br);
        !           339:         
        !           340:         var clone = node.cloneNode(true);
        !           341: 
        !           342:         // now walk through the original node
        !           343:         var iterator = new NodeIterator(node);
        !           344:         var currnode = iterator.next();
        !           345:         var remove = false;
        !           346:         while (currnode) {
        !           347:             if (currnode.nodeName.toLowerCase() == 'br' && currnode.getAttribute('node_splitter') == 'indeed') {
        !           348:                 // here's the point where we should start removing
        !           349:                 remove = true;
        !           350:             };
        !           351:             // we should fetch the next node before we remove the current one, else the iterator
        !           352:             // will fail (since the current node is removed)
        !           353:             var lastnode = currnode;
        !           354:             currnode = iterator.next();
        !           355:             // XXX this will leave nodes that *became* empty in place, since it doesn't visit it again,
        !           356:             // perhaps we should do a second pass that removes the rest(?)
        !           357:             if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) {
        !           358:                 lastnode.parentNode.removeChild(lastnode);
        !           359:             };
        !           360:         };
        !           361: 
        !           362:         // and through the clone
        !           363:         var iterator = new NodeIterator(clone);
        !           364:         var currnode = iterator.next();
        !           365:         var remove = true;
        !           366:         while (currnode) {
        !           367:             var lastnode = currnode;
        !           368:             currnode = iterator.next();
        !           369:             if (lastnode.nodeName.toLowerCase() == 'br' && lastnode.getAttribute('node_splitter') == 'indeed') {
        !           370:                 // here's the point where we should stop removing
        !           371:                 lastnode.parentNode.removeChild(lastnode);
        !           372:                 remove = false;
        !           373:             };
        !           374:             if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) {
        !           375:                 lastnode.parentNode.removeChild(lastnode);
        !           376:             };
        !           377:         };
        !           378: 
        !           379:         // next we need to attach the node to the document
        !           380:         if (node.nextSibling) {
        !           381:             node.parentNode.insertBefore(clone, node.nextSibling);
        !           382:         } else {
        !           383:             node.parentNode.appendChild(clone);
        !           384:         };
        !           385: 
        !           386:         // this will change the selection, so reselect
        !           387:         this.reset();
        !           388: 
        !           389:         // return a reference to the clone
        !           390:         return clone;
        !           391:     };
        !           392: 
        !           393:     this.selectionInsideNode = function(node) {
        !           394:         /* returns a Boolean to indicate if the selection is resided
        !           395:             inside the node
        !           396:         */
        !           397:         var currnode = this.parentElement();
        !           398:         while (currnode) {
        !           399:             if (currnode == node) {
        !           400:                 return true;
        !           401:             };
        !           402:             currnode = currnode.parentNode;
        !           403:         };
        !           404:         return false;
        !           405:     };
        !           406: };
        !           407: 
        !           408: function MozillaSelection(document) {
        !           409:     this.document = document;
        !           410:     this.selection = document.getWindow().getSelection();
        !           411:     
        !           412:     this.selectNodeContents = function(node) {
        !           413:         /* select the contents of a node */
        !           414:         this.selection.removeAllRanges();
        !           415:         this.selection.selectAllChildren(node);
        !           416:     };
        !           417: 
        !           418:     this.collapse = function(collapseToEnd) {
        !           419:         try {
        !           420:             if (!collapseToEnd) {
        !           421:                 this.selection.collapseToStart();
        !           422:             } else {
        !           423:                 this.selection.collapseToEnd();
        !           424:             };
        !           425:         } catch(e) {};
        !           426:     };
        !           427: 
        !           428:     this.replaceWithNode = function(node, selectAfterPlace) {
        !           429:         // XXX this should be on a range object
        !           430:         /* replaces the current selection with a new node
        !           431:             returns a reference to the inserted node 
        !           432: 
        !           433:             newnode is the node to replace the content with, selectAfterPlace
        !           434:             can either be a DOM node that should be selected after the new
        !           435:             node was placed, or some value that resolves to true to select
        !           436:             the placed node
        !           437:         */
        !           438:         // get the first range of the selection
        !           439:         // (there's almost always only one range)
        !           440:         var range = this.selection.getRangeAt(0);
        !           441: 
        !           442:         // deselect everything
        !           443:         this.selection.removeAllRanges();
        !           444: 
        !           445:         // remove content of current selection from document
        !           446:         range.deleteContents();
        !           447: 
        !           448:         // get location of current selection
        !           449:         var container = range.startContainer;
        !           450:         var pos = range.startOffset;
        !           451: 
        !           452:         // make a new range for the new selection
        !           453:         var range = this.document.getDocument().createRange();
        !           454: 
        !           455:         if (container.nodeType == 3 && node.nodeType == 3) {
        !           456:             // if we insert text in a textnode, do optimized insertion
        !           457:             container.insertData(pos, node.nodeValue);
        !           458: 
        !           459:             // put cursor after inserted text
        !           460:             range.setEnd(container, pos + node.length);
        !           461:             range.setStart(container, pos + node.length);
        !           462:         } else {
        !           463:             var afterNode;
        !           464:             if (container.nodeType == 3) {
        !           465:                 // when inserting into a textnode
        !           466:                 // we create 2 new textnodes
        !           467:                 // and put the node in between
        !           468: 
        !           469:                 var textNode = container;
        !           470:                 var container = textNode.parentNode;
        !           471:                 var text = textNode.nodeValue;
        !           472: 
        !           473:                 // text before the split
        !           474:                 var textBefore = text.substr(0,pos);
        !           475:                 // text after the split
        !           476:                 var textAfter = text.substr(pos);
        !           477: 
        !           478:                 var beforeNode = this.document.getDocument().createTextNode(textBefore);
        !           479:                 afterNode = this.document.getDocument().createTextNode(textAfter);
        !           480: 
        !           481:                 // insert the 3 new nodes before the old one
        !           482:                 container.insertBefore(afterNode, textNode);
        !           483:                 container.insertBefore(node, afterNode);
        !           484:                 container.insertBefore(beforeNode, node);
        !           485: 
        !           486:                 // remove the old node
        !           487:                 container.removeChild(textNode);
        !           488:             } else {
        !           489:                 // else simply insert the node
        !           490:                 afterNode = container.childNodes[pos];
        !           491:                 if (afterNode) {
        !           492:                     container.insertBefore(node, afterNode);
        !           493:                 } else {
        !           494:                     container.appendChild(node);
        !           495:                 };
        !           496:             }
        !           497: 
        !           498:             range.setEnd(afterNode, 0);
        !           499:             range.setStart(afterNode, 0);
        !           500:         }
        !           501: 
        !           502:         if (selectAfterPlace) {
        !           503:             // a bit implicit here, but I needed this to be backward 
        !           504:             // compatible and also I didn't want yet another argument,
        !           505:             // JavaScript isn't as nice as Python in that respect (kwargs)
        !           506:             // if selectAfterPlace is a DOM node, select all of that node's
        !           507:             // contents, else select the newly added node's
        !           508:             this.selection = this.document.getWindow().getSelection();
        !           509:             this.selection.addRange(range);
        !           510:             if (selectAfterPlace.nodeType == 1) {
        !           511:                 this.selection.selectAllChildren(selectAfterPlace);
        !           512:             } else {
        !           513:                 if (node.hasChildNodes()) {
        !           514:                     this.selection.selectAllChildren(node);
        !           515:                 } else {
        !           516:                     var range = this.selection.getRangeAt(0).cloneRange();
        !           517:                     this.selection.removeAllRanges();
        !           518:                     range.selectNode(node);
        !           519:                     this.selection.addRange(range);
        !           520:                 };
        !           521:             };
        !           522:             this.document.getWindow().focus();
        !           523:         };
        !           524:         return node;
        !           525:     };
        !           526: 
        !           527:     this.startOffset = function() {
        !           528:         // XXX this should be on a range object
        !           529:         var startnode = this.startNode();
        !           530:         var startnodeoffset = 0;
        !           531:         if (startnode == this.selection.anchorNode) {
        !           532:             startnodeoffset = this.selection.anchorOffset;
        !           533:         } else {
        !           534:             startnodeoffset = this.selection.focusOffset;
        !           535:         };
        !           536:         var parentnode = this.parentElement();
        !           537:         if (startnode == parentnode) {
        !           538:             return startnodeoffset;
        !           539:         };
        !           540:         var currnode = parentnode.firstChild;
        !           541:         var offset = 0;
        !           542:         if (!currnode) {
        !           543:             // 'Control range', range consists of a single element, so startOffset is 0
        !           544:             if (startnodeoffset != 0) {
        !           545:                 // just an assertion to see if my assumption about this case is right
        !           546:                 throw(_('Start node offset detected in a node without children!'));
        !           547:             };
        !           548:             return 0;
        !           549:         };
        !           550:         while (currnode != startnode) {
        !           551:             if (currnode.nodeType == 3) {
        !           552:                 offset += currnode.nodeValue.length;
        !           553:             };
        !           554:             currnode = currnode.nextSibling;
        !           555:         };
        !           556:         return offset + startnodeoffset;
        !           557:     };
        !           558: 
        !           559:     this.startNode = function() {
        !           560:         // XXX this should be on a range object
        !           561:         var anode = this.selection.anchorNode;
        !           562:         var aoffset = this.selection.anchorOffset;
        !           563:         var onode = this.selection.focusNode;
        !           564:         var ooffset = this.selection.focusOffset;
        !           565:         var arange = this.document.getDocument().createRange();
        !           566:         arange.setStart(anode, aoffset);
        !           567:         var orange = this.document.getDocument().createRange();
        !           568:         orange.setStart(onode, ooffset);
        !           569:         return arange.compareBoundaryPoints('START_TO_START', orange) <= 0 ? anode : onode;
        !           570:     };
        !           571: 
        !           572:     this.endOffset = function() {
        !           573:         // XXX this should be on a range object
        !           574:         var endnode = this.endNode();
        !           575:         var endnodeoffset = 0;
        !           576:         if (endnode = this.selection.focusNode) {
        !           577:             endnodeoffset = this.selection.focusOffset;
        !           578:         } else {
        !           579:             endnodeoffset = this.selection.anchorOffset;
        !           580:         };
        !           581:         var parentnode = this.parentElement();
        !           582:         var currnode = parentnode.firstChild;
        !           583:         var offset = 0;
        !           584:         if (parentnode == endnode) {
        !           585:             for (var i=0; i < parentnode.childNodes.length; i++) {
        !           586:                 var child = parentnode.childNodes[i];
        !           587:                 if (i == endnodeoffset) {
        !           588:                     return offset;
        !           589:                 };
        !           590:                 if (child.nodeType == 3) {
        !           591:                     offset += child.nodeValue.length;
        !           592:                 };
        !           593:             };
        !           594:         };
        !           595:         if (!currnode) {
        !           596:             // node doesn't have any content, so offset is always 0
        !           597:             if (endnodeoffset != 0) {
        !           598:                 // just an assertion to see if my assumption about this case is right
        !           599:                 var msg = _('End node offset detected in a node without ' +
        !           600:                             'children!');
        !           601:                 alert(msg);
        !           602:                 throw(msg);
        !           603:             };
        !           604:             return 0;
        !           605:         };
        !           606:         while (currnode != endnode) {
        !           607:             if (currnode.nodeType == 3) { // should account for CDATA nodes as well
        !           608:                 offset += currnode.nodeValue.length;
        !           609:             };
        !           610:             currnode = currnode.nextSibling;
        !           611:         };
        !           612:         return offset + endnodeoffset;
        !           613:     };
        !           614: 
        !           615:     this.endNode = function() {
        !           616:         // XXX this should be on a range object
        !           617:         var anode = this.selection.anchorNode;
        !           618:         var aoffset = this.selection.anchorOffset;
        !           619:         var onode = this.selection.focusNode;
        !           620:         var ooffset = this.selection.focusOffset;
        !           621:         var arange = this.document.getDocument().createRange();
        !           622:         arange.setStart(anode, aoffset);
        !           623:         var orange = this.document.getDocument().createRange();
        !           624:         orange.setStart(onode, ooffset);
        !           625:         return arange.compareBoundaryPoints('START_TO_START', orange) > 0 ? anode : onode;
        !           626:     };
        !           627: 
        !           628:     this.getContentLength = function() {
        !           629:         // XXX this should be on a range object
        !           630:         return this.selection.toString().length;
        !           631:     };
        !           632: 
        !           633:     this.cutChunk = function(startOffset, endOffset) {
        !           634:         // XXX this should be on a range object
        !           635:         var range = this.selection.getRangeAt(0);
        !           636:         
        !           637:         // set start point
        !           638:         var offsetParent = this.parentElement();
        !           639:         var currnode = offsetParent.firstChild;
        !           640:         var curroffset = 0;
        !           641: 
        !           642:         var startparent = null;
        !           643:         var startparentoffset = 0;
        !           644:         
        !           645:         while (currnode) {
        !           646:             if (currnode.nodeType == 3) { // XXX need to add CDATA support
        !           647:                 var nodelength = currnode.nodeValue.length;
        !           648:                 if (curroffset + nodelength < startOffset) {
        !           649:                     curroffset += nodelength;
        !           650:                 } else {
        !           651:                     startparent = currnode;
        !           652:                     startparentoffset = startOffset - curroffset;
        !           653:                     break;
        !           654:                 };
        !           655:             };
        !           656:             currnode = currnode.nextSibling;
        !           657:         };
        !           658:         // set end point
        !           659:         var currnode = offsetParent.firstChild;
        !           660:         var curroffset = 0;
        !           661: 
        !           662:         var endparent = null;
        !           663:         var endoffset = 0;
        !           664:         
        !           665:         while (currnode) {
        !           666:             if (currnode.nodeType == 3) { // XXX need to add CDATA support
        !           667:                 var nodelength = currnode.nodeValue.length;
        !           668:                 if (curroffset + nodelength < endOffset) {
        !           669:                     curroffset += nodelength;
        !           670:                 } else {
        !           671:                     endparent = currnode;
        !           672:                     endparentoffset = endOffset - curroffset;
        !           673:                     break;
        !           674:                 };
        !           675:             };
        !           676:             currnode = currnode.nextSibling;
        !           677:         };
        !           678:         
        !           679:         // now cut the chunk
        !           680:         if (!startparent) {
        !           681:             throw(_('Start offset out of range!'));
        !           682:         };
        !           683:         if (!endparent) {
        !           684:             throw(_('End offset out of range!'));
        !           685:         };
        !           686: 
        !           687:         var newrange = range.cloneRange();
        !           688:         newrange.setStart(startparent, startparentoffset);
        !           689:         newrange.setEnd(endparent, endparentoffset);
        !           690:         return newrange.extractContents();
        !           691:     };
        !           692: 
        !           693:     this.getElementLength = function(element) {
        !           694:         // XXX this should be a helper function
        !           695:         var length = 0;
        !           696:         var currnode = element.firstChild;
        !           697:         while (currnode) {
        !           698:             if (currnode.nodeType == 3) { // XXX should support CDATA as well
        !           699:                 length += currnode.nodeValue.length;
        !           700:             };
        !           701:             currnode = currnode.nextSibling;
        !           702:         };
        !           703:         return length;
        !           704:     };
        !           705: 
        !           706:     this.parentElement = function() {
        !           707:         /* return the selected node (or the node containing the selection) */
        !           708:         // XXX this should be on a range object
        !           709:         if (this.selection.rangeCount == 0) {
        !           710:             var parent = this.document.getDocument().body;
        !           711:             while (parent.firstChild) {
        !           712:                 parent = parent.firstChild;
        !           713:             };
        !           714:         } else {
        !           715:             var range = this.selection.getRangeAt(0);
        !           716:             var parent = range.commonAncestorContainer;
        !           717: 
        !           718:             // the following deals with cases where only a single child is
        !           719:             // selected, e.g. after a click on an image
        !           720:             var inv = range.compareBoundaryPoints(Range.START_TO_END, range) < 0;
        !           721:             var startNode = inv ? range.endContainer : range.startContainer;
        !           722:             var startOffset = inv ? range.endOffset : range.startOffset;
        !           723:             var endNode = inv ? range.startContainer : range.endContainer;
        !           724:             var endOffset = inv ? range.startOffset : range.endOffset;
        !           725: 
        !           726:             var selectedChild = null;
        !           727:             var child = parent.firstChild;
        !           728:             while (child) {
        !           729:                 // XXX the additional conditions catch some invisible
        !           730:                 // intersections, but still not all of them
        !           731:                 if (range.intersectsNode(child) &&
        !           732:                     !(child == startNode && startOffset == child.length) &&
        !           733:                     !(child == endNode && endOffset == 0)) {
        !           734:                     if (selectedChild) {
        !           735:                         // current child is the second selected child found
        !           736:                         selectedChild = null;
        !           737:                         break;
        !           738:                     } else {
        !           739:                         // current child is the first selected child found
        !           740:                         selectedChild = child;
        !           741:                     };
        !           742:                 } else if (selectedChild) {
        !           743:                     // current child is after the selection
        !           744:                     break;
        !           745:                 };
        !           746:                 child = child.nextSibling;
        !           747:             };
        !           748:             if (selectedChild) {
        !           749:                 parent = selectedChild;
        !           750:             };
        !           751:         };
        !           752:         if (parent.nodeType == Node.TEXT_NODE) {
        !           753:             parent = parent.parentNode;
        !           754:         };
        !           755:         return parent;
        !           756:     };
        !           757: 
        !           758:     // deprecated alias of parentElement
        !           759:     this.getSelectedNode = this.parentElement;
        !           760: 
        !           761:     this.moveStart = function(offset) {
        !           762:         // XXX this should be on a range object
        !           763:         var offsetparent = this.parentElement();
        !           764:         // the offset within the offsetparent
        !           765:         var startoffset = this.startOffset();
        !           766:         var realoffset = offset + startoffset;
        !           767:         if (realoffset >= 0) {
        !           768:             var currnode = offsetparent.firstChild;
        !           769:             var curroffset = 0;
        !           770:             var startparent = null;
        !           771:             var startoffset = 0;
        !           772:             while (currnode) {
        !           773:                 if (currnode.nodeType == 3) { // XXX need to support CDATA sections
        !           774:                     var nodelength = currnode.nodeValue.length;
        !           775:                     if (curroffset + nodelength >= realoffset) {
        !           776:                         var range = this.selection.getRangeAt(0);
        !           777:                         //range.setEnd(this.endNode(), this.endOffset());
        !           778:                         range.setStart(currnode, realoffset - curroffset);
        !           779:                         return;
        !           780:                         //this.selection.removeAllRanges();
        !           781:                         //this.selection.addRange(range);
        !           782:                     };
        !           783:                 };
        !           784:                 currnode = currnode.nextSibling;
        !           785:             };
        !           786:             // if we still haven't found the startparent we should walk to 
        !           787:             // all nodes following offsetparent as well
        !           788:             var currnode = offsetparent.nextSibling;
        !           789:             while (currnode) {
        !           790:                 if (currnode.nodeType == 3) {
        !           791:                     var nodelength = currnode.nodeValue.length;
        !           792:                     if (curroffset + nodelength >= realoffset) {
        !           793:                         var range = this.selection.getRangeAt(0);
        !           794:                         // XXX does IE switch the begin and end nodes here as well?
        !           795:                         var endnode = this.endNode();
        !           796:                         var endoffset = this.endOffset();
        !           797:                         range.setEnd(currnode, realoffset - curroffset);
        !           798:                         range.setStart(endnode, endoffset);
        !           799:                         return;
        !           800:                     };
        !           801:                     curroffset += nodelength;
        !           802:                 };
        !           803:                 currnode = currnode.nextSibling;
        !           804:             };
        !           805:             throw(_('Offset out of document range'));
        !           806:         } else if (realoffset < 0) {
        !           807:             var currnode = offsetparent.prevSibling;
        !           808:             var curroffset = 0;
        !           809:             while (currnode) {
        !           810:                 if (currnode.nodeType == 3) { // XXX need to support CDATA sections
        !           811:                     var currlength = currnode.nodeValue.length;
        !           812:                     if (curroffset - currlength < realoffset) {
        !           813:                         var range = this.selection.getRangeAt(0);
        !           814:                         range.setStart(currnode, realoffset - curroffset);
        !           815:                     };
        !           816:                     curroffset -= currlength;
        !           817:                 };
        !           818:                 currnode = currnode.prevSibling;
        !           819:             };
        !           820:         } else {
        !           821:             var range = this.selection.getRangeAt(0);
        !           822:             range.setStart(offsetparent, 0);
        !           823:             //this.selection.removeAllRanges();
        !           824:             //this.selection.addRange(range);
        !           825:         };
        !           826:     };
        !           827: 
        !           828:     this.moveEnd = function(offset) {
        !           829:         // XXX this should be on a range object
        !           830:     };
        !           831: 
        !           832:     this.reset = function() {
        !           833:         this.selection = this.document.getWindow().getSelection();
        !           834:     };
        !           835: 
        !           836:     this.cloneContents = function() {
        !           837:         /* returns a document fragment with a copy of the contents */
        !           838:         var range = this.selection.getRangeAt(0);
        !           839:         return range.cloneContents();
        !           840:     };
        !           841: 
        !           842:     this.containsNode = function(node) {
        !           843:         return this.selection.containsNode(node, true);
        !           844:     }
        !           845: 
        !           846:     this.toString = function() {
        !           847:         return this.selection.toString();
        !           848:     };
        !           849: 
        !           850:     this.getRange = function() {
        !           851:         return this.selection.getRangeAt(0);
        !           852:     }
        !           853:     this.restoreRange = function(range) {
        !           854:         var selection = this.selection;
        !           855:         selection.removeAllRanges();
        !           856:         selection.addRange(range);
        !           857:     }
        !           858: };
        !           859: 
        !           860: MozillaSelection.prototype = new BaseSelection;
        !           861: 
        !           862: function IESelection(document) {
        !           863:     this.document = document;
        !           864:     this.selection = document.getDocument().selection;
        !           865: 
        !           866:     /* If no selection in editable document, IE returns selection from
        !           867:      * main page, so force an inner selection. */
        !           868:     var doc = document.getDocument();
        !           869: 
        !           870:     var range = this.selection.createRange()
        !           871:     var parent = this.selection.type=="Text" ?
        !           872:         range.parentElement() :
        !           873:         this.selection.type=="Control" ?  range.parentElement : null;
        !           874: 
        !           875:     if(parent && parent.ownerDocument != doc) {
        !           876:             var range = doc.body.createTextRange();
        !           877:             range.collapse();
        !           878:             range.select();
        !           879:     }
        !           880: 
        !           881:     this.selectNodeContents = function(node) {
        !           882:         /* select the contents of a node */
        !           883:         // a bit nasty, when moveToElementText is called it will move the selection start
        !           884:         // to just before the element instead of inside it, and since IE doesn't reserve
        !           885:         // an index for the element itself as well the way to get it inside the element is
        !           886:         // by moving the start one pos and then moving it back (yuck!)
        !           887:         var range = this.selection.createRange().duplicate();
        !           888:         range.moveToElementText(node);
        !           889:         range.moveStart('character', 1);
        !           890:         range.moveStart('character', -1);
        !           891:         range.moveEnd('character', -1);
        !           892:         range.moveEnd('character', 1);
        !           893:         range.select();
        !           894:         this.selection = this.document.getDocument().selection;
        !           895:     };
        !           896: 
        !           897:     this.collapse = function(collapseToEnd) {
        !           898:         var range = this.selection.createRange();
        !           899:         range.collapse(!collapseToEnd);
        !           900:         range.select();
        !           901:         this.selection = document.getDocument().selection;
        !           902:     };
        !           903: 
        !           904:     this.replaceWithNode = function(newnode, selectAfterPlace) {
        !           905:         /* replaces the current selection with a new node
        !           906:             returns a reference to the inserted node 
        !           907: 
        !           908:             newnode is the node to replace the content with, selectAfterPlace
        !           909:             can either be a DOM node that should be selected after the new
        !           910:             node was placed, or some value that resolves to true to select
        !           911:             the placed node
        !           912:         */
        !           913:         if (this.selection.type == 'Control') {
        !           914:             var range = this.selection.createRange();
        !           915:             range.item(0).parentNode.replaceChild(newnode, range.item(0));
        !           916:             for (var i=1; i < range.length; i++) {
        !           917:                 range.item(i).parentNode.removeChild(range[i]);
        !           918:             };
        !           919:             if (selectAfterPlace) {
        !           920:                 var range = this.document.getDocument().body.createTextRange();
        !           921:                 range.moveToElementText(newnode);
        !           922:                 range.select();
        !           923:             };
        !           924:         } else {
        !           925:             var document = this.document.getDocument();
        !           926:             var range = this.selection.createRange();
        !           927: 
        !           928:             range.pasteHTML('<img id="kupu-tempnode">');
        !           929:             tempnode = document.getElementById('kupu-tempnode');
        !           930:             tempnode.replaceNode(newnode);
        !           931: 
        !           932:             if (selectAfterPlace) {
        !           933:                 // see MozillaSelection.replaceWithNode() for some comments about
        !           934:                 // selectAfterPlace
        !           935:                 if (selectAfterPlace.nodeType == Node.ELEMENT_NODE) {
        !           936:                     range.moveToElementText(selectAfterPlace);
        !           937:                 } else {
        !           938:                     range.moveToElementText(newnode);
        !           939:                 };
        !           940:                 range.select();
        !           941:             };
        !           942:         };
        !           943:         this.reset();
        !           944:         return newnode;
        !           945:     };
        !           946: 
        !           947:     this.startOffset = function() {
        !           948:         var startoffset = 0;
        !           949:         var selrange = this.selection.createRange();
        !           950:         var parent = selrange.parentElement();
        !           951:         var elrange = selrange.duplicate();
        !           952:         elrange.moveToElementText(parent);
        !           953:         var tempstart = selrange.duplicate();
        !           954:         while (elrange.compareEndPoints('StartToStart', tempstart) < 0) {
        !           955:             startoffset++;
        !           956:             tempstart.moveStart('character', -1);
        !           957:         };
        !           958: 
        !           959:         return startoffset;
        !           960:     };
        !           961: 
        !           962:     this.endOffset = function() {
        !           963:         var endoffset = 0;
        !           964:         var selrange = this.selection.createRange();
        !           965:         var parent = selrange.parentElement();
        !           966:         var elrange = selrange.duplicate();
        !           967:         elrange.moveToElementText(parent);
        !           968:         var tempend = selrange.duplicate();
        !           969:         while (elrange.compareEndPoints('EndToEnd', tempend) > 0) {
        !           970:             endoffset++;
        !           971:             tempend.moveEnd('character', 1);
        !           972:         };
        !           973: 
        !           974:         return endoffset;
        !           975:     };
        !           976: 
        !           977:     this.getContentLength = function() {
        !           978:         if (this.selection.type == 'Control') {
        !           979:             return this.selection.createRange().length;
        !           980:         };
        !           981:         var contentlength = 0;
        !           982:         var range = this.selection.createRange();
        !           983:         var endrange = range.duplicate();
        !           984:         while (range.compareEndPoints('StartToEnd', endrange) < 0) {
        !           985:             range.move('character', 1);
        !           986:             contentlength++;
        !           987:         };
        !           988:         return contentlength;
        !           989:     };
        !           990: 
        !           991:     this.cutChunk = function(startOffset, endOffset) {
        !           992:         /* cut a chunk of HTML from the selection
        !           993: 
        !           994:             this *should* return the chunk of HTML but doesn't yet
        !           995:         */
        !           996:         var range = this.selection.createRange().duplicate();
        !           997:         range.moveStart('character', startOffset);
        !           998:         range.moveEnd('character', -endOffset);
        !           999:         range.pasteHTML('');
        !          1000:         // XXX here it should return the chunk
        !          1001:     };
        !          1002: 
        !          1003:     this.getElementLength = function(element) {
        !          1004:         /* returns the length of an element *including* 1 char for each child element
        !          1005: 
        !          1006:             this is defined on the selection since it returns results that can be used
        !          1007:             to work with selection offsets
        !          1008:         */
        !          1009:         var length = 0;
        !          1010:         var range = this.selection.createRange().duplicate();
        !          1011:         range.moveToElementText(element);
        !          1012:         range.moveStart('character', 1);
        !          1013:         range.moveEnd('character', -1);
        !          1014:         var endpoint = range.duplicate();
        !          1015:         endpoint.collapse(false);
        !          1016:         range.collapse();
        !          1017:         while (!range.isEqual(endpoint)) {
        !          1018:             range.moveEnd('character', 1);
        !          1019:             range.moveStart('character', 1);
        !          1020:             length++;
        !          1021:         };
        !          1022:         return length;
        !          1023:     };
        !          1024: 
        !          1025:     this.parentElement = function() {
        !          1026:         /* return the selected node (or the node containing the selection) */
        !          1027:         // XXX this should be on a range object
        !          1028:         if (this.selection.type == 'Control') {
        !          1029:             return this.selection.createRange().item(0);
        !          1030:         } else {
        !          1031:             return this.selection.createRange().parentElement();
        !          1032:         };
        !          1033:     };
        !          1034: 
        !          1035:     // deprecated alias of parentElement
        !          1036:     this.getSelectedNode = this.parentElement;
        !          1037: 
        !          1038:     this.moveStart = function(offset) {
        !          1039:         /* move the start of the selection */
        !          1040:         var range = this.selection.createRange();
        !          1041:         range.moveStart('character', offset);
        !          1042:         range.select();
        !          1043:     };
        !          1044: 
        !          1045:     this.moveEnd = function(offset) {
        !          1046:         /* moves the end of the selection */
        !          1047:         var range = this.selection.createRange();
        !          1048:         range.moveEnd('character', offset);
        !          1049:         range.select();
        !          1050:     };
        !          1051: 
        !          1052:     this.reset = function() {
        !          1053:        this.selection = this.document.getDocument().selection;
        !          1054:     };
        !          1055: 
        !          1056:     this.cloneContents = function() {
        !          1057:         /* returns a document fragment with a copy of the contents */
        !          1058:         var contents = this.selection.createRange().htmlText;
        !          1059:         var doc = this.document.getDocument();
        !          1060:         var docfrag = doc.createElement('span');
        !          1061:         docfrag.innerHTML = contents;
        !          1062:         return docfrag;
        !          1063:     };
        !          1064: 
        !          1065:     this.containsNode = function(node) {
        !          1066:         var selected = this.selection.createRange();
        !          1067:         
        !          1068:         if (this.selection.type.toLowerCase()=='text') {
        !          1069:             var range = doc.body.createTextRange();
        !          1070:             range.moveToElementText(node);
        !          1071: 
        !          1072:             if (selected.compareEndPoints('StartToEnd', range) >= 0 ||
        !          1073:                 selected.compareEndPoints('EndToStart', range) <= 0) {
        !          1074:                 return false;
        !          1075:             }
        !          1076:             return true;
        !          1077:         } else {
        !          1078:             for (var i = 0; i < selected.length; i++) {
        !          1079:                 if (selected.item(i).contains(node)) {
        !          1080:                     return true;
        !          1081:                 }
        !          1082:             }
        !          1083:             return false;
        !          1084:         }
        !          1085:     };
        !          1086:     
        !          1087:     this.getRange = function() {
        !          1088:         return this.selection.createRange();
        !          1089:     }
        !          1090: 
        !          1091:     this.restoreRange = function(range) {
        !          1092:         try {
        !          1093:             range.select();
        !          1094:         } catch(e) {
        !          1095:         };
        !          1096:     }
        !          1097: 
        !          1098:     this.toString = function() {
        !          1099:         return this.selection.createRange().text;
        !          1100:     };
        !          1101: };
        !          1102: 
        !          1103: IESelection.prototype = new BaseSelection;
        !          1104: 
        !          1105: /* ContextFixer, fixes a problem with the prototype based model
        !          1106: 
        !          1107:     When a method is called in certain particular ways, for instance
        !          1108:     when it is used as an event handler, the context for the method
        !          1109:     is changed, so 'this' inside the method doesn't refer to the object
        !          1110:     on which the method is defined (or to which it is attached), but for
        !          1111:     instance to the element on which the method was bound to as an event
        !          1112:     handler. This class can be used to wrap such a method, the wrapper 
        !          1113:     has one method that can be used as the event handler instead. The
        !          1114:     constructor expects at least 2 arguments, first is a reference to the
        !          1115:     method, second the context (a reference to the object) and optionally
        !          1116:     it can cope with extra arguments, they will be passed to the method
        !          1117:     as arguments when it is called (which is a nice bonus of using 
        !          1118:     this wrapper).
        !          1119: */
        !          1120: 
        !          1121: function ContextFixer(func, context) {
        !          1122:     /* Make sure 'this' inside a method points to its class */
        !          1123:     this.func = func;
        !          1124:     this.context = context;
        !          1125:     this.args = arguments;
        !          1126:     var self = this;
        !          1127:     
        !          1128:     this.execute = function() {
        !          1129:         /* execute the method */
        !          1130:         var args = new Array();
        !          1131:         // the first arguments will be the extra ones of the class
        !          1132:         for (var i=0; i < self.args.length - 2; i++) {
        !          1133:             args.push(self.args[i + 2]);
        !          1134:         };
        !          1135:         // the last are the ones passed on to the execute method
        !          1136:         for (var i=0; i < arguments.length; i++) {
        !          1137:             args.push(arguments[i]);
        !          1138:         };
        !          1139:         return self.func.apply(self.context, args);
        !          1140:     };
        !          1141: 
        !          1142: };
        !          1143: 
        !          1144: /* Alternative implementation of window.setTimeout
        !          1145: 
        !          1146:     This is a singleton class, the name of the single instance of the
        !          1147:     object is 'timer_instance', which has one public method called
        !          1148:     registerFunction. This method takes at least 2 arguments: a
        !          1149:     reference to the function (or method) to be called and the timeout.
        !          1150:     Arguments to the function are optional arguments to the 
        !          1151:     registerFunction method. Example:
        !          1152: 
        !          1153:     timer_instance.registerMethod(foo, 100, 'bar', 'baz');
        !          1154: 
        !          1155:     will call the function 'foo' with the arguments 'bar' and 'baz' with
        !          1156:     a timeout of 100 milliseconds.
        !          1157: 
        !          1158:     Since the method doesn't expect a string but a reference to a function
        !          1159:     and since it can handle arguments that are resolved within the current
        !          1160:     namespace rather then in the global namespace, the method can be used
        !          1161:     to call methods on objects from within the object (so this.foo calls
        !          1162:     this.foo instead of failing to find this inside the global namespace)
        !          1163:     and since the arguments aren't strings which are resolved in the global
        !          1164:     namespace the arguments work as expected even inside objects.
        !          1165: 
        !          1166: */
        !          1167: 
        !          1168: function Timer() {
        !          1169:     /* class that has a method to replace window.setTimeout */
        !          1170:     this.lastid = 0;
        !          1171:     this.functions = {};
        !          1172:     
        !          1173:     this.registerFunction = function(object, func, timeout) {
        !          1174:         /* register a function to be called with a timeout
        !          1175: 
        !          1176:             args: 
        !          1177:                 func - the function
        !          1178:                 timeout - timeout in millisecs
        !          1179:                 
        !          1180:             all other args will be passed 1:1 to the function when called
        !          1181:         */
        !          1182:         var args = new Array();
        !          1183:         for (var i=0; i < arguments.length - 3; i++) {
        !          1184:             args.push(arguments[i + 3]);
        !          1185:         }
        !          1186:         var id = this._createUniqueId();
        !          1187:         this.functions[id] = new Array(object, func, args);
        !          1188:         setTimeout("timer_instance._handleFunction(" + id + ")", timeout);
        !          1189:     };
        !          1190: 
        !          1191:     this._handleFunction = function(id) {
        !          1192:         /* private method that does the actual function call */
        !          1193:         var obj = this.functions[id][0];
        !          1194:         var func = this.functions[id][1];
        !          1195:         var args = this.functions[id][2];
        !          1196:         this.functions[id] = null;
        !          1197:         func.apply(obj, args);
        !          1198:     };
        !          1199: 
        !          1200:     this._createUniqueId = function() {
        !          1201:         /* create a unique id to store the function by */
        !          1202:         while (this.lastid in this.functions && this.functions[this.lastid]) {
        !          1203:             this.lastid++;
        !          1204:             if (this.lastid > 100000) {
        !          1205:                 this.lastid = 0;
        !          1206:             }
        !          1207:         }
        !          1208:         return this.lastid;
        !          1209:     };
        !          1210: };
        !          1211: 
        !          1212: // create a timer instance in the global namespace, obviously this does some
        !          1213: // polluting but I guess it's impossible to avoid...
        !          1214: 
        !          1215: // OBVIOUSLY THIS VARIABLE SHOULD NEVER BE OVERWRITTEN!!!
        !          1216: timer_instance = new Timer();
        !          1217: 
        !          1218: // helper function on the Array object to test for containment
        !          1219: Array.prototype.contains = function(element, objectequality) {
        !          1220:     /* see if some value is in this */
        !          1221:     for (var i=0; i < this.length; i++) {
        !          1222:         if (objectequality) {
        !          1223:             if (element === this[i]) {
        !          1224:                 return true;
        !          1225:             };
        !          1226:         } else {
        !          1227:             if (element == this[i]) {
        !          1228:                 return true;
        !          1229:             };
        !          1230:         };
        !          1231:     };
        !          1232:     return false;
        !          1233: };
        !          1234: 
        !          1235: // return a copy of an array with doubles removed
        !          1236: Array.prototype.removeDoubles = function() {
        !          1237:     var ret = [];
        !          1238:     for (var i=0; i < this.length; i++) {
        !          1239:         if (!ret.contains(this[i])) {
        !          1240:             ret.push(this[i]);
        !          1241:         };
        !          1242:     };
        !          1243:     return ret;
        !          1244: };
        !          1245: 
        !          1246: Array.prototype.map = function(func) {
        !          1247:     /* apply 'func' to each element in the array */
        !          1248:     for (var i=0; i < this.length; i++) {
        !          1249:         this[i] = func(this[i]);
        !          1250:     };
        !          1251: };
        !          1252: 
        !          1253: Array.prototype.reversed = function() {
        !          1254:     var ret = [];
        !          1255:     for (var i = this.length; i > 0; i--) {
        !          1256:         ret.push(this[i - 1]);
        !          1257:     };
        !          1258:     return ret;
        !          1259: };
        !          1260: 
        !          1261: // JavaScript has a friggin' blink() function, but not for string stripping...
        !          1262: String.prototype.strip = function() {
        !          1263:     var stripspace = /^\s*([\s\S]*?)\s*$/;
        !          1264:     return stripspace.exec(this)[1];
        !          1265: };
        !          1266: 
        !          1267: String.prototype.reduceWhitespace = function() {
        !          1268:     /* returns a string in which all whitespace is reduced 
        !          1269:         to a single, plain space */
        !          1270:     var spacereg = /(\s+)/g;
        !          1271:     var copy = this;
        !          1272:     while (true) {
        !          1273:         var match = spacereg.exec(copy);
        !          1274:         if (!match) {
        !          1275:             return copy;
        !          1276:         };
        !          1277:         copy = copy.replace(match[0], ' ');
        !          1278:     };
        !          1279: };
        !          1280: 
        !          1281: String.prototype.entitize = function() {
        !          1282:     var ret = this.replace(/&/g, '&amp;');
        !          1283:     ret = ret.replace(/"/g, '&quot;');
        !          1284:     ret = ret.replace(/</g, '&lt;');
        !          1285:     ret = ret.replace(/>/g, '&gt;');
        !          1286:     return ret;
        !          1287: };
        !          1288: 
        !          1289: String.prototype.deentitize = function() {
        !          1290:     var ret = this.replace(/&gt;/g, '>');
        !          1291:     ret = ret.replace(/&lt;/g, '<');
        !          1292:     ret = ret.replace(/&quot;/g, '"');
        !          1293:     ret = ret.replace(/&amp;/g, '&');
        !          1294:     return ret;
        !          1295: };
        !          1296: 
        !          1297: String.prototype.urldecode = function() {
        !          1298:     var reg = /%([a-fA-F0-9]{2})/g;
        !          1299:     var str = this;
        !          1300:     while (true) {
        !          1301:         var match = reg.exec(str);
        !          1302:         if (!match || !match.length) {
        !          1303:             break;
        !          1304:         };
        !          1305:         var repl = new RegExp(match[0], 'g');
        !          1306:         str = str.replace(repl, String.fromCharCode(parseInt(match[1], 16)));
        !          1307:     };
        !          1308:     return str;
        !          1309: };
        !          1310: 
        !          1311: String.prototype.centerTruncate = function(maxlength) {
        !          1312:     if (this.length <= maxlength) {
        !          1313:         return this;
        !          1314:     };
        !          1315:     var chunklength = maxlength / 2 - 3;
        !          1316:     var start = this.substr(0, chunklength);
        !          1317:     var end = this.substr(this.length - chunklength);
        !          1318:     return start + ' ... ' + end;
        !          1319: };
        !          1320: 
        !          1321: //----------------------------------------------------------------------------
        !          1322: // Exceptions
        !          1323: //----------------------------------------------------------------------------
        !          1324: 
        !          1325: function debug(str, win) {
        !          1326:     if (!win) {
        !          1327:         win = window;
        !          1328:     };
        !          1329:     var doc = win.document;
        !          1330:     var div = doc.createElement('div');
        !          1331:     div.appendChild(doc.createTextNode(str));
        !          1332:     doc.getElementsByTagName('body')[0].appendChild(div);
        !          1333: };
        !          1334: 
        !          1335: // XXX don't know if this is the regular way to define exceptions in JavaScript?
        !          1336: function Exception() {
        !          1337:     return;
        !          1338: };
        !          1339: 
        !          1340: // throw this as an exception inside an updateState handler to restart the
        !          1341: // update, may be required in situations where updateState changes the structure
        !          1342: // of the document (e.g. does a cleanup or so)
        !          1343: UpdateStateCancelBubble = new Exception();

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>