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