/***************************************************************************** * * Copyright (c) 2003-2005 Kupu Contributors. All rights reserved. * * This software is distributed under the terms of the Kupu * License. See LICENSE.txt for license text. For a list of Kupu * Contributors see CREDITS.txt. * *****************************************************************************/ // $Id: kupuhelpers.js,v 1.1 2005/08/30 17:10:22 dwinter Exp $ /* Some notes about the scripts: - Problem with bound event handlers: When a method on an object is used as an event handler, the method uses its reference to the object it is defined on. The 'this' keyword no longer points to the class, but instead refers to the element on which the event is bound. To overcome this problem, you can wrap the method in a class that holds a reference to the object and have a method on the wrapper that calls the input method in the input object's context. This wrapped method can be used as the event handler. An example: class Foo() { this.foo = function() { // the method used as an event handler // using this here wouldn't work if the method // was passed to addEventListener directly this.baz(); }; this.baz = function() { // some method on the same object }; }; f = new Foo(); // create the wrapper for the function, args are func, context wrapper = new ContextFixer(f.foo, f); // the wrapper can be passed to addEventListener, 'this' in the method // will be pointing to the right context. some_element.addEventListener("click", wrapper.execute, false); - Problem with window.setTimeout: The window.setTimeout function has a couple of problems in usage, all caused by the fact that it expects a *string* argument that will be evalled in the global namespace rather than a function reference with plain variables as arguments. This makes that the methods on 'this' can not be called (the 'this' variable doesn't exist in the global namespace) and references to variables in the argument list aren't allowed (since they don't exist in the global namespace). To overcome these problems, there's now a singleton instance of a class called Timer, which has one public method called registerFunction. This can be called with a function reference and a variable number of extra arguments to pass on to the function. Usage: timer_instance.registerFunction(this, this.myFunc, 10, 'foo', bar); will call this.myFunc('foo', bar); in 10 milliseconds (with 'this' as its context). */ //---------------------------------------------------------------------------- // Helper classes and functions //---------------------------------------------------------------------------- function addEventHandler(element, event, method, context) { /* method to add an event handler for both IE and Mozilla */ var wrappedmethod = new ContextFixer(method, context); var args = new Array(null, null); for (var i=4; i < arguments.length; i++) { args.push(arguments[i]); }; wrappedmethod.args = args; try { if (_SARISSA_IS_MOZ) { element.addEventListener(event, wrappedmethod.execute, false); } else if (_SARISSA_IS_IE) { element.attachEvent("on" + event, wrappedmethod.execute); } else { throw _("Unsupported browser!"); }; return wrappedmethod.execute; } catch(e) { alert(_('exception ${message} while registering an event handler ' + 'for element ${element}, event ${event}, method ${method}', {'message': e.message, 'element': element, 'event': event, 'method': method})); }; }; function removeEventHandler(element, event, method) { /* method to remove an event handler for both IE and Mozilla */ if (_SARISSA_IS_MOZ) { window.removeEventListener(event, method, false); } else if (_SARISSA_IS_IE) { element.detachEvent("on" + event, method); } else { throw _("Unsupported browser!"); }; }; /* Replacement for window.document.getElementById() * selector can be an Id (so we maintain backwards compatability) * but is intended to be a subset of valid CSS selectors. * For now we only support the format: "#id tag.class" */ function getFromSelector(selector) { var match = /#(\S+)\s*([^ .]+)\.(\S+)/.exec(selector); if (!match) { return window.document.getElementById(selector); } var id=match[1], tag=match[2], className=match[3]; var base = window.document.getElementById(id); return getBaseTagClass(base, tag, className); } function getBaseTagClass(base, tag, className) { var classPat = new RegExp('\\b'+className+'\\b'); var nodes = base.getElementsByTagName(tag); for (var i = 0; i < nodes.length; i++) { if (classPat.test(nodes[i].className)) { return nodes[i]; } } return null; } function openPopup(url, width, height) { /* open and center a popup window */ var sw = screen.width; var sh = screen.height; var left = sw / 2 - width / 2; var top = sh / 2 - height / 2; var win = window.open(url, 'someWindow', 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top); return win; }; function selectSelectItem(select, item) { /* select a certain item from a select */ for (var i=0; i < select.options.length; i++) { var option = select.options[i]; if (option.value == item) { select.selectedIndex = i; return; } } select.selectedIndex = 0; }; function ParentWithStyleChecker(tagnames, style, stylevalue, command) { /* small wrapper that provides a generic function to check if a button should look pressed in */ return function(selNode, button, editor, event) { /* check if the button needs to look pressed in */ if (command) { var result = editor.getInnerDocument().queryCommandState(command) if (result || editor.getSelection().getContentLength() == 0) { return result; }; }; var currnode = selNode; while (currnode && currnode.style) { for (var i=0; i < tagnames.length; i++) { if (currnode.nodeName.toLowerCase() == tagnames[i].toLowerCase()) { return true; }; }; if (style && currnode.style[style] == stylevalue) { return true; }; currnode = currnode.parentNode; }; return false; }; }; function _load_dict_helper(element) { /* walks through a set of XML nodes and builds a nested tree of objects */ var dict = {}; for (var i=0; i < element.childNodes.length; i++) { var child = element.childNodes[i]; if (child.nodeType == 1) { var value = ''; for (var j=0; j < child.childNodes.length; j++) { // test if we can recurse, if so ditch the string (probably // ignorable whitespace) and dive into the node if (child.childNodes[j].nodeType == 1) { value = _load_dict_helper(child); break; } else if (typeof(value) == typeof('')) { value += child.childNodes[j].nodeValue; }; }; if (typeof(value) == typeof('') && !isNaN(parseInt(value)) && parseInt(value).toString().length == value.length) { value = parseInt(value); } else if (typeof(value) != typeof('')) { if (value.length == 1) { value = value[0]; }; }; var name = child.nodeName.toLowerCase(); if (dict[name] != undefined) { if (!dict[name].push) { dict[name] = new Array(dict[name], value); } else { dict[name].push(value); }; } else { dict[name] = value; }; }; }; return dict; }; function loadDictFromXML(document, islandid) { /* load configuration values from an XML chunk this is quite generic, it just reads data from a chunk of XML into an object, checking if the object is complete should be done in the calling context. */ var dict = {}; var confnode = getFromSelector(islandid); var root = null; for (var i=0; i < confnode.childNodes.length; i++) { if (confnode.childNodes[i].nodeType == 1) { root = confnode.childNodes[i]; break; }; }; if (!root) { throw(_('No element found in the config island!')); }; dict = _load_dict_helper(root); return dict; }; function NodeIterator(node, continueatnextsibling) { /* simple node iterator can be used to recursively walk through all children of a node, the next() method will return the next node until either the next sibling of the startnode is reached (when continueatnextsibling is false, the default) or when there's no node left (when continueatnextsibling is true) returns false if no nodes are left */ this.node = node; this.current = node; this.terminator = continueatnextsibling ? null : node; this.next = function() { /* return the next node */ if (this.current === false) { // restart this.current = this.node; }; var current = this.current; if (current.firstChild) { this.current = current.firstChild; } else { // walk up parents until we finish or find one with a nextSibling while (current != this.terminator && !current.nextSibling) { current = current.parentNode; }; if (current == this.terminator) { this.current = false; } else { this.current = current.nextSibling; }; }; return this.current; }; this.reset = function() { /* reset the iterator so it starts at the first node */ this.current = this.node; }; this.setCurrent = function(node) { /* change the current node can be really useful for specific hacks, the user must take care that the node is inside the iterator's scope or it will go wild */ this.current = node; }; }; /* selection classes, these are wrappers around the browser-specific selection objects to provide a generic abstraction layer */ function BaseSelection() { /* superclass for the Selection objects this will contain higher level methods that don't contain browser-specific code */ this.splitNodeAtSelection = function(node) { /* split the node at the current selection remove any selected text, then split the node on the location of the selection, thus creating a new node, this is attached to the node's parent after the node this will fail if the selection is not inside the node */ if (!this.selectionInsideNode(node)) { throw(_('Selection not inside the node!')); }; // a bit sneaky: what we'll do is insert a new br node to replace // the current selection, then we'll walk up to that node in both // the original and the cloned node, in the original we'll remove // the br node and everything that's behind it, on the cloned one // we'll remove the br and everything before it // anyway, we'll end up with 2 nodes, the first already in the // document (the original node) and the second we can just attach // to the doc after the first one var doc = this.document.getDocument(); var br = doc.createElement('br'); br.setAttribute('node_splitter', 'indeed'); this.replaceWithNode(br); var clone = node.cloneNode(true); // now walk through the original node var iterator = new NodeIterator(node); var currnode = iterator.next(); var remove = false; while (currnode) { if (currnode.nodeName.toLowerCase() == 'br' && currnode.getAttribute('node_splitter') == 'indeed') { // here's the point where we should start removing remove = true; }; // we should fetch the next node before we remove the current one, else the iterator // will fail (since the current node is removed) var lastnode = currnode; currnode = iterator.next(); // XXX this will leave nodes that *became* empty in place, since it doesn't visit it again, // perhaps we should do a second pass that removes the rest(?) if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) { lastnode.parentNode.removeChild(lastnode); }; }; // and through the clone var iterator = new NodeIterator(clone); var currnode = iterator.next(); var remove = true; while (currnode) { var lastnode = currnode; currnode = iterator.next(); if (lastnode.nodeName.toLowerCase() == 'br' && lastnode.getAttribute('node_splitter') == 'indeed') { // here's the point where we should stop removing lastnode.parentNode.removeChild(lastnode); remove = false; }; if (remove && (lastnode.nodeType == 3 || !lastnode.hasChildNodes())) { lastnode.parentNode.removeChild(lastnode); }; }; // next we need to attach the node to the document if (node.nextSibling) { node.parentNode.insertBefore(clone, node.nextSibling); } else { node.parentNode.appendChild(clone); }; // this will change the selection, so reselect this.reset(); // return a reference to the clone return clone; }; this.selectionInsideNode = function(node) { /* returns a Boolean to indicate if the selection is resided inside the node */ var currnode = this.parentElement(); while (currnode) { if (currnode == node) { return true; }; currnode = currnode.parentNode; }; return false; }; }; function MozillaSelection(document) { this.document = document; this.selection = document.getWindow().getSelection(); this.selectNodeContents = function(node) { /* select the contents of a node */ this.selection.removeAllRanges(); this.selection.selectAllChildren(node); }; this.collapse = function(collapseToEnd) { try { if (!collapseToEnd) { this.selection.collapseToStart(); } else { this.selection.collapseToEnd(); }; } catch(e) {}; }; this.replaceWithNode = function(node, selectAfterPlace) { // XXX this should be on a range object /* replaces the current selection with a new node returns a reference to the inserted node newnode is the node to replace the content with, selectAfterPlace can either be a DOM node that should be selected after the new node was placed, or some value that resolves to true to select the placed node */ // get the first range of the selection // (there's almost always only one range) var range = this.selection.getRangeAt(0); // deselect everything this.selection.removeAllRanges(); // remove content of current selection from document range.deleteContents(); // get location of current selection var container = range.startContainer; var pos = range.startOffset; // make a new range for the new selection var range = this.document.getDocument().createRange(); if (container.nodeType == 3 && node.nodeType == 3) { // if we insert text in a textnode, do optimized insertion container.insertData(pos, node.nodeValue); // put cursor after inserted text range.setEnd(container, pos + node.length); range.setStart(container, pos + node.length); } else { var afterNode; if (container.nodeType == 3) { // when inserting into a textnode // we create 2 new textnodes // and put the node in between var textNode = container; var container = textNode.parentNode; var text = textNode.nodeValue; // text before the split var textBefore = text.substr(0,pos); // text after the split var textAfter = text.substr(pos); var beforeNode = this.document.getDocument().createTextNode(textBefore); afterNode = this.document.getDocument().createTextNode(textAfter); // insert the 3 new nodes before the old one container.insertBefore(afterNode, textNode); container.insertBefore(node, afterNode); container.insertBefore(beforeNode, node); // remove the old node container.removeChild(textNode); } else { // else simply insert the node afterNode = container.childNodes[pos]; if (afterNode) { container.insertBefore(node, afterNode); } else { container.appendChild(node); }; } range.setEnd(afterNode, 0); range.setStart(afterNode, 0); } if (selectAfterPlace) { // a bit implicit here, but I needed this to be backward // compatible and also I didn't want yet another argument, // JavaScript isn't as nice as Python in that respect (kwargs) // if selectAfterPlace is a DOM node, select all of that node's // contents, else select the newly added node's this.selection = this.document.getWindow().getSelection(); this.selection.addRange(range); if (selectAfterPlace.nodeType == 1) { this.selection.selectAllChildren(selectAfterPlace); } else { if (node.hasChildNodes()) { this.selection.selectAllChildren(node); } else { var range = this.selection.getRangeAt(0).cloneRange(); this.selection.removeAllRanges(); range.selectNode(node); this.selection.addRange(range); }; }; this.document.getWindow().focus(); }; return node; }; this.startOffset = function() { // XXX this should be on a range object var startnode = this.startNode(); var startnodeoffset = 0; if (startnode == this.selection.anchorNode) { startnodeoffset = this.selection.anchorOffset; } else { startnodeoffset = this.selection.focusOffset; }; var parentnode = this.parentElement(); if (startnode == parentnode) { return startnodeoffset; }; var currnode = parentnode.firstChild; var offset = 0; if (!currnode) { // 'Control range', range consists of a single element, so startOffset is 0 if (startnodeoffset != 0) { // just an assertion to see if my assumption about this case is right throw(_('Start node offset detected in a node without children!')); }; return 0; }; while (currnode != startnode) { if (currnode.nodeType == 3) { offset += currnode.nodeValue.length; }; currnode = currnode.nextSibling; }; return offset + startnodeoffset; }; this.startNode = function() { // XXX this should be on a range object var anode = this.selection.anchorNode; var aoffset = this.selection.anchorOffset; var onode = this.selection.focusNode; var ooffset = this.selection.focusOffset; var arange = this.document.getDocument().createRange(); arange.setStart(anode, aoffset); var orange = this.document.getDocument().createRange(); orange.setStart(onode, ooffset); return arange.compareBoundaryPoints('START_TO_START', orange) <= 0 ? anode : onode; }; this.endOffset = function() { // XXX this should be on a range object var endnode = this.endNode(); var endnodeoffset = 0; if (endnode = this.selection.focusNode) { endnodeoffset = this.selection.focusOffset; } else { endnodeoffset = this.selection.anchorOffset; }; var parentnode = this.parentElement(); var currnode = parentnode.firstChild; var offset = 0; if (parentnode == endnode) { for (var i=0; i < parentnode.childNodes.length; i++) { var child = parentnode.childNodes[i]; if (i == endnodeoffset) { return offset; }; if (child.nodeType == 3) { offset += child.nodeValue.length; }; }; }; if (!currnode) { // node doesn't have any content, so offset is always 0 if (endnodeoffset != 0) { // just an assertion to see if my assumption about this case is right var msg = _('End node offset detected in a node without ' + 'children!'); alert(msg); throw(msg); }; return 0; }; while (currnode != endnode) { if (currnode.nodeType == 3) { // should account for CDATA nodes as well offset += currnode.nodeValue.length; }; currnode = currnode.nextSibling; }; return offset + endnodeoffset; }; this.endNode = function() { // XXX this should be on a range object var anode = this.selection.anchorNode; var aoffset = this.selection.anchorOffset; var onode = this.selection.focusNode; var ooffset = this.selection.focusOffset; var arange = this.document.getDocument().createRange(); arange.setStart(anode, aoffset); var orange = this.document.getDocument().createRange(); orange.setStart(onode, ooffset); return arange.compareBoundaryPoints('START_TO_START', orange) > 0 ? anode : onode; }; this.getContentLength = function() { // XXX this should be on a range object return this.selection.toString().length; }; this.cutChunk = function(startOffset, endOffset) { // XXX this should be on a range object var range = this.selection.getRangeAt(0); // set start point var offsetParent = this.parentElement(); var currnode = offsetParent.firstChild; var curroffset = 0; var startparent = null; var startparentoffset = 0; while (currnode) { if (currnode.nodeType == 3) { // XXX need to add CDATA support var nodelength = currnode.nodeValue.length; if (curroffset + nodelength < startOffset) { curroffset += nodelength; } else { startparent = currnode; startparentoffset = startOffset - curroffset; break; }; }; currnode = currnode.nextSibling; }; // set end point var currnode = offsetParent.firstChild; var curroffset = 0; var endparent = null; var endoffset = 0; while (currnode) { if (currnode.nodeType == 3) { // XXX need to add CDATA support var nodelength = currnode.nodeValue.length; if (curroffset + nodelength < endOffset) { curroffset += nodelength; } else { endparent = currnode; endparentoffset = endOffset - curroffset; break; }; }; currnode = currnode.nextSibling; }; // now cut the chunk if (!startparent) { throw(_('Start offset out of range!')); }; if (!endparent) { throw(_('End offset out of range!')); }; var newrange = range.cloneRange(); newrange.setStart(startparent, startparentoffset); newrange.setEnd(endparent, endparentoffset); return newrange.extractContents(); }; this.getElementLength = function(element) { // XXX this should be a helper function var length = 0; var currnode = element.firstChild; while (currnode) { if (currnode.nodeType == 3) { // XXX should support CDATA as well length += currnode.nodeValue.length; }; currnode = currnode.nextSibling; }; return length; }; this.parentElement = function() { /* return the selected node (or the node containing the selection) */ // XXX this should be on a range object if (this.selection.rangeCount == 0) { var parent = this.document.getDocument().body; while (parent.firstChild) { parent = parent.firstChild; }; } else { var range = this.selection.getRangeAt(0); var parent = range.commonAncestorContainer; // the following deals with cases where only a single child is // selected, e.g. after a click on an image var inv = range.compareBoundaryPoints(Range.START_TO_END, range) < 0; var startNode = inv ? range.endContainer : range.startContainer; var startOffset = inv ? range.endOffset : range.startOffset; var endNode = inv ? range.startContainer : range.endContainer; var endOffset = inv ? range.startOffset : range.endOffset; var selectedChild = null; var child = parent.firstChild; while (child) { // XXX the additional conditions catch some invisible // intersections, but still not all of them if (range.intersectsNode(child) && !(child == startNode && startOffset == child.length) && !(child == endNode && endOffset == 0)) { if (selectedChild) { // current child is the second selected child found selectedChild = null; break; } else { // current child is the first selected child found selectedChild = child; }; } else if (selectedChild) { // current child is after the selection break; }; child = child.nextSibling; }; if (selectedChild) { parent = selectedChild; }; }; if (parent.nodeType == Node.TEXT_NODE) { parent = parent.parentNode; }; return parent; }; // deprecated alias of parentElement this.getSelectedNode = this.parentElement; this.moveStart = function(offset) { // XXX this should be on a range object var offsetparent = this.parentElement(); // the offset within the offsetparent var startoffset = this.startOffset(); var realoffset = offset + startoffset; if (realoffset >= 0) { var currnode = offsetparent.firstChild; var curroffset = 0; var startparent = null; var startoffset = 0; while (currnode) { if (currnode.nodeType == 3) { // XXX need to support CDATA sections var nodelength = currnode.nodeValue.length; if (curroffset + nodelength >= realoffset) { var range = this.selection.getRangeAt(0); //range.setEnd(this.endNode(), this.endOffset()); range.setStart(currnode, realoffset - curroffset); return; //this.selection.removeAllRanges(); //this.selection.addRange(range); }; }; currnode = currnode.nextSibling; }; // if we still haven't found the startparent we should walk to // all nodes following offsetparent as well var currnode = offsetparent.nextSibling; while (currnode) { if (currnode.nodeType == 3) { var nodelength = currnode.nodeValue.length; if (curroffset + nodelength >= realoffset) { var range = this.selection.getRangeAt(0); // XXX does IE switch the begin and end nodes here as well? var endnode = this.endNode(); var endoffset = this.endOffset(); range.setEnd(currnode, realoffset - curroffset); range.setStart(endnode, endoffset); return; }; curroffset += nodelength; }; currnode = currnode.nextSibling; }; throw(_('Offset out of document range')); } else if (realoffset < 0) { var currnode = offsetparent.prevSibling; var curroffset = 0; while (currnode) { if (currnode.nodeType == 3) { // XXX need to support CDATA sections var currlength = currnode.nodeValue.length; if (curroffset - currlength < realoffset) { var range = this.selection.getRangeAt(0); range.setStart(currnode, realoffset - curroffset); }; curroffset -= currlength; }; currnode = currnode.prevSibling; }; } else { var range = this.selection.getRangeAt(0); range.setStart(offsetparent, 0); //this.selection.removeAllRanges(); //this.selection.addRange(range); }; }; this.moveEnd = function(offset) { // XXX this should be on a range object }; this.reset = function() { this.selection = this.document.getWindow().getSelection(); }; this.cloneContents = function() { /* returns a document fragment with a copy of the contents */ var range = this.selection.getRangeAt(0); return range.cloneContents(); }; this.containsNode = function(node) { return this.selection.containsNode(node, true); } this.toString = function() { return this.selection.toString(); }; this.getRange = function() { return this.selection.getRangeAt(0); } this.restoreRange = function(range) { var selection = this.selection; selection.removeAllRanges(); selection.addRange(range); } }; MozillaSelection.prototype = new BaseSelection; function IESelection(document) { this.document = document; this.selection = document.getDocument().selection; /* If no selection in editable document, IE returns selection from * main page, so force an inner selection. */ var doc = document.getDocument(); var range = this.selection.createRange() var parent = this.selection.type=="Text" ? range.parentElement() : this.selection.type=="Control" ? range.parentElement : null; if(parent && parent.ownerDocument != doc) { var range = doc.body.createTextRange(); range.collapse(); range.select(); } this.selectNodeContents = function(node) { /* select the contents of a node */ // a bit nasty, when moveToElementText is called it will move the selection start // to just before the element instead of inside it, and since IE doesn't reserve // an index for the element itself as well the way to get it inside the element is // by moving the start one pos and then moving it back (yuck!) var range = this.selection.createRange().duplicate(); range.moveToElementText(node); range.moveStart('character', 1); range.moveStart('character', -1); range.moveEnd('character', -1); range.moveEnd('character', 1); range.select(); this.selection = this.document.getDocument().selection; }; this.collapse = function(collapseToEnd) { var range = this.selection.createRange(); range.collapse(!collapseToEnd); range.select(); this.selection = document.getDocument().selection; }; this.replaceWithNode = function(newnode, selectAfterPlace) { /* replaces the current selection with a new node returns a reference to the inserted node newnode is the node to replace the content with, selectAfterPlace can either be a DOM node that should be selected after the new node was placed, or some value that resolves to true to select the placed node */ if (this.selection.type == 'Control') { var range = this.selection.createRange(); range.item(0).parentNode.replaceChild(newnode, range.item(0)); for (var i=1; i < range.length; i++) { range.item(i).parentNode.removeChild(range[i]); }; if (selectAfterPlace) { var range = this.document.getDocument().body.createTextRange(); range.moveToElementText(newnode); range.select(); }; } else { var document = this.document.getDocument(); var range = this.selection.createRange(); range.pasteHTML(''); tempnode = document.getElementById('kupu-tempnode'); tempnode.replaceNode(newnode); if (selectAfterPlace) { // see MozillaSelection.replaceWithNode() for some comments about // selectAfterPlace if (selectAfterPlace.nodeType == Node.ELEMENT_NODE) { range.moveToElementText(selectAfterPlace); } else { range.moveToElementText(newnode); }; range.select(); }; }; this.reset(); return newnode; }; this.startOffset = function() { var startoffset = 0; var selrange = this.selection.createRange(); var parent = selrange.parentElement(); var elrange = selrange.duplicate(); elrange.moveToElementText(parent); var tempstart = selrange.duplicate(); while (elrange.compareEndPoints('StartToStart', tempstart) < 0) { startoffset++; tempstart.moveStart('character', -1); }; return startoffset; }; this.endOffset = function() { var endoffset = 0; var selrange = this.selection.createRange(); var parent = selrange.parentElement(); var elrange = selrange.duplicate(); elrange.moveToElementText(parent); var tempend = selrange.duplicate(); while (elrange.compareEndPoints('EndToEnd', tempend) > 0) { endoffset++; tempend.moveEnd('character', 1); }; return endoffset; }; this.getContentLength = function() { if (this.selection.type == 'Control') { return this.selection.createRange().length; }; var contentlength = 0; var range = this.selection.createRange(); var endrange = range.duplicate(); while (range.compareEndPoints('StartToEnd', endrange) < 0) { range.move('character', 1); contentlength++; }; return contentlength; }; this.cutChunk = function(startOffset, endOffset) { /* cut a chunk of HTML from the selection this *should* return the chunk of HTML but doesn't yet */ var range = this.selection.createRange().duplicate(); range.moveStart('character', startOffset); range.moveEnd('character', -endOffset); range.pasteHTML(''); // XXX here it should return the chunk }; this.getElementLength = function(element) { /* returns the length of an element *including* 1 char for each child element this is defined on the selection since it returns results that can be used to work with selection offsets */ var length = 0; var range = this.selection.createRange().duplicate(); range.moveToElementText(element); range.moveStart('character', 1); range.moveEnd('character', -1); var endpoint = range.duplicate(); endpoint.collapse(false); range.collapse(); while (!range.isEqual(endpoint)) { range.moveEnd('character', 1); range.moveStart('character', 1); length++; }; return length; }; this.parentElement = function() { /* return the selected node (or the node containing the selection) */ // XXX this should be on a range object if (this.selection.type == 'Control') { return this.selection.createRange().item(0); } else { return this.selection.createRange().parentElement(); }; }; // deprecated alias of parentElement this.getSelectedNode = this.parentElement; this.moveStart = function(offset) { /* move the start of the selection */ var range = this.selection.createRange(); range.moveStart('character', offset); range.select(); }; this.moveEnd = function(offset) { /* moves the end of the selection */ var range = this.selection.createRange(); range.moveEnd('character', offset); range.select(); }; this.reset = function() { this.selection = this.document.getDocument().selection; }; this.cloneContents = function() { /* returns a document fragment with a copy of the contents */ var contents = this.selection.createRange().htmlText; var doc = this.document.getDocument(); var docfrag = doc.createElement('span'); docfrag.innerHTML = contents; return docfrag; }; this.containsNode = function(node) { var selected = this.selection.createRange(); if (this.selection.type.toLowerCase()=='text') { var range = doc.body.createTextRange(); range.moveToElementText(node); if (selected.compareEndPoints('StartToEnd', range) >= 0 || selected.compareEndPoints('EndToStart', range) <= 0) { return false; } return true; } else { for (var i = 0; i < selected.length; i++) { if (selected.item(i).contains(node)) { return true; } } return false; } }; this.getRange = function() { return this.selection.createRange(); } this.restoreRange = function(range) { try { range.select(); } catch(e) { }; } this.toString = function() { return this.selection.createRange().text; }; }; IESelection.prototype = new BaseSelection; /* ContextFixer, fixes a problem with the prototype based model When a method is called in certain particular ways, for instance when it is used as an event handler, the context for the method is changed, so 'this' inside the method doesn't refer to the object on which the method is defined (or to which it is attached), but for instance to the element on which the method was bound to as an event handler. This class can be used to wrap such a method, the wrapper has one method that can be used as the event handler instead. The constructor expects at least 2 arguments, first is a reference to the method, second the context (a reference to the object) and optionally it can cope with extra arguments, they will be passed to the method as arguments when it is called (which is a nice bonus of using this wrapper). */ function ContextFixer(func, context) { /* Make sure 'this' inside a method points to its class */ this.func = func; this.context = context; this.args = arguments; var self = this; this.execute = function() { /* execute the method */ var args = new Array(); // the first arguments will be the extra ones of the class for (var i=0; i < self.args.length - 2; i++) { args.push(self.args[i + 2]); }; // the last are the ones passed on to the execute method for (var i=0; i < arguments.length; i++) { args.push(arguments[i]); }; return self.func.apply(self.context, args); }; }; /* Alternative implementation of window.setTimeout This is a singleton class, the name of the single instance of the object is 'timer_instance', which has one public method called registerFunction. This method takes at least 2 arguments: a reference to the function (or method) to be called and the timeout. Arguments to the function are optional arguments to the registerFunction method. Example: timer_instance.registerMethod(foo, 100, 'bar', 'baz'); will call the function 'foo' with the arguments 'bar' and 'baz' with a timeout of 100 milliseconds. Since the method doesn't expect a string but a reference to a function and since it can handle arguments that are resolved within the current namespace rather then in the global namespace, the method can be used to call methods on objects from within the object (so this.foo calls this.foo instead of failing to find this inside the global namespace) and since the arguments aren't strings which are resolved in the global namespace the arguments work as expected even inside objects. */ function Timer() { /* class that has a method to replace window.setTimeout */ this.lastid = 0; this.functions = {}; this.registerFunction = function(object, func, timeout) { /* register a function to be called with a timeout args: func - the function timeout - timeout in millisecs all other args will be passed 1:1 to the function when called */ var args = new Array(); for (var i=0; i < arguments.length - 3; i++) { args.push(arguments[i + 3]); } var id = this._createUniqueId(); this.functions[id] = new Array(object, func, args); setTimeout("timer_instance._handleFunction(" + id + ")", timeout); }; this._handleFunction = function(id) { /* private method that does the actual function call */ var obj = this.functions[id][0]; var func = this.functions[id][1]; var args = this.functions[id][2]; this.functions[id] = null; func.apply(obj, args); }; this._createUniqueId = function() { /* create a unique id to store the function by */ while (this.lastid in this.functions && this.functions[this.lastid]) { this.lastid++; if (this.lastid > 100000) { this.lastid = 0; } } return this.lastid; }; }; // create a timer instance in the global namespace, obviously this does some // polluting but I guess it's impossible to avoid... // OBVIOUSLY THIS VARIABLE SHOULD NEVER BE OVERWRITTEN!!! timer_instance = new Timer(); // helper function on the Array object to test for containment Array.prototype.contains = function(element, objectequality) { /* see if some value is in this */ for (var i=0; i < this.length; i++) { if (objectequality) { if (element === this[i]) { return true; }; } else { if (element == this[i]) { return true; }; }; }; return false; }; // return a copy of an array with doubles removed Array.prototype.removeDoubles = function() { var ret = []; for (var i=0; i < this.length; i++) { if (!ret.contains(this[i])) { ret.push(this[i]); }; }; return ret; }; Array.prototype.map = function(func) { /* apply 'func' to each element in the array */ for (var i=0; i < this.length; i++) { this[i] = func(this[i]); }; }; Array.prototype.reversed = function() { var ret = []; for (var i = this.length; i > 0; i--) { ret.push(this[i - 1]); }; return ret; }; // JavaScript has a friggin' blink() function, but not for string stripping... String.prototype.strip = function() { var stripspace = /^\s*([\s\S]*?)\s*$/; return stripspace.exec(this)[1]; }; String.prototype.reduceWhitespace = function() { /* returns a string in which all whitespace is reduced to a single, plain space */ var spacereg = /(\s+)/g; var copy = this; while (true) { var match = spacereg.exec(copy); if (!match) { return copy; }; copy = copy.replace(match[0], ' '); }; }; String.prototype.entitize = function() { var ret = this.replace(/&/g, '&'); ret = ret.replace(/"/g, '"'); ret = ret.replace(//g, '>'); return ret; }; String.prototype.deentitize = function() { var ret = this.replace(/>/g, '>'); ret = ret.replace(/</g, '<'); ret = ret.replace(/"/g, '"'); ret = ret.replace(/&/g, '&'); return ret; }; String.prototype.urldecode = function() { var reg = /%([a-fA-F0-9]{2})/g; var str = this; while (true) { var match = reg.exec(str); if (!match || !match.length) { break; }; var repl = new RegExp(match[0], 'g'); str = str.replace(repl, String.fromCharCode(parseInt(match[1], 16))); }; return str; }; String.prototype.centerTruncate = function(maxlength) { if (this.length <= maxlength) { return this; }; var chunklength = maxlength / 2 - 3; var start = this.substr(0, chunklength); var end = this.substr(this.length - chunklength); return start + ' ... ' + end; }; //---------------------------------------------------------------------------- // Exceptions //---------------------------------------------------------------------------- function debug(str, win) { if (!win) { win = window; }; var doc = win.document; var div = doc.createElement('div'); div.appendChild(doc.createTextNode(str)); doc.getElementsByTagName('body')[0].appendChild(div); }; // XXX don't know if this is the regular way to define exceptions in JavaScript? function Exception() { return; }; // throw this as an exception inside an updateState handler to restart the // update, may be required in situations where updateState changes the structure // of the document (e.g. does a cleanup or so) UpdateStateCancelBubble = new Exception();