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, '&');
! 1283: ret = ret.replace(/"/g, '"');
! 1284: ret = ret.replace(/</g, '<');
! 1285: ret = ret.replace(/>/g, '>');
! 1286: return ret;
! 1287: };
! 1288:
! 1289: String.prototype.deentitize = function() {
! 1290: var ret = this.replace(/>/g, '>');
! 1291: ret = ret.replace(/</g, '<');
! 1292: ret = ret.replace(/"/g, '"');
! 1293: ret = ret.replace(/&/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>