1634 lines
No EOL
50 KiB
JavaScript
1634 lines
No EOL
50 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2009 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
const TEXT_TYPE = Components.interfaces.nsIDOMNode.TEXT_NODE;
|
|
|
|
/**
|
|
* Globally accessible functions relating to annotations
|
|
* @namespace
|
|
*/
|
|
Zotero.Annotate = new function() {
|
|
var _annotated = {};
|
|
|
|
this.highlightColor = "#fff580";
|
|
this.alternativeHighlightColor = "#555fa9";
|
|
|
|
/**
|
|
* Gets the pixel offset of an item from the top left of a page
|
|
*
|
|
* @param {Node} node DOM node to get the pixel offset of
|
|
* @param {Integer} offset Text offset
|
|
* @return {Integer[]} X and Y coordinates
|
|
*/
|
|
this.getPixelOffset = function(node, offset) {
|
|
var x = 0;
|
|
var y = 0;
|
|
|
|
do {
|
|
x += node.offsetLeft;
|
|
y += node.offsetTop;
|
|
node = node.offsetParent;
|
|
} while(node);
|
|
|
|
return [x, y];
|
|
}
|
|
|
|
/**
|
|
* Gets the annotation ID from a given URL
|
|
*/
|
|
this.getAnnotationIDFromURL = function(url) {
|
|
const attachmentRe = /^zotero:\/\/attachment\/([0-9]+)\/$/;
|
|
var m = attachmentRe.exec(url);
|
|
if (m) {
|
|
var id = m[1];
|
|
var item = Zotero.Items.get(id);
|
|
var contentType = item.attachmentContentType;
|
|
var file = item.getFilePath();
|
|
var ext = Zotero.File.getExtension(file);
|
|
if (contentType == 'text/plain' || !Zotero.MIME.hasNativeHandler(contentType, ext)) {
|
|
return false;
|
|
}
|
|
return id;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parses CSS/HTML color descriptions
|
|
*
|
|
* @return {Integer[]} An array of 3 values from 0 to 255 representing R, G, and B components
|
|
*/
|
|
this.parseColor = function(color) {
|
|
const rgbColorRe = /rgb\(([0-9]+), ?([0-9]+), ?([0-9]+)\)/i;
|
|
|
|
var colorArray = rgbColorRe.exec(color);
|
|
if(colorArray) return [parseInt(colorArray[1]), parseInt(colorArray[2]), parseInt(colorArray[3])];
|
|
|
|
if(color[0] == "#") color = color.substr(1);
|
|
try {
|
|
colorArray = [];
|
|
for(var i=0; i<6; i+=2) {
|
|
colorArray.push(parseInt(color.substr(i, 2), 16));
|
|
}
|
|
return colorArray;
|
|
} catch(e) {
|
|
throw "Annotate: parseColor passed invalid color";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the city block distance between two colors. Accepts colors in the format returned by
|
|
* Zotero.Annotate.parseColor()
|
|
*
|
|
* @param {Integer[]} color1
|
|
* @param {Integer[]} color2
|
|
* @return {Integer} The distance
|
|
*/
|
|
this.getColorDistance = function(color1, color2) {
|
|
color1 = this.parseColor(color1);
|
|
color2 = this.parseColor(color2);
|
|
|
|
var distance = 0;
|
|
for(var i=0; i<3; i++) {
|
|
distance += Math.abs(color1[i] - color2[i]);
|
|
}
|
|
|
|
return distance;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if a given item is already open for annotation
|
|
*
|
|
* @param {Integer} id An item ID
|
|
* @return {Boolean}
|
|
*/
|
|
this.isAnnotated = function(id) {
|
|
const XUL_NAMESPACE = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
var annotationURL = "zotero://attachment/"+id+"/";
|
|
var haveBrowser = false;
|
|
|
|
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Components.interfaces.nsIWindowMediator);
|
|
var enumerator = wm.getEnumerator("navigator:browser");
|
|
while(enumerator.hasMoreElements()) {
|
|
var win = enumerator.getNext();
|
|
var tabbrowser = win.document.getElementsByTagNameNS(XUL_NAMESPACE, "tabbrowser");
|
|
if(tabbrowser && tabbrowser.length) {
|
|
var browsers = tabbrowser[0].browsers;
|
|
} else {
|
|
var browsers = win.document.getElementsByTagNameNS(XUL_NAMESPACE, "browser");
|
|
}
|
|
for (let browser of browsers) {
|
|
if(browser.currentURI) {
|
|
if(browser.currentURI.spec == annotationURL) {
|
|
if(haveBrowser) {
|
|
// require two with this URI
|
|
return true;
|
|
} else {
|
|
haveBrowser = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sometimes, Firefox gives us a node offset inside another node, as opposed to a text offset
|
|
* This function replaces such offsets with references to the nodes themselves
|
|
*
|
|
* @param {Node} node DOM node
|
|
* @param {Integer} offset Node offset
|
|
* @return {Node} The DOM node after dereferencing has taken place
|
|
*/
|
|
this.dereferenceNodeOffset = function(node, offset) {
|
|
if(offset != 0) {
|
|
if(offset == node.childNodes.length) {
|
|
node = node.lastChild;
|
|
} else if(offset < node.childNodes.length) {
|
|
node = node.childNodes[offset];
|
|
} else {
|
|
throw "Annotate: dereferenceNodeOffset called with invalid offset "+offset;
|
|
}
|
|
if(!node) throw "Annotate: dereferenceNodeOffset resolved to invalid node";
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Normalizes a DOM range, resolving it to a range that begins and ends at a text offset and
|
|
* remains unchanged when serialized to a Zotero.Annotate.Path object
|
|
*
|
|
* @param {Range} selectedRange The range to normalize
|
|
* @param {Function} nsResolver Namespace resolver function
|
|
* @return {Zotero.Annotate.Path[]} Start and end paths
|
|
*/
|
|
this.normalizeRange = function(selectedRange, nsResolver) {
|
|
var document = selectedRange.startContainer.ownerDocument;
|
|
|
|
var container, offset;
|
|
if(selectedRange.startContainer.nodeType != TEXT_TYPE) {
|
|
[container, offset] = _getTextNode(selectedRange.startContainer, selectedRange.startOffset, true);
|
|
selectedRange.setStart(container, offset);
|
|
}
|
|
if(selectedRange.endContainer.nodeType != TEXT_TYPE) {
|
|
[container, offset] = _getTextNode(selectedRange.endContainer, selectedRange.endOffset);
|
|
selectedRange.setEnd(container, offset);
|
|
}
|
|
|
|
var startPath = new Zotero.Annotate.Path(document, nsResolver);
|
|
var endPath = new Zotero.Annotate.Path(document, nsResolver);
|
|
startPath.fromNode(selectedRange.startContainer, selectedRange.startOffset);
|
|
endPath.fromNode(selectedRange.endContainer, selectedRange.endOffset);
|
|
|
|
[container, offset] = startPath.toNode();
|
|
selectedRange.setStart(container, offset);
|
|
[container, offset] = endPath.toNode();
|
|
selectedRange.setEnd(container, offset);
|
|
|
|
return [startPath, endPath];
|
|
}
|
|
|
|
/**
|
|
* Takes a node and finds the relevant text node inside of it
|
|
*
|
|
* @private
|
|
* @param {Node} container Node to get text node of
|
|
* @param {Integer} offset Node offset (see dereferenceNodeOffset)
|
|
* @param {Boolean} isStart Whether to treat this node as a start node. We look for the first
|
|
* text node from the start of start nodes, or the first from the end of end nodes
|
|
* @return {Array} The node and offset
|
|
*/
|
|
function _getTextNode(container, offset, isStart) {
|
|
var firstTarget = isStart ? "firstChild" : "lastChild";
|
|
var secondTarget = isStart ? "nextSibling" : "previousSibling";
|
|
|
|
container = Zotero.Annotate.dereferenceNodeOffset(container, offset);
|
|
if(container.nodeType == TEXT_TYPE) return [container, 0];
|
|
|
|
var seenArray = new Array();
|
|
var node = container;
|
|
while(node) {
|
|
if ( !node ) {
|
|
// uh-oh
|
|
break;
|
|
}
|
|
if(node.nodeType == TEXT_TYPE ) {
|
|
container = node;
|
|
break;
|
|
}
|
|
if( node[firstTarget] && ! _seen(node[firstTarget],seenArray)) {
|
|
var node = node[firstTarget];
|
|
} else if( node[secondTarget] && ! _seen(node[secondTarget],seenArray)) {
|
|
var node = node[secondTarget];
|
|
} else {
|
|
var node = node.parentNode;
|
|
}
|
|
}
|
|
return [container, (!isStart && container.nodeType == TEXT_TYPE ? container.nodeValue.length : 0)];
|
|
}
|
|
|
|
/**
|
|
* look for a node object in an array. return true if the node
|
|
* is found in the array. otherwise push the node onto the array
|
|
* and return false. used by _getTextNode.
|
|
*/
|
|
function _seen(node,array) {
|
|
var seen = false;
|
|
for (n in array) {
|
|
if (node === array[n]) {
|
|
var seen = true;
|
|
}
|
|
}
|
|
if ( !seen ) {
|
|
array.push(node);
|
|
}
|
|
return seen;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new Zotero.Annotate.Path object from an XPath, text node index, and text offset
|
|
*
|
|
* @class A persistent descriptor for a point in the DOM, invariant to modifications of
|
|
* the DOM produced by highlights and annotations
|
|
*
|
|
* @property {String} parent XPath of parent node of referenced text node, or XPath of referenced
|
|
* element
|
|
* @property {Integer} textNode Index of referenced text node
|
|
* @property {Integer} offset Offset of referenced point inside text node
|
|
*
|
|
* @constructor
|
|
* @param {Document} document DOM document this path references
|
|
* @param {Function} nsResolver Namespace resolver (for XPaths)
|
|
* @param {String} parent (Optional) XPath of parent node
|
|
* @param {Integer} textNode (Optional) Text node number
|
|
* @param {Integer} offset (Optional) Text offset
|
|
*/
|
|
Zotero.Annotate.Path = function(document, nsResolver, parent, textNode, offset) {
|
|
if(parent !== undefined) {
|
|
this.parent = parent;
|
|
this.textNode = textNode;
|
|
this.offset = offset;
|
|
}
|
|
this._document = document;
|
|
this._nsResolver = nsResolver;
|
|
}
|
|
|
|
/**
|
|
* Converts a DOM node/offset combination to a Zotero.Annotate.Path object
|
|
*
|
|
* @param {Node} node The DOM node to reference
|
|
* @param {Integer} offset The text offset, if the DOM node is a text node
|
|
*/
|
|
Zotero.Annotate.Path.prototype.fromNode = function(node, offset) {
|
|
if(!node) throw "Annotate: Path() called with invalid node";
|
|
Zotero.debug("Annotate: Path() called with node "+node.tagName+" offset "+offset);
|
|
|
|
this.parent = "";
|
|
this.textNode = null;
|
|
this.offset = (offset === 0 || offset ? offset : null);
|
|
|
|
var lastWasTextNode = node.nodeType == TEXT_TYPE;
|
|
|
|
if(!lastWasTextNode && offset) {
|
|
node = Zotero.Annotate.dereferenceNodeOffset(node, offset);
|
|
offset = 0;
|
|
lastWasTextNode = node.nodeType == TEXT_TYPE;
|
|
}
|
|
|
|
if(node.parentNode.getAttribute && node.parentNode.getAttribute("zotero")) {
|
|
// if the selected point is inside a Zotero node node, add offsets of preceding
|
|
// text nodes
|
|
var first = false;
|
|
var sibling = node.previousSibling;
|
|
while(sibling) {
|
|
if(sibling.nodeType == TEXT_TYPE) this.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 last character of the previous node
|
|
node = node.previousSibling ? node.previousSibling : node.parentNode;
|
|
if(node.nodeType == TEXT_TYPE) {
|
|
this.offset = node.nodeValue.length;
|
|
lastWasTextNode = true;
|
|
} else {
|
|
this.offset = 0;
|
|
}
|
|
}
|
|
if(!node) throw "Annotate: Path() handled Zotero <span> inappropriately";
|
|
|
|
lastWasTextNode = lastWasTextNode || node.nodeType == TEXT_TYPE;
|
|
|
|
if(lastWasTextNode) {
|
|
this.textNode = 1;
|
|
var first = true;
|
|
|
|
var sibling = node.previousSibling;
|
|
while(sibling) {
|
|
var isZotero = (sibling.getAttribute ? sibling.getAttribute("zotero") : false);
|
|
|
|
if(sibling.nodeType == TEXT_TYPE ||
|
|
(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 (let child of sibling.childNodes) {
|
|
if(child && child.nodeType == TEXT_TYPE) {
|
|
this.offset += child.nodeValue.length;
|
|
}
|
|
}
|
|
} else {
|
|
this.offset += sibling.nodeValue.length;
|
|
}
|
|
} else if(!lastWasTextNode) {
|
|
// is part of another text node
|
|
this.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;
|
|
}
|
|
if(!node) throw "Annotate: Path() resolved text offset inappropriately";
|
|
|
|
while(node && node !== this._document) {
|
|
var number = 1;
|
|
var sibling = node.previousSibling;
|
|
while(sibling) {
|
|
if(sibling.tagName) {
|
|
if(sibling.tagName == node.tagName && !sibling.hasAttribute("zotero")) number++;
|
|
} else {
|
|
if(sibling.nodeType == node.nodeType) number++;
|
|
}
|
|
sibling = sibling.previousSibling;
|
|
}
|
|
|
|
// don't add highlight nodes
|
|
if(node.tagName) {
|
|
var tag = node.tagName.toLowerCase();
|
|
if(tag == "span") {
|
|
tag += "[not(@zotero)]";
|
|
}
|
|
this.parent = "/"+tag+"["+number+"]"+this.parent;
|
|
} else if(node.nodeType == Components.interfaces.nsIDOMNode.COMMENT_NODE) {
|
|
this.parent = "/comment()["+number+"]";
|
|
} else if(node.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE) {
|
|
Zotero.debug("Annotate: Path() referenced a text node; this should never happen");
|
|
this.parent = "/text()["+number+"]";
|
|
} else {
|
|
Zotero.debug("Annotate: Path() encountered unrecognized node type");
|
|
}
|
|
|
|
node = node.parentNode;
|
|
}
|
|
|
|
Zotero.debug("Annotate: got path "+this.parent+", "+this.textNode+", "+this.offset);
|
|
}
|
|
|
|
/**
|
|
* Converts a Zotero.Annotate.Path object to a DOM/offset combination
|
|
*
|
|
* @return {Array} Node and offset
|
|
*/
|
|
Zotero.Annotate.Path.prototype.toNode = function() {
|
|
Zotero.debug("toNode on "+this.parent+" "+this.textNode+", "+this.offset);
|
|
|
|
var offset = 0;
|
|
|
|
// try to evaluate parent
|
|
try {
|
|
var node = this._document.evaluate(this.parent, this._document, this._nsResolver,
|
|
Components.interfaces.nsIDOMXPathResult.ANY_TYPE, null).iterateNext();
|
|
} catch(e) {
|
|
Zotero.debug("Annotate: could not find XPath "+this.parent+" in Path.toNode()");
|
|
return [false, false];
|
|
}
|
|
|
|
// don't do further processing if this path does not refer to a text node
|
|
if(!this.textNode) return [node, offset];
|
|
|
|
// parent node must have children if we have a text node index
|
|
if(!node.hasChildNodes()) {
|
|
Zotero.debug("Annotate: Parent node has no child nodes, but a text node was specified");
|
|
return [false, false];
|
|
}
|
|
|
|
node = node.firstChild;
|
|
offset = this.offset;
|
|
var lastWasTextNode = false;
|
|
var number = 0;
|
|
|
|
// find text node
|
|
while(true) {
|
|
var isZotero = undefined;
|
|
if(node.getAttribute) isZotero = node.getAttribute("zotero");
|
|
|
|
if(node.nodeType == TEXT_TYPE ||
|
|
isZotero == "highlight") {
|
|
if(!lastWasTextNode) {
|
|
number++;
|
|
|
|
// if we found the node we're looking for, break
|
|
if(number == this.textNode) break;
|
|
|
|
lastWasTextNode = true;
|
|
}
|
|
} else if(!isZotero) {
|
|
lastWasTextNode = false;
|
|
}
|
|
|
|
node = node.nextSibling;
|
|
// if there's no node, this point is invalid
|
|
if(!node) {
|
|
Zotero.debug("Annotate: reached end of node list while searching for text node "+this.textNode+" of "+this.parent);
|
|
return [false, false];
|
|
}
|
|
}
|
|
|
|
// find offset
|
|
while(true) {
|
|
// get length of enclosed text node
|
|
if(node.getAttribute) {
|
|
// this is a highlighted node; loop through and subtract all
|
|
// offsets, breaking if we reach the end
|
|
var parentNode = node;
|
|
node = node.firstChild;
|
|
while(node) {
|
|
if(node.nodeType == TEXT_TYPE) {
|
|
// break if end condition reached
|
|
if(node.nodeValue.length >= offset) return [node, offset];
|
|
// otherwise, continue subtracting offsets
|
|
offset -= node.nodeValue.length;
|
|
}
|
|
node = node.nextSibling;
|
|
}
|
|
// restore parent node
|
|
node = parentNode;
|
|
} else {
|
|
// this is not a highlighted node; use simple node length
|
|
if(node.nodeValue.length >= offset) return [node, offset];
|
|
offset -= node.nodeValue.length;
|
|
}
|
|
|
|
// get next node
|
|
node = node.nextSibling;
|
|
// if next node does not exist or is not a text node, this
|
|
// point is invalid
|
|
if(!node || (node.nodeType != TEXT_TYPE && (!node.getAttribute || !node.getAttribute("zotero")))) {
|
|
Zotero.debug("Annotate: could not find offset "+this.offset+" for text node "+this.textNode+" of "+this.parent);
|
|
return [false, false];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new Zotero.Annotations object
|
|
* @class Manages all annotations and highlights for a given item
|
|
*
|
|
* @constructor
|
|
* @param {Zotero_Browser} Zotero_Browser object for the tab in which this item is loaded
|
|
* @param {Browser} Mozilla Browser object
|
|
* @param {Integer} itemID ID of the item to be annotated/highlighted
|
|
*/
|
|
Zotero.Annotations = function(Zotero_Browser, browser, itemID) {
|
|
this.Zotero_Browser = Zotero_Browser;
|
|
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 = 9999;
|
|
}
|
|
|
|
/**
|
|
* Creates a new annotation at the cursor position
|
|
* @return {Zotero.Annotation}
|
|
*/
|
|
Zotero.Annotations.prototype.createAnnotation = function() {
|
|
var annotation = new Zotero.Annotation(this);
|
|
this.annotations.push(annotation);
|
|
return annotation;
|
|
}
|
|
|
|
/**
|
|
* Highlights text
|
|
*
|
|
* @param {Range} selectedRange Range to highlight
|
|
* @return {Zotero.Highlight}
|
|
*/
|
|
Zotero.Annotations.prototype.highlight = function(selectedRange) {
|
|
var startPath, endPath;
|
|
[startPath, endPath] = Zotero.Annotate.normalizeRange(selectedRange, this.nsResolver);
|
|
|
|
var deleteHighlights = new Array();
|
|
var startIn = false, endIn = false;
|
|
|
|
// first, see if part of this range is already
|
|
for(var i in this.highlights) {
|
|
var compareHighlight = this.highlights[i];
|
|
var compareRange = compareHighlight.getRange();
|
|
|
|
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
|
|
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) {
|
|
// starts in and ends in existing highlights
|
|
if(startIn !== false) {
|
|
var highlight = this.highlights[startIn];
|
|
startRange = highlight.getRange();
|
|
selectedRange.setStart(startRange.startContainer, startRange.startOffset);
|
|
startPath = highlight.startPath;
|
|
} else {
|
|
var highlight = this.highlights[endIn];
|
|
}
|
|
|
|
if(endIn !== false) {
|
|
endRange = this.highlights[endIn].getRange();
|
|
selectedRange.setEnd(endRange.endContainer, endRange.endOffset);
|
|
endPath = this.highlights[endIn].endPath;
|
|
}
|
|
|
|
// if bridging ranges, delete end range
|
|
if(startIn !== false && endIn !== false) {
|
|
delete this.highlights[endIn];
|
|
}
|
|
} else {
|
|
// need to create a new highlight
|
|
var highlight = new Zotero.Highlight(this);
|
|
this.highlights.push(highlight);
|
|
}
|
|
|
|
// actually generate ranges
|
|
highlight.initWithRange(selectedRange, startPath, endPath);
|
|
|
|
//for(var i in this.highlights) Zotero.debug(i+" = "+this.highlights[i].startPath.offset+" to "+this.highlights[i].endPath.offset+" ("+this.highlights[i].startPath.parent+" to "+this.highlights[i].endPath.parent+")");
|
|
return highlight;
|
|
}
|
|
|
|
/**
|
|
* Unhighlights text
|
|
*
|
|
* @param {Range} selectedRange Range to unhighlight
|
|
*/
|
|
Zotero.Annotations.prototype.unhighlight = function(selectedRange) {
|
|
var startPath, endPath, node, offset;
|
|
[startPath, endPath] = Zotero.Annotate.normalizeRange(selectedRange, this.nsResolver);
|
|
|
|
// first, see if part of this range is already highlighted
|
|
for(var i in this.highlights) {
|
|
var updateStart = false;
|
|
var updateEnd = false;
|
|
|
|
var compareHighlight = this.highlights[i];
|
|
var compareRange = compareHighlight.getRange();
|
|
|
|
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) {
|
|
// need to split range into two highlights
|
|
var compareEndPath = compareHighlight.endPath;
|
|
|
|
// this will unhighlight the entire end
|
|
compareHighlight.unhighlight(selectedRange.startContainer, selectedRange.startOffset,
|
|
startPath, Zotero.Highlight.UNHIGHLIGHT_FROM_POINT);
|
|
var newRange = this.document.createRange();
|
|
|
|
// need to use point references because they disregard highlights
|
|
[node, offset] = endPath.toNode();
|
|
newRange.setStart(node, offset);
|
|
[node, offset] = compareEndPath.toNode();
|
|
newRange.setEnd(node, offset);
|
|
|
|
// create new node
|
|
var highlight = new Zotero.Highlight(this);
|
|
highlight.initWithRange(newRange, endPath, compareEndPath);
|
|
this.highlights.push(highlight);
|
|
break;
|
|
} else if(startToStart != -1 && endToEnd != 1) {
|
|
// if this range is inside selected range, delete
|
|
compareHighlight.unhighlight(null, null, null, Zotero.Highlight.UNHIGHLIGHT_ALL);
|
|
delete this.highlights[i];
|
|
updateEnd = updateStart = true;
|
|
} else if(startToStart == -1) {
|
|
var startToEnd = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.START_TO_END, selectedRange);
|
|
if(startToEnd != -1) {
|
|
// if the start of the selected range is between the start and end of this range
|
|
compareHighlight.unhighlight(selectedRange.startContainer, selectedRange.startOffset,
|
|
startPath, Zotero.Highlight.UNHIGHLIGHT_FROM_POINT);
|
|
updateEnd = true;
|
|
}
|
|
} else {
|
|
var endToStart = compareRange.compareBoundaryPoints(Components.interfaces.nsIDOMRange.END_TO_START, selectedRange);
|
|
if(endToStart != 1) {
|
|
// if the end of the selected range is between the start and end of this range
|
|
compareHighlight.unhighlight(selectedRange.endContainer, selectedRange.endOffset,
|
|
endPath, Zotero.Highlight.UNHIGHLIGHT_TO_POINT);
|
|
updateStart = true;
|
|
}
|
|
}
|
|
|
|
// need to update start and end parts of ranges if spans have shifted around
|
|
if(updateStart) {
|
|
[node, offset] = startPath.toNode();
|
|
selectedRange.setStart(node, offset);
|
|
}
|
|
if(updateEnd) {
|
|
[node, offset] = endPath.toNode();
|
|
selectedRange.setEnd(node, offset);
|
|
}
|
|
}
|
|
|
|
//for(var i in this.highlights) Zotero.debug(i+" = "+this.highlights[i].startPath.offset+" to "+this.highlights[i].endPath.offset+" ("+this.highlights[i].startPath.parent+" to "+this.highlights[i].endPath.parent+")");
|
|
}
|
|
|
|
/**
|
|
* Refereshes display of annotations (useful if page is reloaded)
|
|
*/
|
|
Zotero.Annotations.prototype.refresh = function() {
|
|
for (let annotation of this.annotations) {
|
|
annotation.display();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves annotations to DB
|
|
*/
|
|
Zotero.Annotations.prototype.save = function() {
|
|
Zotero.DB.beginTransaction();
|
|
try {
|
|
Zotero.DB.query("DELETE FROM highlights WHERE itemID = ?", [this.itemID]);
|
|
|
|
// save highlights
|
|
for (let highlight of this.highlights) {
|
|
if(highlight) highlight.save();
|
|
}
|
|
|
|
// save annotations
|
|
for (let annotation of this.annotations) {
|
|
// Don't drop all annotations if one is broken (due to ~3.0 glitch)
|
|
try {
|
|
annotation.save();
|
|
}
|
|
catch(e) {
|
|
Zotero.debug(e);
|
|
continue;
|
|
}
|
|
}
|
|
Zotero.DB.commitTransaction();
|
|
} catch(e) {
|
|
Zotero.debug(e);
|
|
Zotero.DB.rollbackTransaction();
|
|
throw(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads annotations from DB
|
|
*/
|
|
Zotero.Annotations.prototype.load = Zotero.Promise.coroutine(function* () {
|
|
// load annotations
|
|
var rows = yield Zotero.DB.queryAsync("SELECT * FROM annotations WHERE itemID = ?", [this.itemID]);
|
|
for (let row of rows) {
|
|
var annotation = this.createAnnotation();
|
|
annotation.initWithDBRow(row);
|
|
}
|
|
|
|
// load highlights
|
|
var rows = yield Zotero.DB.queryAsync("SELECT * FROM highlights WHERE itemID = ?", [this.itemID]);
|
|
for (let row of rows) {
|
|
try {
|
|
var highlight = new Zotero.Highlight(this);
|
|
highlight.initWithDBRow(row);
|
|
this.highlights.push(highlight);
|
|
} catch(e) {
|
|
Zotero.debug("Annotate: could not load highlight");
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Expands annotations if any are collapsed, or collapses highlights if all are expanded
|
|
*/
|
|
Zotero.Annotations.prototype.toggleCollapsed = function() {
|
|
// look to see if there are any collapsed annotations
|
|
var status = true;
|
|
for (let annotation of this.annotations) {
|
|
if(annotation.collapsed) {
|
|
status = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// set status on all annotations
|
|
for (let annotation of this.annotations) {
|
|
annotation.setCollapsed(status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @class Represents an individual annotation
|
|
*
|
|
* @constructor
|
|
* @property {Boolean} collapsed Whether this annotation is collapsed (minimized)
|
|
* @param {Zotero.Annotations} annotationsObj The Zotero.Annotations object corresponding to the
|
|
* page this annotation is on
|
|
*/
|
|
Zotero.Annotation = function(annotationsObj) {
|
|
this.annotationsObj = annotationsObj;
|
|
this.window = annotationsObj.browser.contentWindow;
|
|
this.document = annotationsObj.browser.contentDocument;
|
|
this.nsResolver = annotationsObj.nsResolver;
|
|
this.cols = 30;
|
|
this.rows = 5;
|
|
}
|
|
|
|
/**
|
|
* Generates annotation from a click event
|
|
*
|
|
* @param {Event} e The DOM click event
|
|
*/
|
|
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.collapsed = false;
|
|
|
|
Zotero.debug("Annotate: added new annotation");
|
|
|
|
this.displayWithAbsoluteCoordinates(clickX, clickY, true);
|
|
}
|
|
|
|
/**
|
|
* Generates annotation from a DB row
|
|
*
|
|
* @param {Object} row The DB row
|
|
*/
|
|
Zotero.Annotation.prototype.initWithDBRow = function(row) {
|
|
var path = new Zotero.Annotate.Path(this.document, this.nsResolver, row.parent, row.textNode, row.offset);
|
|
[node, offset] = path.toNode();
|
|
if(!node) {
|
|
Zotero.debug("Annotate: could not load annotation "+row.annotationID+" from DB");
|
|
return;
|
|
}
|
|
this.node = node;
|
|
if(offset) this._generateMarker(offset);
|
|
|
|
this.x = row.x;
|
|
this.y = row.y;
|
|
this.cols = row.cols;
|
|
this.rows = row.rows;
|
|
this.annotationID = row.annotationID;
|
|
this.collapsed = !!row.collapsed;
|
|
|
|
this.display();
|
|
|
|
var me = this;
|
|
this.iframe.addEventListener("load", function() { me.textarea.value = row.text }, false);
|
|
}
|
|
|
|
/**
|
|
* Saves annotation to DB
|
|
*/
|
|
Zotero.Annotation.prototype.save = function() {
|
|
var text = this.textarea.value;
|
|
|
|
// 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 = new Zotero.Annotate.Path(this.document, this.nsResolver);
|
|
path.fromNode(node, offset);
|
|
|
|
var parameters = [
|
|
this.annotationsObj.itemID, // itemID
|
|
path.parent, // parent
|
|
path.textNode, // textNode
|
|
path.offset, // offset
|
|
this.x, // x
|
|
this.y, // y
|
|
this.cols, // cols
|
|
this.rows, // rows
|
|
text, // text
|
|
(this.collapsed ? 1 : 0) // collapsed
|
|
];
|
|
|
|
if(this.annotationID) {
|
|
var query = "INSERT OR REPLACE INTO annotations VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, DATETIME('now'))";
|
|
parameters.unshift(this.annotationID);
|
|
} else {
|
|
var query = "INSERT INTO annotations VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, DATETIME('now'))";
|
|
}
|
|
|
|
Zotero.DB.query(query, parameters);
|
|
}
|
|
|
|
/**
|
|
* Displays annotation
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Displays annotation given absolute coordinates for its position
|
|
*/
|
|
Zotero.Annotation.prototype.displayWithAbsoluteCoordinates = function(absX, absY, select) {
|
|
if(!this.node) throw "Annotation not initialized!";
|
|
|
|
var startScroll = this.window.scrollMaxX;
|
|
|
|
if(!this.iframe) {
|
|
var me = this;
|
|
var body = this.document.getElementsByTagName("body")[0];
|
|
|
|
const style = "position: absolute; margin: 0; padding: 0; border: none; overflow: hidden; ";
|
|
|
|
// generate regular div
|
|
this.iframe = this.document.createElement("iframe");
|
|
this.iframe.setAttribute("zotero", "annotation");
|
|
this.iframe.setAttribute("style", style+" -moz-opacity: 0.9;");
|
|
this.iframe.setAttribute("src", "zotero://attachment/annotation.html");
|
|
body.appendChild(this.iframe);
|
|
this.iframe.addEventListener("load", function() {
|
|
me._addChildElements(select);
|
|
me.iframe.style.display = (me.collapsed ? "none" : "block");
|
|
}, false);
|
|
|
|
// generate pushpin image
|
|
this.pushpinDiv = this.document.createElement("img");
|
|
this.pushpinDiv.setAttribute("style", style+" cursor: pointer;");
|
|
this.pushpinDiv.setAttribute("src", "zotero://attachment/annotation-hidden.gif");
|
|
this.pushpinDiv.setAttribute("title", Zotero.getString("annotations.expand.tooltip"));
|
|
body.appendChild(this.pushpinDiv);
|
|
this.pushpinDiv.style.display = (this.collapsed ? "block" : "none");
|
|
this.pushpinDiv.addEventListener("click", function() { me.setCollapsed(false) }, false);
|
|
}
|
|
this.iframe.style.left = this.pushpinDiv.style.left = absX+"px";
|
|
this.iframeX = absX;
|
|
this.iframe.style.top = this.pushpinDiv.style.top = absY+"px";
|
|
this.iframeY = absY;
|
|
this.pushpinDiv.style.zIndex = this.iframe.style.zIndex = this.annotationsObj.zIndex;
|
|
|
|
// move to the left if we're making things scroll
|
|
if(absX + this.iframe.scrollWidth > this.window.innerWidth) {
|
|
this.iframe.style.left = (absX-this.iframe.scrollWidth)+"px";
|
|
this.iframeX = absX-this.iframe.scrollWidth;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collapses or uncollapses annotation
|
|
*
|
|
* @param {Boolean} status True to collapse, false to uncollapse
|
|
*/
|
|
Zotero.Annotation.prototype.setCollapsed = function(status) {
|
|
if(status == true) { // hide iframe
|
|
this.iframe.style.display = "none";
|
|
this.pushpinDiv.style.display = "block";
|
|
this.collapsed = true;
|
|
} else { // hide pushpin div
|
|
this.pushpinDiv.style.display = "none";
|
|
this.iframe.style.display = "block";
|
|
this.collapsed = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a marker within a paragraph for this annotation. Such markers will remain in place
|
|
* even if the DOM is changed, e.g., by highlighting
|
|
*
|
|
* @param {Integer} offset Text offset within parent node
|
|
* @private
|
|
*/
|
|
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 delete the old node, if there is one
|
|
if(this.node && this.node.getAttribute && this.node.getAttribute("zotero") == "annotation-marker") {
|
|
this.node.parentNode.removeChild(this.node);
|
|
this.node = undefined;
|
|
}
|
|
|
|
// next, we insert a span
|
|
this.node = this.document.createElement("span");
|
|
this.node.setAttribute("zotero", "annotation-marker");
|
|
range.insertNode(this.node);
|
|
}
|
|
|
|
/**
|
|
* Prepare iframe representing this annotation
|
|
*
|
|
* @param {Boolean} select Whether to select the textarea once iframe is prepared
|
|
* @private
|
|
*/
|
|
Zotero.Annotation.prototype._addChildElements = function(select) {
|
|
var me = this;
|
|
this.iframeDoc = this.iframe.contentDocument;
|
|
|
|
// close
|
|
var img = this.iframeDoc.getElementById("close");
|
|
img.title = Zotero.getString("annotations.close.tooltip");
|
|
img.addEventListener("click", function(e) { me._confirmDelete(e) }, false);
|
|
|
|
// move
|
|
this.moveImg = this.iframeDoc.getElementById("move");
|
|
this.moveImg.title = Zotero.getString("annotations.move.tooltip");
|
|
this.moveImg.addEventListener("click", function(e) { me._startMove(e) }, false);
|
|
|
|
// hide
|
|
img = this.iframeDoc.getElementById("collapse");
|
|
img.title = Zotero.getString("annotations.collapse.tooltip");
|
|
img.addEventListener("click", function(e) { me.setCollapsed(true) }, false);
|
|
|
|
// collapse
|
|
this.grippyDiv = this.iframeDoc.getElementById("grippy");
|
|
this.grippyDiv.addEventListener("mousedown", function(e) { me._startDrag(e) }, false);
|
|
|
|
// text area
|
|
this.textarea = this.iframeDoc.getElementById("text");
|
|
this.textarea.setAttribute("zotero", "annotation");
|
|
this.textarea.cols = this.cols;
|
|
this.textarea.rows = this.rows;
|
|
|
|
this.iframe.style.width = (6+this.textarea.offsetWidth)+"px";
|
|
this.iframe.style.height = this.iframeDoc.body.offsetHeight+"px";
|
|
this.iframeDoc.addEventListener("click", function() { me._click() }, false);
|
|
|
|
if(select) this.textarea.select();
|
|
}
|
|
|
|
/**
|
|
* Brings annotation to the foreground
|
|
* @private
|
|
*/
|
|
Zotero.Annotation.prototype._click = function() {
|
|
// clear current action
|
|
this.annotationsObj.Zotero_Browser.toggleMode(null);
|
|
|
|
// alter z-index
|
|
this.annotationsObj.zIndex++
|
|
this.iframe.style.zIndex = this.pushpinDiv.style.zIndex = this.annotationsObj.zIndex;
|
|
}
|
|
|
|
/**
|
|
* Asks user to confirm deletion of this annotation
|
|
* @private
|
|
*/
|
|
Zotero.Annotation.prototype._confirmDelete = function(event) {
|
|
if (this.textarea.value == '' || !Zotero.Prefs.get('annotations.warnOnClose')) {
|
|
var del = true;
|
|
} else {
|
|
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);
|
|
}
|
|
}
|
|
|
|
if(del) this._delete();
|
|
}
|
|
|
|
/**
|
|
* Deletes this annotation
|
|
* @private
|
|
*/
|
|
Zotero.Annotation.prototype._delete = function() {
|
|
if(this.annotationID) {
|
|
Zotero.DB.query("DELETE FROM annotations WHERE annotationID = ?", [this.annotationID]);
|
|
}
|
|
|
|
// hide div
|
|
this.iframe.parentNode.removeChild(this.iframe);
|
|
// delete from list
|
|
for(var i in this.annotationsObj.annotations) {
|
|
if(this.annotationsObj.annotations[i] == this) {
|
|
this.annotationsObj.annotations.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called to begin resizing the annotation
|
|
*
|
|
* @param {Event} e DOM event corresponding to click on the grippy
|
|
* @private
|
|
*/
|
|
Zotero.Annotation.prototype._startDrag = function(e) {
|
|
var me = this;
|
|
|
|
this.clickStartX = e.screenX;
|
|
this.clickStartY = e.screenY;
|
|
this.clickStartCols = this.textarea.cols;
|
|
this.clickStartRows = this.textarea.rows;
|
|
|
|
/**
|
|
* Listener to handle mouse moves
|
|
* @inner
|
|
*/
|
|
var handleDrag = function(e) { me._doDrag(e); };
|
|
this.iframeDoc.addEventListener("mousemove", handleDrag, false);
|
|
this.document.addEventListener("mousemove", handleDrag, false);
|
|
|
|
/**
|
|
* Listener to call when mouse is let up
|
|
* @inner
|
|
*/
|
|
var endDrag = function() {
|
|
me.iframeDoc.removeEventListener("mousemove", handleDrag, false);
|
|
me.document.removeEventListener("mousemove", handleDrag, false);
|
|
me.iframeDoc.removeEventListener("mouseup", endDrag, false);
|
|
me.document.removeEventListener("mouseup", endDrag, false);
|
|
me.dragging = false;
|
|
}
|
|
this.iframeDoc.addEventListener("mouseup", endDrag, false);
|
|
this.document.addEventListener("mouseup", endDrag, false);
|
|
|
|
// stop propagation
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
|
|
/**
|
|
* Called when mouse is moved while annotation is being resized
|
|
*
|
|
* @param {Event} e DOM event corresponding to mouse move
|
|
* @private
|
|
*/
|
|
Zotero.Annotation.prototype._doDrag = function(e) {
|
|
var x = e.screenX - this.clickStartX;
|
|
var y = e.screenY - this.clickStartY;
|
|
|
|
// update sizes
|
|
var colSize = this.textarea.clientWidth/this.textarea.cols;
|
|
var rowSize = this.textarea.clientHeight/this.textarea.rows;
|
|
|
|
// update cols and rows
|
|
var cols = this.clickStartCols+Math.floor(x/colSize);
|
|
cols = (cols > 5 ? cols : 5);
|
|
this.textarea.cols = this.cols = cols;
|
|
|
|
var rows = this.clickStartRows+Math.floor(y/rowSize);
|
|
rows = (rows > 2 ? rows : 2);
|
|
this.textarea.rows = this.rows = rows;
|
|
|
|
this.iframe.style.width = (6+this.textarea.offsetWidth)+"px";
|
|
this.iframe.style.height = this.iframe.contentDocument.body.offsetHeight+"px";
|
|
}
|
|
|
|
/**
|
|
* Called to begin moving the annotation
|
|
*
|
|
* @param {Event} e DOM event corresponding to click on the grippy
|
|
* @private
|
|
*/
|
|
Zotero.Annotation.prototype._startMove = function(e) {
|
|
// stop propagation
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
var body = this.document.getElementsByTagName("body")[0];
|
|
|
|
// deactivate current action
|
|
this.annotationsObj.Zotero_Browser.toggleMode(null);
|
|
|
|
var me = this;
|
|
// set the handler required to deactivate
|
|
|
|
/**
|
|
* Callback to end move action
|
|
* @inner
|
|
*/
|
|
this.annotationsObj.clearAction = function() {
|
|
me.document.removeEventListener("click", me._handleMove, false);
|
|
body.style.cursor = "auto";
|
|
me.moveImg.src = "zotero://attachment/annotation-move.png";
|
|
me.annotationsObj.clearAction = undefined;
|
|
}
|
|
|
|
/**
|
|
* Listener to handle mouse moves on main page
|
|
* @inner
|
|
*/
|
|
var handleMoveMouse1 = function(e) {
|
|
me.displayWithAbsoluteCoordinates(e.pageX + 1, e.pageY + 1);
|
|
};
|
|
/**
|
|
* Listener to handle mouse moves in iframe
|
|
* @inner
|
|
*/
|
|
var handleMoveMouse2 = function(e) {
|
|
me.displayWithAbsoluteCoordinates(e.pageX + me.iframeX + 1, e.pageY + me.iframeY + 1);
|
|
};
|
|
this.document.addEventListener("mousemove", handleMoveMouse1, false);
|
|
this.iframeDoc.addEventListener("mousemove", handleMoveMouse2, false);
|
|
|
|
/**
|
|
* Listener to finish off move when a click is made
|
|
* @inner
|
|
*/
|
|
var handleMove = function(e) {
|
|
me.document.removeEventListener("mousemove", handleMoveMouse1, false);
|
|
me.iframeDoc.removeEventListener("mousemove", handleMoveMouse2, false);
|
|
me.document.removeEventListener("click", handleMove, false);
|
|
|
|
me.initWithEvent(e);
|
|
me.annotationsObj.clearAction();
|
|
|
|
// stop propagation
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
};
|
|
this.document.addEventListener("click", handleMove, false);
|
|
|
|
body.style.cursor = "pointer";
|
|
this.moveImg.src = "zotero://attachment/annotation-move-selected.png";
|
|
}
|
|
|
|
/**
|
|
* @class Represents an individual highlighted range
|
|
*
|
|
* @constructor
|
|
* @param {Zotero.Annotations} annotationsObj The Zotero.Annotations object corresponding to the
|
|
* page this highlight is on
|
|
*/
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Gets the highlighted DOM range
|
|
* @return {Range} DOM range
|
|
*/
|
|
Zotero.Highlight.prototype.getRange = function() {
|
|
this.range = this.document.createRange();
|
|
var startContainer, startOffset, endContainer, endOffset;
|
|
[startContainer, startOffset] = this.startPath.toNode();
|
|
[endContainer, endOffset] = this.endPath.toNode();
|
|
|
|
if(!startContainer || !endContainer) {
|
|
throw("Annotate: PATH ERROR in highlight module!");
|
|
}
|
|
|
|
this.range.setStart(startContainer, startOffset);
|
|
this.range.setEnd(endContainer, endOffset);
|
|
return this.range;
|
|
}
|
|
|
|
/**
|
|
* Generates a highlight representing the given DB row
|
|
*/
|
|
Zotero.Highlight.prototype.initWithDBRow = function(row) {
|
|
this.startPath = new Zotero.Annotate.Path(this.document, this.nsResolver, row.startParent,
|
|
row.startTextNode, row.startOffset);
|
|
this.endPath = new Zotero.Annotate.Path(this.document, this.nsResolver, row.endParent,
|
|
row.endTextNode, row.endOffset);
|
|
this.getRange();
|
|
this._highlight();
|
|
}
|
|
|
|
/**
|
|
* Generates a highlight representing given a DOM range
|
|
*
|
|
* @param {Range} range DOM range
|
|
* @param {Zotero.Annotate.Path} startPath Path representing start of range
|
|
* @param {Zotero.Annotate.Path} endPath Path representing end of range
|
|
*/
|
|
Zotero.Highlight.prototype.initWithRange = function(range, startPath, endPath) {
|
|
this.startPath = startPath;
|
|
this.endPath = endPath;
|
|
this.range = range;
|
|
this._highlight();
|
|
}
|
|
|
|
/**
|
|
* Saves this highlight to the DB
|
|
*/
|
|
Zotero.Highlight.prototype.save = function() {
|
|
// don't save defective highlights
|
|
if(this.startPath.parent == this.endPath.parent
|
|
&& this.startPath.textNode == this.endPath.textNode
|
|
&& this.startPath.offset == this.endPath.offset) {
|
|
return false;
|
|
}
|
|
|
|
var query = "INSERT INTO highlights VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, DATETIME('now'))";
|
|
var parameters = [
|
|
this.annotationsObj.itemID, // itemID
|
|
this.startPath.parent, // startParent
|
|
(this.startPath.textNode ? this.startPath.textNode : null), // startTextNode
|
|
(this.startPath.offset || this.startPath.offset === 0 ? this.startPath.offset : null), // startOffset
|
|
this.endPath.parent, // endParent
|
|
(this.endPath.textNode ? this.endPath.textNode : null), // endTextNode
|
|
(this.endPath.offset || this.endPath.offset === 0 ? this.endPath.offset: null) // endOffset
|
|
];
|
|
|
|
Zotero.DB.query(query, parameters);
|
|
}
|
|
|
|
Zotero.Highlight.UNHIGHLIGHT_ALL = 0;
|
|
Zotero.Highlight.UNHIGHLIGHT_TO_POINT = 1;
|
|
Zotero.Highlight.UNHIGHLIGHT_FROM_POINT = 2;
|
|
|
|
/**
|
|
* Un-highlights a range
|
|
*
|
|
* @param {Node} container Node to highlight/unhighlight from, or null if mode == UNHIGHLIGHT_ALL
|
|
* @param {Integer} offset Text offset, or null if mode == UNHIGHLIGHT_ALL
|
|
* @param {Zotero.Annotate.Path} path Path representing node, offset combination, or null
|
|
* if mode == UNHIGHLIGHT_ALL
|
|
* @param {Integer} mode Unhighlight mode
|
|
*/
|
|
Zotero.Highlight.prototype.unhighlight = function(container, offset, path, mode) {
|
|
this.getRange();
|
|
|
|
if(mode == 1) {
|
|
this.range.setStart(container, offset);
|
|
this.startPath = path;
|
|
} else if(mode == 2) {
|
|
this.range.setEnd(container, offset);
|
|
this.endPath = path;
|
|
}
|
|
|
|
var length = this.spans.length;
|
|
for(var i=0; i<length; i++) {
|
|
var span = this.spans[i];
|
|
if(!span) continue;
|
|
var parentNode = span.parentNode;
|
|
|
|
if(mode != 0 && span === container.parentNode && offset != 0) {
|
|
if(mode == 1) {
|
|
// split text node
|
|
var textNode = container.splitText(offset);
|
|
this.range.setStart(container, offset);
|
|
|
|
// loop through, removing nodes
|
|
var node = span.firstChild;
|
|
|
|
while(span.firstChild && span.firstChild !== textNode) {
|
|
parentNode.insertBefore(span.removeChild(span.firstChild), span);
|
|
}
|
|
} else if(mode == 2) {
|
|
// split text node
|
|
var textNode = container.splitText(offset);
|
|
|
|
// loop through, removing nodes
|
|
var node = textNode;
|
|
var nextNode = span.nextSibling ? span.nextSibling : null;
|
|
var child;
|
|
while(node) {
|
|
child = node;
|
|
node = node.nextSibling;
|
|
parentNode.insertBefore(span.removeChild(child), nextNode);
|
|
}
|
|
|
|
this.range.setEnd(span.lastChild, span.lastChild.nodeValue.length);
|
|
}
|
|
} else if((mode == 0 || !this.range.isPointInRange(span, 0)) && parentNode) {
|
|
// attach child nodes before
|
|
while(span.hasChildNodes()) {
|
|
parentNode.insertBefore(span.removeChild(span.firstChild), span);
|
|
}
|
|
|
|
// remove span from DOM
|
|
parentNode.removeChild(span);
|
|
|
|
// remove span from list
|
|
this.spans.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
|
|
this.document.normalize();
|
|
}
|
|
|
|
/**
|
|
* Actually highlights the range this object refers to
|
|
* @private
|
|
*/
|
|
Zotero.Highlight.prototype._highlight = function() {
|
|
var endUpdated = false;
|
|
var startNode = this.range.startContainer;
|
|
var endNode = this.range.endContainer;
|
|
|
|
var ancestor = this.range.commonAncestorContainer;
|
|
|
|
var onlyOneNode = startNode === endNode;
|
|
|
|
if(!onlyOneNode && startNode !== ancestor && endNode !== ancestor) {
|
|
// highlight nodes after start node in the DOM hierarchy not at ancestor level
|
|
while(startNode.parentNode && startNode.parentNode !== 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 && endNode.parentNode !== 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 !== 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.length) {
|
|
var textNode = this.range.endContainer.splitText(this.range.endOffset);
|
|
}
|
|
if(!onlyOneNode) {
|
|
var span = this._highlightTextNode(this.range.endContainer);
|
|
this.range.setEnd(span.lastChild, span.lastChild.nodeValue.length);
|
|
endUpdated = true;
|
|
} else if(textNode) {
|
|
this.range.setEnd(textNode, 0);
|
|
endUpdated = true;
|
|
}
|
|
}
|
|
|
|
// split the start off of the first node
|
|
if(this.range.startContainer.nodeType == Components.interfaces.nsIDOMNode.TEXT_NODE) {
|
|
if(!this.range.startOffset) {
|
|
var highlightNode = this.range.startContainer;
|
|
} else {
|
|
var highlightNode = this.range.startContainer.splitText(this.range.startOffset);
|
|
}
|
|
var span = this._highlightTextNode(highlightNode);
|
|
} else {
|
|
var span = this._highlightSpaceBetween(this.range.startContainer, this.range.endContainer);
|
|
}
|
|
|
|
this.range.setStart(span.firstChild, 0);
|
|
if(onlyOneNode && !endUpdated) {
|
|
this.range.setEnd(span.lastChild, span.lastChild.nodeValue.length);
|
|
}
|
|
|
|
this.document.normalize();
|
|
}
|
|
|
|
/**
|
|
* Highlights a single text node
|
|
*
|
|
* @param {Node} textNode
|
|
* @return {Node} Span including the highlighted text
|
|
* @private
|
|
*/
|
|
Zotero.Highlight.prototype._highlightTextNode = function(textNode) {
|
|
if(!textNode) return;
|
|
var parent = textNode.parentNode;
|
|
|
|
var span = false;
|
|
var saveSpan = true;
|
|
|
|
var alreadyHighlighted = parent.getAttribute("zotero") == "highlight";
|
|
|
|
var nextSibling = (alreadyHighlighted ? textNode.parentNode.nextSibling : textNode.nextSibling);
|
|
var previousSibling = (alreadyHighlighted ? textNode.parentNode.previousSibling : textNode.previousSibling);
|
|
var previousSiblingHighlighted = previousSibling && previousSibling.getAttribute &&
|
|
previousSibling.getAttribute("zotero") == "highlight";
|
|
var nextSiblingHighlighted = nextSibling && nextSibling.getAttribute &&
|
|
nextSibling.getAttribute("zotero") == "highlight";
|
|
|
|
if(alreadyHighlighted) {
|
|
if(previousSiblingHighlighted || nextSiblingHighlighted) {
|
|
// merge with previous sibling
|
|
while(parent.firstChild) {
|
|
if(previousSiblingHighlighted) {
|
|
previousSibling.appendChild(parent.removeChild(parent.firstChild));
|
|
} else {
|
|
nextSibling.insertBefore(parent.removeChild(parent.firstChild),
|
|
(nextSibling.firstChild ? nextSibling.firstChild : null));
|
|
}
|
|
}
|
|
parent.parentNode.removeChild(parent);
|
|
// look for span in this.spans and delete it if it's there
|
|
var span = previousSiblingHighlighted ? previousSibling : nextSibling;
|
|
for(var i=0; i<this.spans.length; i++) {
|
|
if(parent === this.spans[i]) {
|
|
this.spans.splice(i, 1);
|
|
i--;
|
|
} else if(span === this.spans[i]) {
|
|
saveSpan = false;
|
|
}
|
|
}
|
|
} else {
|
|
span = parent;
|
|
}
|
|
} else if(previousSiblingHighlighted) {
|
|
previousSibling.appendChild(parent.removeChild(textNode));
|
|
|
|
var span = previousSibling;
|
|
for(var i=0; i<this.spans.length; i++) {
|
|
if(span === this.spans[i]) saveSpan = false;
|
|
}
|
|
} else if(nextSiblingHighlighted) {
|
|
nextSibling.insertBefore(parent.removeChild(textNode), nextSibling.firstChild);
|
|
|
|
var span = nextSibling;
|
|
for(var i=0; i<this.spans.length; i++) {
|
|
if(span === this.spans[i]) saveSpan = false;
|
|
}
|
|
} else {
|
|
var previousSibling = textNode.previousSibling;
|
|
|
|
var span = this.document.createElement("span");
|
|
span.setAttribute("zotero", "highlight");
|
|
span.style.display = "inline";
|
|
span.style.backgroundColor = Zotero.Annotate.highlightColor;
|
|
|
|
var computedColor = this.document.defaultView.getComputedStyle(parent, null).color;
|
|
if(computedColor) {
|
|
var distance1 = Zotero.Annotate.getColorDistance(computedColor, Zotero.Annotate.highlightColor)
|
|
if(distance1 <= 180) {
|
|
var distance2 = Zotero.Annotate.getColorDistance(computedColor, Zotero.Annotate.alternativeHighlightColor);
|
|
if(distance2 > distance1) {
|
|
span.style.backgroundColor = Zotero.Annotate.alternativeHighlightColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
span.appendChild(parent.removeChild(textNode));
|
|
parent.insertBefore(span, (nextSibling ? nextSibling : null));
|
|
}
|
|
|
|
if(span && saveSpan) this.spans.push(span);
|
|
return span;
|
|
}
|
|
|
|
/**
|
|
* Highlights the space between two nodes at the same level
|
|
*
|
|
* @param {Node} start
|
|
* @param {Node} end
|
|
* @return {Node} Span containing the first block of highlighted text
|
|
* @private
|
|
*/
|
|
Zotero.Highlight.prototype._highlightSpaceBetween = function(start, end) {
|
|
var firstSpan = false;
|
|
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.ORDERED_NODE_ITERATOR_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 === end) {
|
|
node = false;
|
|
} else {
|
|
node = node.nextSibling;
|
|
}
|
|
|
|
for (let textNode of textArray) {
|
|
if(firstSpan) {
|
|
this._highlightTextNode(textNode);
|
|
} else {
|
|
firstSpan = this._highlightTextNode(textNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
return firstSpan;
|
|
} |