Annotation of kupuMPIWG/common/kupuhelpers.js, revision 1.1.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>