zotero/chrome/content/zotero/xpcom/annotate.js
2007-02-02 10:52:47 +00:00

986 lines
31 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright (c) 2006 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://chnm.gmu.edu
Licensed under the Educational Community License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.opensource.org/licenses/ecl1.php
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
***** END LICENSE BLOCK *****
*/
//////////////////////////////////////////////////////////////////////////////
//
// Zotero.Annotate
//
//////////////////////////////////////////////////////////////////////////////
// general purpose annotation/highlighting methods
Zotero.Annotate = new function() {
this.annotationColor = "#fff580";
this.annotationBarColor = "#c0b860";
this.annotationBorderColor = "#878244";
this.highlightColor = "#fff580";
this.getPathForPoint = getPathForPoint;
this.getPointForPath = getPointForPath;
this.getPixelOffset = getPixelOffset;
var textType = Components.interfaces.nsIDOMNode.TEXT_NODE;
/*
* gets a path object, comprising an XPath, text node index, and offset, for
* a given node.
*/
function getPathForPoint(node, offset) {
Zotero.debug("have node of offset "+offset);
var path = {parent:"", textNode:null, offset:(offset ? offset : null)};
var lastWasTextNode = node.nodeType == textType;
if(node.parentNode.getAttribute && node.parentNode.getAttribute("zotero")) {
// if the selected point is inside a highlight node, add offsets of
// preceding text nodes in this zotero node
var sibling = node.previousSibling;
while(sibling) {
if(sibling.nodeType == textType) path.offset += sibling.nodeValue.length;
sibling = sibling.previousSibling;
}
// use parent node for future purposes
node = node.parentNode;
} else if(node.getAttribute && node.getAttribute("zotero")) {
// if selected point is a zotero node, move it to the last character
// of the previous node
node = node.previousSibling;
if(node.nodeType == textType) {
offset = node.nodeValue.length;
} else {
offset = 0;
}
}
if(lastWasTextNode) {
path.textNode = 1;
var sibling = node.previousSibling;
var first = true;
while(sibling) {
var isZotero = undefined;
if(sibling.getAttribute) isZotero = sibling.getAttribute("zotero");
if(sibling.nodeType == textType ||
(isZotero == "highlight")) {
// is a text node
if(first == true) {
// is still part of the first text node
if(sibling.getAttribute) {
// get offset of all child nodes
for each(var child in sibling.childNodes) {
if(child.nodeType == textType) path.offset += child.nodeValue.length;
}
} else {
path.offset += sibling.nodeValue.length;
}
} else if(!lastWasTextNode) {
// is part of another text node
path.textNode++;
lastWasTextNode = true;
}
} else if(!isZotero) { // skip over annotation marker nodes
// is not a text node
lastWasTextNode = first = false;
}
sibling = sibling.previousSibling;
}
node = node.parentNode;
}
var doc = node.ownerDocument;
while(node && node != doc) {
var number = 1;
var sibling = node.previousSibling;
while(sibling) {
if(sibling.tagName == node.tagName) number++;
sibling = sibling.previousSibling;
}
// don't add highlight nodes
var tag = node.tagName.toLowerCase();
if(tag == "span") {
tag += "[not(@zotero)]";
}
path.parent = "/"+tag+"["+number+"]"+path.parent;
node = node.parentNode;
}
Zotero.debug("Annotate: got path "+path.parent+", "+path.textNode+", "+path.offset);
return path;
}
function getPointForPath(parent, textNode, offset, document, nsResolver) {
var point = {offset:0};
// try to evaluate parent
try {
point.node = document.evaluate(parent, document, nsResolver,
Components.interfaces.nsIDOMXPathResult.ANY_TYPE, null).iterateNext();
} catch(e) {
Zotero.debug("Annotate: could not find XPath "+parent+" in getPointForPath");
return false;
}
// don't do further processing if this path does not refer to a text node
if(!textNode) return point;
// parent node must have children if we have a text node index
if(!point.node.firstChild) {
Zotero.debug("Annotate: node "+parent+" has no children in getPointForPath");
return false;
}
point.node = point.node.firstChild;
point.offset = offset;
var lastWasTextNode = false;
var number = 0;
// find text node
while(true) {
var isZotero = undefined;
if(point.node.getAttribute) isZotero = point.node.getAttribute("zotero");
if(point.node.nodeType == textType ||
isZotero == "highlight") {
if(!lastWasTextNode) {
number++;
// if we found the node we're looking for, break
if(number == textNode) break;
lastWasTextNode = true;
}
} else if(!isZotero) {
lastWasTextNode = false;
}
point.node = point.node.nextSibling;
// if there's no node, this point is invalid
if(!point.node) {
Zotero.debug("Annotate: reached end of node list while searching for text node "+textNode+" of "+parent);
return false;
}
}
// find point.offset
while(true) {
// get length of enclosed text node
if(point.node.getAttribute) {
// this is a highlighted node; loop through and subtract all
// offsets, breaking if we reach the end
var parentNode = point.node;
point.node = point.node.firstChild;
while(point.node) {
if(point.node.nodeType == textType) {
// break if end condition reached
if(point.node.nodeValue.length >= point.offset) return point;
// otherwise, continue subtracting offsets
point.offset -= point.node.nodeValue.length;
}
point.node = point.node.nextSibling;
}
// restore parent node
point.node = parentNode;
} else {
// this is not a highlighted node; use simple node length
if(point.node.nodeValue.length >= point.offset) return point;
point.offset -= point.node.nodeValue.length;
}
// get next node
point.node = point.node.nextSibling;
// if next node does not exist or is not a text node, this
// point is invalid
if(!point.node || (point.node.nodeType != textType &&
(!point.node.getAttribute || !point.node.getAttribute("zotero")))) {
Zotero.debug("Annotate: could not find point.offset "+point.offset+" for text node "+textNode+" of "+parent);
return false;
}
}
}
/*
* gets the pixel offset of an item from the top left of a page. the
* optional "offset" argument specifies a text offset.
*/
function getPixelOffset(node, offset) {
var x = 0;
var y = 0;
do {
x += node.offsetLeft;
y += node.offsetTop;
node = node.offsetParent;
} while(node);
return [x, y];
}
}
//////////////////////////////////////////////////////////////////////////////
//
// Zotero.Annotations
//
//////////////////////////////////////////////////////////////////////////////
// a set of annotations to correspond to a given page
Zotero.Annotations = function(browser, itemID) {
this.browser = browser;
this.document = browser.contentDocument;
this.window = browser.contentWindow;
this.nsResolver = this.document.createNSResolver(this.document.documentElement);
this.itemID = itemID;
this.annotations = new Array();
this.highlights = new Array();
this.zIndex = 100;
this.load();
}
Zotero.Annotations.prototype.createAnnotation = function() {
var annotation = new Zotero.Annotation(this);
this.annotations.push(annotation);
return annotation;
}
Zotero.Annotations.prototype.createHighlight = function(selectedRange) {
var deleteHighlights = new Array();
var startIn = false, endIn = false;
// first, see if part of this range is already covered
for(var i in this.highlights) {
var compareHighlight = this.highlights[i];
var compareRange = compareHighlight.range;
var startToStart = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.START_TO_START, selectedRange);
var endToEnd = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.END_TO_END, selectedRange);
if(startToStart != 1 && endToEnd != -1) {
// if the selected range is inside this one
return compareHighlight;
} else if(startToStart != -1 && endToEnd != 1) {
// if this range is inside selected range, delete
this.highlights[i] = undefined;
delete this.highlights[i];
} else {
var endToStart = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.END_TO_START, selectedRange);
if(endToStart != 1 && endToEnd != -1) {
// if the end of the selected range is between the start and
// end of this range
var endIn = i;
} else {
var startToEnd = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.START_TO_END, selectedRange);
if(startToEnd != -1 && startToStart != 1) {
// if the start of the selected range is between the
// start and end of this range
var startIn = i;
}
}
}
}
if(startIn !== false && endIn !== false) {
selectedRange.setStart(this.highlights[startIn].range.endContainer,
this.highlights[startIn].range.endOffset);
selectedRange.setEnd(this.highlights[endIn].range.endContainer,
this.highlights[endIn].range.endOffset);
this.highlights[startIn].initWithRange(selectedRange);
// delete end range
this.highlights[endIn] = undefined;
delete this.highlights[endIn];
return startIn;
} else if(startIn !== false) {
selectedRange.setStart(this.highlights[startIn].range.startContainer,
this.highlights[startIn].range.startOffset);
this.highlights[startIn].initWithRange(selectedRange);
return this.highlights[startIn];
} else if(endIn != false) {
selectedRange.setEnd(this.highlights[endIn].range.endContainer,
this.highlights[endIn].range.endOffset);
this.highlights[endIn].initWithRange(selectedRange);
return this.highlights[endIn];
}
var highlight = new Zotero.Highlight(this);
highlight.initWithRange(selectedRange);
this.highlights.push(highlight);
return highlight;
}
Zotero.Annotations.prototype.unhighlight = function(selectedRange) {
// first, see if part of this range is already covered
for(var i in this.highlights) {
var compareHighlight = this.highlights[i];
var compareRange = compareHighlight.range;
var startToStart = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.START_TO_START, selectedRange);
var endToEnd = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.END_TO_END, selectedRange);
var done = false;
if(startToStart == -1 && endToEnd == 1) {
Zotero.debug("checkpoint 1");
// there's a bug in Mozilla's handling of ranges
var selectStartPoint = Zotero.Annotate.getPathForPoint(selectedRange.startContainer, selectedRange.startOffset);
var compareStartPoint = Zotero.Annotate.getPathForPoint(compareRange.startContainer, compareRange.startOffset);
if(selectStartPoint.parent == compareStartPoint.parent &&
selectStartPoint.textNode == compareStartPoint.textNode &&
selectStartPoint.offset == compareStartPoint.offset) {
startToStart = 0;
} else {
var selectEndPoint = Zotero.Annotate.getPathForPoint(selectedRange.endContainer, selectedRange.endOffset);
var compareEndPoint = Zotero.Annotate.getPathForPoint(compareRange.endContainer, compareRange.endOffset);
if(selectEndPoint.parent == compareEndPoint.parent &&
selectEndPoint.textNode == compareEndPoint.textNode &&
selectEndPoint.offset == compareEndPoint.offset) {
endToEnd = 0;
} else {
// this will unhighlight the entire end
compareHighlight.unhighlight(selectedRange.startContainer, selectedRange.startOffset, 2);
// need to use point references because they disregard highlights
var newRange = this.document.createRange();
var startPoint = Zotero.Annotate.getPointForPath(selectEndPoint.parent, selectEndPoint.textNode, selectEndPoint.offset,
this.document, this.nsResolver);
var endPoint = Zotero.Annotate.getPointForPath(compareEndPoint.parent, compareEndPoint.textNode, compareEndPoint.offset,
this.document, this.nsResolver);
newRange.setStart(startPoint.node, startPoint.offset);
newRange.setEnd(endPoint.node, endPoint.offset);
// create new node
var highlight = new Zotero.Highlight(this);
highlight.initWithRange(newRange);
this.highlights.push(highlight);
done = true;
}
}
}
if(!done) {
if(startToStart != -1 && endToEnd != 1) {
Zotero.debug("checkpoint 2");
// if this range is inside selected range, delete
compareHighlight.unhighlight(null, null, 0);
this.highlights[i] = undefined;
delete this.highlights[i];
} else {
var endToStart = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.END_TO_START, selectedRange);
if(endToStart != 1 && endToEnd != -1) {
Zotero.debug("checkpoint 3");
// if the end of the selected range is between the start and
// end of this range
//compareRange.setStart(selectedRange.endContainer, selectedRange.endOffset);
compareHighlight.unhighlight(selectedRange.endContainer, selectedRange.endOffset, 1);
} else {
var startToEnd = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.START_TO_END, selectedRange);
if(startToEnd != -1 && startToStart != 1) {
Zotero.debug("checkpoint 4");
// if the start of the selected range is between the
// start and end of this range
//compareRange.setEnd(selectedRange.startContainer, selectedRange.startOffset);
compareHighlight.unhighlight(selectedRange.startContainer, selectedRange.startOffset, 2);
}
}
}
}
}
}
Zotero.Annotations.prototype.refresh = function() {
for each(var annotation in this.annotations) {
annotation.display();
}
}
Zotero.Annotations.prototype.save = function() {
Zotero.DB.beginTransaction();
try {
Zotero.DB.query("DELETE FROM highlights WHERE itemID = ?", [this.itemID]);
// save highlights
for each(var highlight in this.highlights) {
if(highlight) highlight.save();
}
// save annotations
for each(var annotation in this.annotations) {
annotation.save();
}
Zotero.DB.commitTransaction();
} catch(e) {
Zotero.DB.rollbackTransaction();
}
}
Zotero.Annotations.prototype.load = function() {
// load annotations
var rows = Zotero.DB.query("SELECT * FROM annotations WHERE itemID = ?", [this.itemID]);
for each(var row in rows) {
var annotation = this.createAnnotation();
annotation.initWithDBRow(row);
}
// load highlights
var rows = Zotero.DB.query("SELECT * FROM highlights WHERE itemID = ?", [this.itemID]);
for each(var row in rows) {
var highlight = new Zotero.Highlight(this);
highlight.initWithDBRow(row);
this.highlights.push(highlight);
}
}
//////////////////////////////////////////////////////////////////////////////
//
// Zotero.Annotation
//
//////////////////////////////////////////////////////////////////////////////
// an annotation (usually generated using Zotero.Annotations.createAnnotation())
Zotero.Annotation = function(annotationsObj) {
this.annotationsObj = annotationsObj;
this.window = annotationsObj.browser.contentWindow;
this.document = annotationsObj.browser.contentDocument;
this.nsResolver = annotationsObj.nsResolver;
}
Zotero.Annotation.prototype.initWithEvent = function(e) {
var maxOffset = false;
try {
var range = this.window.getSelection().getRangeAt(0);
this.node = range.startContainer;
var offset = range.startOffset;
if(this.node.nodeValue) maxOffset = this.node.nodeValue.length;
} catch(err) {
this.node = e.target;
var offset = 0;
}
var clickX = this.window.pageXOffset + e.clientX;
var clickY = this.window.pageYOffset + e.clientY;
var isTextNode = (this.node.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE);
if(offset == 0 || !isTextNode) {
// tag by this.offset from parent this.node, rather than text
if(isTextNode) this.node = this.node.parentNode;
offset = 0;
}
if(offset) this._generateMarker(offset);
var pixelOffset = Zotero.Annotate.getPixelOffset(this.node);
this.x = clickX - pixelOffset[0];
this.y = clickY - pixelOffset[1];
this.editable = true;
Zotero.debug("Annotate: added new annotation");
this.displayWithAbsoluteCoordinates(clickX, clickY);
}
Zotero.Annotation.prototype.initWithDBRow = function(row) {
var point = Zotero.Annotate.getPointForPath(row.parent, row.textNode,
row.offset, this.document, this.nsResolver);
if(!point) {
Zotero.debug("Annotate: could not load annotation "+row.annotationID+" from DB");
return;
}
this.node = point.node;
if(point.offset) this._generateMarker(point.offset);
this.x = row.x;
this.y = row.y;
this.annotationID = row.annotationID;
this.editable = true;
this.display();
this.textarea.value = row.text;
}
Zotero.Annotation.prototype.save = function() {
var text = this.textarea.value;
if(this.annotationID) {
// already in the DB; all we need to do is update the text
var query = "UPDATE annotations SET text = ? WHERE annotationID = ?";
var parameters = [
text,
this.annotationID
];
} else {
// fetch marker location
if(this.node.getAttribute && this.node.getAttribute("zotero") == "annotation-marker") {
var node = this.node.previousSibling;
if(node.nodeType != Components.interfaces.nsIDOMNode.TEXT_NODE) {
// someone added a highlight around this annotation
node = node.lastChild;
}
var offset = node.nodeValue.length;
} else {
var node = this.node;
var offset = 0;
}
// fetch path to node
var path = Zotero.Annotate.getPathForPoint(node, offset);
var query = "INSERT INTO annotations VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
var parameters = [
this.annotationsObj.itemID, // itemID
path.parent, // parent
path.textNode, // textNode
path.offset, // offset
this.x, // x
this.y, // y
30, 5, // cols, rows
text // text
];
}
Zotero.DB.query(query, parameters);
}
Zotero.Annotation.prototype.display = function() {
if(!this.node) throw "Annotation not initialized!";
var x = 0, y = 0;
// first fetch the coordinates
var pixelOffset = Zotero.Annotate.getPixelOffset(this.node);
var x = pixelOffset[0] + this.x;
var y = pixelOffset[1] + this.y;
// then display
this.displayWithAbsoluteCoordinates(x, y);
}
Zotero.Annotation.prototype.displayWithAbsoluteCoordinates = function(absX, absY) {
if(!this.node) throw "Annotation not initialized!";
var startScroll = this.window.scrollMaxX;
if(!this.div) {
this.div = this.document.createElement("div");
this.div.setAttribute("zotero", "annotation");
this.document.getElementsByTagName("body")[0].appendChild(this.div);
this.div.style.backgroundColor = Zotero.Annotate.annotationColor;
this.div.style.padding = "0";
this.div.style.display = "block";
this.div.style.position = "absolute";
this.div.style.border = "1px solid";
this.div.style.borderColor = Zotero.Annotate.annotationBorderColor;
this.div.style.MozOpacity = 0.9;
this.div.style.zIndex = this.annotationsObj.zIndex;
var me = this;
this.div.addEventListener("click", function() { me._click() }, false);
this._addChildElements();
}
this.div.style.display = "block";
this.div.style.left = absX+"px";
this.div.style.top = absY+"px";
// move to the left if we're making things scroll
if(absX + this.div.scrollWidth > this.window.innerWidth) {
this.div.style.left = (absX-this.div.scrollWidth)+"px";
}
}
Zotero.Annotation.prototype._generateMarker = function(offset) {
// first, we create a new span at the correct offset in the node
var range = this.document.createRange();
range.setStart(this.node, offset);
range.setEnd(this.node, offset);
// next, we insert a span
this.node = this.document.createElement("span");
this.node.setAttribute("zotero", "annotation-marker");
range.insertNode(this.node);
}
Zotero.Annotation.prototype._addChildElements = function() {
var me = this;
if(this.editable) {
var div = this.document.createElement("div");
div.style.display = "block";
div.style.textAlign = "left";
div.style.backgroundColor = Zotero.Annotate.annotationBarColor;
div.style.paddingRight = "0";
div.style.paddingLeft = div.style.paddingTop = div.style.paddingBottom = "1px";
div.style.borderBottom = "1px solid";
div.style.borderColor = Zotero.Annotate.annotationBorderColor;
var img = this.document.createElement("img");
img.src = "chrome://zotero/skin/annotation-close.png";
img.addEventListener("click", function(event) {
if (me._confirmDelete(event)) {
me._delete()
}
}, false);
div.appendChild(img);
this.textarea = this.document.createElement("textarea");
this.textarea.setAttribute("zotero", "annotation");
this.textarea.setAttribute("cols", "30");
this.textarea.setAttribute("rows", "5");
this.textarea.setAttribute("wrap", "soft");
this.textarea.style.fontFamily = "Arial, Lucida Grande, FreeSans, sans";
this.textarea.style.fontSize = "12px";
this.textarea.style.backgroundColor = Zotero.Annotate.annotationColor;
this.textarea.style.border = "none";
this.textarea.style.margin = "3px";
this.div.appendChild(div);
this.div.appendChild(this.textarea);
var me = this;
}
}
Zotero.Annotation.prototype._click = function() {
this.annotationsObj.zIndex++
this.div.style.zIndex = this.annotationsObj.zIndex;
}
Zotero.Annotation.prototype._confirmDelete = function(event) {
if (event.target.parentNode.nextSibling.value == '' ||
!Zotero.Prefs.get('annotations.warnOnClose')) {
return true;
}
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
var dontShowAgain = { value: false };
var del = promptService.confirmCheck(
this.window,
Zotero.getString('annotations.confirmClose.title'),
Zotero.getString('annotations.confirmClose.body'),
Zotero.getString('general.dontShowWarningAgain'),
dontShowAgain
);
if (dontShowAgain.value) {
Zotero.Prefs.set('annotations.warnOnClose', false);
}
return del;
}
Zotero.Annotation.prototype._delete = function() {
if(this.annotationID) {
Zotero.DB.query("DELETE FROM annotations WHERE annotationID = ?", [this.annotationID]);
}
// hide div
this.div.parentNode.removeChild(this.div);
// delete from list
for(var i in this.annotationsObj.annotations) {
if(this.annotationsObj.annotations[i] == this) {
this.annotationsObj.annotations.splice(i, 1);
}
}
}
//////////////////////////////////////////////////////////////////////////////
//
// Zotero.Highlight
//
//////////////////////////////////////////////////////////////////////////////
// a highlight (usually generated using Zotero.Annotations.createHighlight())
Zotero.Highlight = function(annotationsObj) {
this.annotationsObj = annotationsObj;
this.window = annotationsObj.browser.contentWindow;
this.document = annotationsObj.browser.contentDocument;
this.nsResolver = annotationsObj.nsResolver;
this.spans = new Array();
}
Zotero.Highlight.prototype.initWithDBRow = function(row) {
Zotero.debug(row.startParent);
var start = Zotero.Annotate.getPointForPath(row.startParent, row.startTextNode,
row.startOffset, this.document, this.nsResolver);
var end = Zotero.Annotate.getPointForPath(row.endParent, row.endTextNode,
row.endOffset, this.document, this.nsResolver);
if(!start || !end) {
Zotero.debug("Highlight: could not initialize from DB row");
return false;
}
this.range = this.document.createRange();
this.range.setStart(start.node, start.offset);
this.range.setEnd(end.node, end.offset);
this._highlight();
}
Zotero.Highlight.prototype.initWithRange = function(range) {
this.range = range;
this._highlight();
}
Zotero.Highlight.prototype.save = function(index) {
var textType = Components.interfaces.nsIDOMNode.TEXT_NODE;
var start = Zotero.Annotate.getPathForPoint(this.range.startContainer, this.range.startOffset);
var end = Zotero.Annotate.getPathForPoint(this.range.endContainer, this.range.endOffset);
var query = "INSERT INTO highlights VALUES (NULL, ?, ?, ?, ?, ?, ?, ?)";
var parameters = [
this.annotationsObj.itemID, // itemID
start.parent, // startParent
start.textNode, // startTextNode
start.offset, // startOffset
end.parent, // endParent
end.textNode, // endTextNode
end.offset // endOffset
];
Zotero.DB.query(query, parameters);
}
/**
* Un-highlights a range.
*
* mode can be:
* 0: unhighlight all
* 1: unhighlight from start to point
* 2: unhighlight from point to end
**/
Zotero.Highlight.prototype.unhighlight = function(container, offset, mode) {
var textType = Components.interfaces.nsIDOMNode.TEXT_NODE;
if(mode == 1) {
this.range.setStart(container, offset);
} else if(mode == 2) {
this.range.setEnd(container, offset);
}
for(var i in this.spans) {
var span = this.spans[i];
var parentNode = span.parentNode;
if(mode != 0 && span.isSameNode(container.parentNode) && offset != 0) {
if(mode == 1) {
// split text node
var textNode = container.splitText(offset);
this.range.setStart(textNode, 0);
if(span.nextSibling && span.nextSibling.nodeType == span.lastChild == textType) {
// attach last node to next text node if possible
span.nextSibling.nodeValue = span.lastChild.nodeValue + span.nextSibling.nodeValue;
span.removeChild(span.lastChild);
}
// loop through, removing nodes
var node = span.firstChild;
while(span.firstChild && !span.firstChild.isSameNode(textNode)) {
parentNode.insertBefore(span.removeChild(span.firstChild), span);
}
} else if(mode == 2) {
// split text node
var textNode = container.splitText(offset);
if(span.previousSibling && span.previousSibling.nodeType == span.firstChild == textType) {
// attach last node to next text node if possible
span.previousSibling.nodeValue += span.firstChild.nodeValue;
span.removeChild(span.firstChild);
}
// loop through, removing nodes
var node = textNode;
var child = node;
while(node) {
child = node;
node = node.nextSibling;
span.removeChild(child);
parentNode.insertBefore(child, span.nextSibling);
}
this.range.setEnd(textNode, 0);
}
} else if(mode == 0 || !this.range.isPointInRange(span, 1)) {
Zotero.debug("point is in range");
// attach child nodes before
while(span.hasChildNodes()) {
Zotero.debug("moving "+span.firstChild.textContent);
span.parentNode.insertBefore(span.removeChild(span.firstChild), span);
}
// remove span from DOM
span.parentNode.removeChild(span);
}
parentNode.normalize();
}
}
Zotero.Highlight.prototype._highlight = function() {
var startNode = this.range.startContainer;
var endNode = this.range.endContainer;
var ancestor = this.range.commonAncestorContainer;
var onlyOneNode = startNode.isSameNode(endNode);
if(!onlyOneNode) {
// highlight nodes after start node in the DOM hierarchy not at ancestor level
while(!startNode.parentNode.isSameNode(ancestor)) {
if(startNode.nextSibling) {
this._highlightSpaceBetween(startNode.nextSibling, startNode.parentNode.lastChild);
}
startNode = startNode.parentNode;
}
// highlight nodes after end node in the DOM hierarchy not at ancestor level
while(!endNode.parentNode.isSameNode(ancestor)) {
if(endNode.previousSibling) {
this._highlightSpaceBetween(endNode.parentNode.firstChild, endNode.previousSibling);
}
endNode = endNode.parentNode;
}
// highlight nodes between start node and end node at ancestor level
if(!startNode.isSameNode(endNode.previousSibling)) {
this._highlightSpaceBetween(startNode.nextSibling, endNode.previousSibling);
}
}
// split the end off the existing node
if(this.range.endContainer.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE && this.range.endOffset != 0) {
if(this.range.endOffset != this.range.endContainer.nodeValue) {
var textNode = this.range.endContainer.splitText(this.range.endOffset);
}
if(!onlyOneNode) {
this._highlightTextNode(this.range.endContainer);
}
if(textNode) this.range.setEnd(textNode, 0);
}
// split the start off of the first node
if(this.range.startContainer.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE) {
if(this.range.startOffset == 0) {
var highlightNode = this.range.startContainer;
} else {
var highlightNode = this.range.startContainer.splitText(this.range.startOffset);
}
var span = this._highlightTextNode(highlightNode);
this.range.setStart(span.firstChild, 0);
} else {
this._highlightSpaceBetween(this.range.startContainer, this.range.startContainer);
}
}
Zotero.Highlight.prototype._highlightTextNode = function(textNode) {
var parent = textNode.parentNode;
if(parent.getAttribute("zotero") == "highlight") {
// already highlighted
return parent;
}
var nextSibling = textNode.nextSibling;
if(nextSibling && nextSibling.getAttribute &&
nextSibling.getAttribute("zotero") == "highlight") {
// next node is highlighted
parent.removeChild(textNode);
nextSibling.firstChild.nodeValue = textNode.nodeValue + nextSibling.firstChild.nodeValue;
return nextSibling;
}
var previousSibling = textNode.previousSibling;
if(previousSibling && previousSibling.getAttribute &&
previousSibling.getAttribute("zotero") == "highlight") {
// previous node is highlighted
parent.removeChild(textNode);
previousSibling.firstChild.nodeValue += textNode.nodeValue;
return previousSibling;
}
var span = this.document.createElement("span");
span.setAttribute("zotero", "highlight");
span.style.display = "inline";
span.style.backgroundColor = Zotero.Annotate.highlightColor;
parent.removeChild(textNode);
span.appendChild(textNode);
parent.insertBefore(span, (nextSibling ? nextSibling : null));
this.spans.push(span);
return span;
}
Zotero.Highlight.prototype._highlightSpaceBetween = function(start, end) {
var meaningfulRe = /[^\s\r\n]/;
var node = start;
var text;
while(node) {
// process nodes
if(node.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE) {
var textArray = [node];
} else {
var texts = this.document.evaluate('.//text()', node, this.nsResolver,
Components.interfaces.nsIDOMXPathResult.ANY_TYPE, null);
var textArray = new Array()
while(text = texts.iterateNext()) textArray.push(text);
}
// do this in the middle, after we're finished with node but before we
// add any spans
if(node.isSameNode(end)) {
node = false;
} else {
node = node.nextSibling;
}
for each(var textNode in textArray) {
this._highlightTextNode(textNode);
}
}
}