From 8026f744c56f859d5091c3916e003d5a689f42d9 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Sat, 20 Jun 2020 06:10:34 -0400 Subject: [PATCH] Remove old HTML snapshot annotation support Any data trapped in these tables hasn't been accessible in many years. --- chrome/content/zotero/xpcom/annotate.js | 1634 ----------------------- chrome/content/zotero/xpcom/schema.js | 3 + components/zotero-service.js | 1 - defaults/preferences/zotero.js | 3 - resource/schema/userdata.sql | 31 - 5 files changed, 3 insertions(+), 1669 deletions(-) delete mode 100644 chrome/content/zotero/xpcom/annotate.js diff --git a/chrome/content/zotero/xpcom/annotate.js b/chrome/content/zotero/xpcom/annotate.js deleted file mode 100644 index 638fc1b73c..0000000000 --- a/chrome/content/zotero/xpcom/annotate.js +++ /dev/null @@ -1,1634 +0,0 @@ -/* - ***** 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 . - - ***** 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 new Error("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 new Error("Annotate: dereferenceNodeOffset called with invalid offset "+offset); - } - if(!node) throw new Error("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 new Error("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 new Error("Annotate: Path() handled Zotero 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 new Error("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 new Error("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 new Error("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 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; -} \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index 84c3e2f578..2369c02211 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -3219,6 +3219,9 @@ Zotero.Schema = new function(){ } else if (i == 112) { + yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS annotations"); + yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS highlights"); + yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS users"); yield Zotero.DB.queryAsync("CREATE TABLE users (\n userID INTEGER PRIMARY KEY,\n name TEXT NOT NULL\n)"); } diff --git a/components/zotero-service.js b/components/zotero-service.js index edd25cc01a..83b54b2d09 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -64,7 +64,6 @@ const xpcomFilesLocal = [ 'libraryTreeView', 'collectionTreeView', 'collectionTreeRow', - 'annotate', 'api', 'attachments', 'cite', diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js index a27c77383a..c0b95467ef 100644 --- a/defaults/preferences/zotero.js +++ b/defaults/preferences/zotero.js @@ -139,9 +139,6 @@ pref("extensions.zotero.httpServer.port", 23119); // ascii "ZO" // Zeroconf pref("extensions.zotero.zeroconf.server.enabled", false); -// Annotation settings -pref("extensions.zotero.annotations.warnOnClose", true); - // Streaming server pref("extensions.zotero.streaming.enabled", true); diff --git a/resource/schema/userdata.sql b/resource/schema/userdata.sql index c0887e5b38..1454624333 100644 --- a/resource/schema/userdata.sql +++ b/resource/schema/userdata.sql @@ -374,37 +374,6 @@ CREATE TABLE storageDeleteLog ( FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE ); -CREATE TABLE annotations ( - annotationID INTEGER PRIMARY KEY, - itemID INT NOT NULL, - parent TEXT, - textNode INT, - offset INT, - x INT, - y INT, - cols INT, - rows INT, - text TEXT, - collapsed BOOL, - dateModified DATE, - FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE -); -CREATE INDEX annotations_itemID ON annotations(itemID); - -CREATE TABLE highlights ( - highlightID INTEGER PRIMARY KEY, - itemID INT NOT NULL, - startParent TEXT, - startTextNode INT, - startOffset INT, - endParent TEXT, - endTextNode INT, - endOffset INT, - dateModified DATE, - FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE -); -CREATE INDEX highlights_itemID ON highlights(itemID); - CREATE TABLE proxies ( proxyID INTEGER PRIMARY KEY, multiHost INT,