wiki-archive/twiki/pub/TWiki/KupuContrib/_kupu/common/kupuhelpers.js

1344 lines
47 KiB
JavaScript
Raw Normal View History

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