www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

browser.js (32267B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     
     24     Based on code from Greasemonkey and PiggyBank
     25     
     26     ***** END LICENSE BLOCK *****
     27 */
     28 
     29 //
     30 // Zotero Ingester Browser Functions
     31 //
     32 
     33 //////////////////////////////////////////////////////////////////////////////
     34 //
     35 // Zotero_Browser
     36 //
     37 //////////////////////////////////////////////////////////////////////////////
     38 
     39 // Class to interface with the browser when ingesting data
     40 
     41 var Zotero_Browser = new function() {
     42 	this.init = init;
     43 	this.annotatePage = annotatePage;
     44 	this.toggleMode = toggleMode;
     45 	this.toggleCollapsed = toggleCollapsed;
     46 	this.chromeLoad = chromeLoad;
     47 	this.itemUpdated = itemUpdated;
     48 	this.tabClose = tabClose;
     49 	this.resize = resize;
     50 	
     51 	this.tabbrowser = null;
     52 	this.appcontent = null;
     53 	this.isScraping = false;
     54 	
     55 	var _browserData = new WeakMap();
     56 	var _attachmentsMap = new WeakMap();
     57 	var _detectCallbacks = [];
     58 	
     59 	var _blacklist = [
     60 		"googlesyndication.com",
     61 		"doubleclick.net",
     62 		"questionmarket.com",
     63 		"atdmt.com",
     64 		"aggregateknowledge.com",
     65 		"ad.yieldmanager.com"
     66 	];
     67 	
     68 	var _locationBlacklist = [
     69 		"zotero://debug/"
     70 	];
     71 	
     72 	var tools = {
     73 		'zotero-annotate-tb-add':{
     74 			cursor:"pointer",
     75 			event:"click",
     76 			callback:function(e) { _add("annotation", e) }
     77 		},
     78 		'zotero-annotate-tb-highlight':{
     79 			cursor:"text",
     80 			event:"mouseup",
     81 			callback:function(e) { _add("highlight", e) }
     82 		},
     83 		'zotero-annotate-tb-unhighlight':{
     84 			cursor:"text",
     85 			event:"mouseup",
     86 			callback:function(e) { _add("unhighlight", e) }
     87 		}
     88 	};
     89 
     90 	//////////////////////////////////////////////////////////////////////////////
     91 	//
     92 	// Public Zotero_Browser methods
     93 	//
     94 	//////////////////////////////////////////////////////////////////////////////
     95 	
     96 	
     97 	/**
     98 	 * Initialize some variables and prepare event listeners for when chrome is done loading
     99 	 */
    100 	function init() {
    101 		// No gBrowser - running in standalone
    102 		if (!window.hasOwnProperty("gBrowser")) {
    103 			return;
    104 		}
    105 		
    106 		var zoteroInitDone;
    107 		if (!Zotero || Zotero.skipLoading) {
    108 			// Zotero either failed to load or is reloading in Connector mode
    109 			// In case of the latter, listen for the 'zotero-loaded' event (once) and retry
    110 			var zoteroInitDone_deferred = Zotero.Promise.defer();
    111 			var obs = Components.classes["@mozilla.org/observer-service;1"]
    112 				.getService(Components.interfaces.nsIObserverService);
    113 			var observer = {
    114 				"observe":function() {
    115 					obs.removeObserver(observer, 'zotero-loaded')
    116 					zoteroInitDone_deferred.resolve();
    117 				}
    118 			};
    119 			obs.addObserver(observer, 'zotero-loaded', false);
    120 			
    121 			zoteroInitDone = zoteroInitDone_deferred.promise;
    122 		} else {
    123 			zoteroInitDone = Zotero.Promise.resolve();
    124 		}
    125 		
    126 		var chromeLoaded = Zotero.Promise.defer();
    127 		window.addEventListener("load", function(e) { chromeLoaded.resolve() }, false);
    128 		
    129 		// Wait for Zotero to init and chrome to load before proceeding
    130 		Zotero.Promise.all([
    131 			zoteroInitDone.then(function() {
    132 				ZoteroPane_Local.addReloadListener(reload);
    133 				reload();
    134 			}),
    135 			chromeLoaded.promise
    136 		])
    137 		.then(function() {
    138 			Zotero_Browser.chromeLoad()
    139 		});
    140 	}
    141 	
    142 	/**
    143 	 * Called when Zotero is reloaded
    144 	 */
    145 	function reload() {
    146 		// Handles the display of a div showing progress in scraping
    147 		Zotero_Browser.progress = new Zotero.ProgressWindow();
    148 	}
    149 	
    150 	/**
    151 	 * Saves from current page using translator (called when the capture icon is clicked)
    152 	 *
    153 	 * @param {String} [translator]
    154 	 * @param {Event} [event]
    155 	 * @return {Promise}
    156 	 */
    157 	this.scrapeThisPage = Zotero.Promise.coroutine(function* (translator, event) {
    158 		// Perform translation
    159 		var tab = _getTabObject(Zotero_Browser.tabbrowser.selectedBrowser);
    160 		var page = tab.getPageObject();
    161 		if(page.translators && page.translators.length) {
    162 			page.translate.setTranslator(translator || page.translators[0]);
    163 			yield Zotero_Browser.performTranslation(page.translate);
    164 		}
    165 		else {
    166 			yield this.saveAsWebPage(
    167 				(event && event.shiftKey) ? !Zotero.Prefs.get('automaticSnapshots') : null
    168 			);
    169 		}
    170 	});
    171 	
    172 	/**
    173 	 * Keep in sync with cmd_zotero_newItemFromCurrentPage
    174 	 *
    175 	 * @return {Promise}
    176 	 */
    177 	this.saveAsWebPage = function (includeSnapshots) {
    178 		// DEBUG: Possible to just trigger command directly with event? Assigning it to the
    179 		// command property of the icon doesn't seem to work, and neither does goDoCommand()
    180 		// from chrome://global/content/globalOverlay.js. Getting the command by id and
    181 		// running doCommand() works but doesn't pass the event.
    182 		return ZoteroPane.addItemFromPage('temporaryPDFHack', includeSnapshots);
    183 	}
    184 	
    185 	/*
    186 	 * flags a page for annotation
    187 	 */
    188 	function annotatePage(id, browser) {
    189 		if (browser) {
    190 			var tab = _getTabObject(browser);
    191 		}
    192 		else {
    193 			var tab = _getTabObject(this.tabbrowser.selectedBrowser);
    194 		}
    195 	}
    196 	
    197 	/*
    198 	 * toggles a tool on/off
    199 	 */
    200 	function toggleMode(toggleTool, ignoreOtherTools) {
    201 		// make sure other tools are turned off
    202 		if(!ignoreOtherTools) {
    203 			for(var tool in tools) {
    204 				if(tool != toggleTool && document.getElementById(tool).getAttribute("tool-active")) {
    205 					toggleMode(tool, true);
    206 				}
    207 			}
    208 		}
    209 		
    210 		// make sure annotation action is toggled
    211 		var tab = _getTabObject(Zotero_Browser.tabbrowser.selectedBrowser);
    212 		var page = tab.getPageObject();
    213 		if(page && page.annotations && page.annotations.clearAction) page.annotations.clearAction();
    214 		
    215 		if(!toggleTool) return;
    216 		
    217 		var body = Zotero_Browser.tabbrowser.selectedBrowser.contentDocument.getElementsByTagName("body")[0];
    218 		var addElement = document.getElementById(toggleTool);
    219 		
    220 		if(addElement.getAttribute("tool-active")) {
    221 			// turn off
    222 			body.style.cursor = "auto";
    223 			addElement.removeAttribute("tool-active");
    224 			Zotero_Browser.tabbrowser.selectedBrowser.removeEventListener(tools[toggleTool].event, tools[toggleTool].callback, true);
    225 		} else {
    226 			body.style.cursor = tools[toggleTool].cursor;
    227 			addElement.setAttribute("tool-active", "true");
    228 			Zotero_Browser.tabbrowser.selectedBrowser.addEventListener(tools[toggleTool].event, tools[toggleTool].callback, true);
    229 		}
    230 	}
    231 	
    232 	/*
    233 	 * expands all annotations
    234 	 */
    235 	function toggleCollapsed() {
    236 		var tab = _getTabObject(Zotero_Browser.tabbrowser.selectedBrowser);
    237 		tab.getPageObject().annotations.toggleCollapsed();
    238 	}
    239 	
    240 	/*
    241 	 * When chrome loads, register our event handlers with the appropriate interfaces
    242 	 */
    243 	function chromeLoad() {
    244 		this.tabbrowser = gBrowser;
    245 		this.appcontent = document.getElementById("appcontent");
    246 		
    247 		// this gives us onLocationChange, for updating when tabs are switched/created
    248 		gBrowser.tabContainer.addEventListener("TabClose",
    249 			function(e) {
    250 				//Zotero.debug("TabClose");
    251 				Zotero_Browser.tabClose(e);
    252 			}, false);
    253 		gBrowser.tabContainer.addEventListener("TabSelect",
    254 			function(e) {
    255 				//Zotero.debug("TabSelect");
    256 				// Note: async
    257 				Zotero_Browser.updateStatus();
    258 			}, false);
    259 		// this is for pageshow, for updating the status of the book icon
    260 		this.appcontent.addEventListener("pageshow", contentLoad, true);
    261 		// this is for turning off the book icon when a user navigates away from a page
    262 		this.appcontent.addEventListener("pagehide",
    263 			function(e) {
    264 				//Zotero.debug("pagehide");
    265 				Zotero_Browser.contentHide(e);
    266 			}, true);
    267 		
    268 		this.tabbrowser.addEventListener("resize",
    269 			function(e) { Zotero_Browser.resize(e) }, false);
    270 		// Resize on text zoom changes
    271 		
    272 		var reduce = document.getElementById('cmd_fullZoomReduce');
    273 		var enlarge = document.getElementById('cmd_fullZoomEnlarge');
    274 		var reset = document.getElementById('cmd_fullZoomReset');
    275 		
    276 		if(reduce) reduce.addEventListener("command",
    277 			function(e) { Zotero_Browser.resize(e) }, false);
    278 		if(enlarge) enlarge.addEventListener("command",
    279 			function(e) { Zotero_Browser.resize(e) }, false);
    280 		if(reset) reset.addEventListener("command",
    281 			function(e) { Zotero_Browser.resize(e) }, false);
    282 	}
    283 	
    284 	
    285 	/*
    286 	 * An event handler called when a new document is loaded. Creates a new document
    287 	 * object, and updates the status of the capture icon
    288 	 */
    289 	var contentLoad = function (event) {
    290 		var doc = event.originalTarget;
    291 		var isHTML = doc instanceof HTMLDocument;
    292 		var rootDoc = (doc instanceof HTMLDocument ? doc.defaultView.top.document : doc);
    293 		var browser = Zotero_Browser.tabbrowser.getBrowserForDocument(rootDoc);
    294 		if(!browser) return;
    295 		
    296 		if(isHTML) {
    297 			// ignore blacklisted domains
    298 			try {
    299 				if(doc.domain) {
    300 					for (let i = 0; i < _blacklist.length; i++) {
    301 						let blacklistedURL = _blacklist[i];
    302 						if(doc.domain.substr(doc.domain.length-blacklistedURL.length) == blacklistedURL) {
    303 							Zotero.debug("Ignoring blacklisted URL "+doc.location);
    304 							return;
    305 						}
    306 					}
    307 				}
    308 			}
    309 			catch (e) {}
    310 		}
    311 		
    312 		try {
    313 			if (_locationBlacklist.indexOf(doc.location.href) != -1) {
    314 				return;
    315 			}
    316 			
    317 			// Ignore TinyMCE popups
    318 			if (!doc.location.host && doc.location.href.indexOf("tinymce/") != -1) {
    319 				return;
    320 			}
    321 		}
    322 		catch (e) {}
    323 		
    324 		// get data object
    325 		var tab = _getTabObject(browser);
    326 		
    327 		if(isHTML && !Zotero.isConnector) {
    328 			var annotationID = Zotero.Annotate.getAnnotationIDFromURL(browser.currentURI.spec);
    329 			if(annotationID) {
    330 				if(Zotero.Annotate.isAnnotated(annotationID)) {
    331 					//window.alert(Zotero.getString("annotations.oneWindowWarning"));
    332 				} else {
    333 					var page = tab.getPageObject();
    334 					if(!page.annotations) {
    335 						// enable annotation
    336 						page.annotations = new Zotero.Annotations(Zotero_Browser, browser, annotationID);
    337 						var saveAnnotations = function() {
    338 							page.annotations.save();
    339 							page.annotations = undefined;
    340 						};
    341 						browser.contentWindow.addEventListener('beforeunload', saveAnnotations, false);
    342 						browser.contentWindow.addEventListener('close', saveAnnotations, false);
    343 						page.annotations.load();
    344 					}
    345 				}
    346 			}
    347 		}
    348 		
    349 		// detect translators
    350 		tab.detectTranslators(rootDoc, doc);
    351 		
    352 		// register metadata updated event
    353 		if(isHTML) {
    354 			var contentWin = doc.defaultView;
    355 			if(!contentWin.haveZoteroEventListener) {
    356 				contentWin.addEventListener("ZoteroItemUpdated", function(event) { itemUpdated(event.originalTarget) }, false);
    357 				contentWin.haveZoteroEventListener = true;
    358 			}
    359 		}
    360 	};
    361 
    362 	/*
    363 	 * called to unregister Zotero icon, etc.
    364 	 */
    365 	this.contentHide = function (event) {
    366 		var doc = event.originalTarget;
    367 		if(!(doc instanceof HTMLDocument)) return;
    368 	
    369 		var rootDoc = (doc instanceof HTMLDocument ? doc.defaultView.top.document : doc);
    370 		var browser = Zotero_Browser.tabbrowser.getBrowserForDocument(rootDoc);
    371 		if(!browser) return;
    372 		
    373 		var tab = _getTabObject(browser);
    374 		if(!tab) return;
    375 
    376 		var page = tab.getPageObject();
    377 		if(!page) return;
    378 
    379 		if(doc == page.document || doc == rootDoc) {
    380 			// clear translator only if the page on which the pagehide event was called is
    381 			// either the page to which the translator corresponded, or the root document
    382 			// (the second check is probably paranoid, but won't hurt)
    383 			tab.clear();
    384 		}
    385 		
    386 		// update status
    387 		if(Zotero_Browser.tabbrowser.selectedBrowser == browser) {
    388 			// Note: async
    389 			this.updateStatus();
    390 		}
    391 	}
    392 	
    393 	/**
    394 	 * Called when item should be updated due to a DOM event
    395 	 */
    396 	function itemUpdated(doc) {
    397 		try {
    398 			var rootDoc = (doc instanceof HTMLDocument ? doc.defaultView.top.document : doc);
    399 			var browser = Zotero_Browser.tabbrowser.getBrowserForDocument(rootDoc);
    400 			var tab = _getTabObject(browser);
    401 			if(doc == tab.getPageObject().document || doc == rootDoc) tab.clear();
    402 			tab.detectTranslators(rootDoc, doc);
    403 		} catch(e) {
    404 			Zotero.debug(e);
    405 		}
    406 	}
    407 	
    408 	/*
    409 	 * called when a tab is closed
    410 	 */
    411 	function tabClose(event) {
    412 		// Save annotations when closing a tab, since the browser is already
    413 		// gone from tabbrowser by the time contentHide() gets called
    414 		var tab = _getTabObject(event.target);
    415 		var page = tab.getPageObject();
    416 		if(page && page.annotations) page.annotations.save();
    417 		tab.clear();
    418 		
    419 		// To execute if document object does not exist
    420 		toggleMode();
    421 	}
    422 	
    423 	
    424 	/*
    425 	 * called when the window is resized
    426 	 */
    427 	function resize(event) {
    428 		var tab = _getTabObject(this.tabbrowser.selectedBrowser);
    429 		var page = tab.getPageObject();
    430 		if(!page.annotations) return;
    431 		
    432 		page.annotations.refresh();
    433 	}
    434 	
    435 	/*
    436 	 * Updates the status of the capture icon to reflect the scrapability or lack
    437 	 * thereof of the current page
    438 	 */
    439 	this.updateStatus = Zotero.Promise.coroutine(function* () {
    440 		// Wait for translator initialization. This allows detection to still run on a page at startup
    441 		// once translators have finished loading.
    442 		if (Zotero.Schema && Zotero.Schema.schemaUpdatePromise.isPending()) {
    443 			yield Zotero.Schema.schemaUpdatePromise;
    444 		}
    445 		
    446 		if (!Zotero_Browser.tabbrowser) return;
    447 		var tab = _getTabObject(Zotero_Browser.tabbrowser.selectedBrowser);
    448 		
    449 		Components.utils.import("resource:///modules/CustomizableUI.jsm");
    450 		var buttons = getSaveButtons();
    451 		if (buttons.length) {
    452 			let state = tab.getCaptureState();
    453 			let tooltiptext = tab.getCaptureTooltip();
    454 			for (let { button, placement } of buttons) {
    455 				let inToolbar = placement.area == CustomizableUI.AREA_NAVBAR;
    456 				button.image = tab.getCaptureIcon(Zotero.hiDPI || !inToolbar);
    457 				button.tooltipText = tooltiptext;
    458 				if (state == tab.CAPTURE_STATE_TRANSLATABLE) {
    459 					button.classList.add('translate');
    460 				}
    461 				else {
    462 					button.classList.remove('translate');
    463 				}
    464 				button.removeAttribute('disabled');
    465 			}
    466 		}
    467 		
    468 		// set annotation bar status
    469 		var page = tab.getPageObject();
    470 		if(page.annotations && page.annotations.annotations.length) {
    471 			document.getElementById('zotero-annotate-tb').hidden = false;
    472 			toggleMode();
    473 		} else {
    474 			document.getElementById('zotero-annotate-tb').hidden = true;
    475 		}
    476 	});
    477 	
    478 	this.addDetectCallback = function (func) {
    479 		_detectCallbacks.push(func);
    480 	};
    481 	
    482 	this.resolveDetectCallbacks = Zotero.Promise.coroutine(function* () {
    483 		while (_detectCallbacks.length) {
    484 			let cb = _detectCallbacks.shift();
    485 			var res = cb();
    486 			if (res && res.then) {
    487 				yield res.then;
    488 			}
    489 		}
    490 	});
    491 	
    492 	function getSaveButtons() {
    493 		Components.utils.import("resource:///modules/CustomizableUI.jsm");
    494 		var buttons = [];
    495 		
    496 		var placement = CustomizableUI.getPlacementOfWidget("zotero-toolbar-buttons");
    497 		if (placement) {
    498 			let button = document.getElementById("zotero-toolbar-save-button");
    499 			if (button) {
    500 				buttons.push({
    501 					button: button,
    502 					placement: placement
    503 				});
    504 			}
    505 		}
    506 		
    507 		placement = CustomizableUI.getPlacementOfWidget("zotero-toolbar-save-button-single");
    508 		if (placement) {
    509 			let button = document.getElementById("zotero-toolbar-save-button-single");
    510 			if (button) {
    511 				buttons.push({
    512 					button: button,
    513 					placement: placement
    514 				});
    515 			}
    516 		}
    517 		
    518 		return buttons;
    519 	}
    520 	
    521 	/**
    522 	 * Called when status bar icon is right-clicked
    523 	 */
    524 	this.onStatusPopupShowing = function(e) {
    525 		var popup = e.target;
    526 		while(popup.hasChildNodes()) popup.removeChild(popup.lastChild);
    527 		
    528 		var tab = _getTabObject(this.tabbrowser.selectedBrowser);
    529 		var captureState = tab.getCaptureState();
    530 		if (captureState == tab.CAPTURE_STATE_TRANSLATABLE) {
    531 			let translators = tab.getPageObject().translators;
    532 			for (var i=0, n = translators.length; i < n; i++) {
    533 				let translator = translators[i];
    534 				
    535 				let menuitem = document.createElement("menuitem");
    536 				menuitem.setAttribute("label",
    537 					Zotero.getString("ingester.saveToZoteroUsing", translator.label));
    538 				menuitem.setAttribute("image", (translator.itemType === "multiple"
    539 					? "chrome://zotero/skin/treesource-collection.png"
    540 					: Zotero.ItemTypes.getImageSrc(translator.itemType)));
    541 				menuitem.setAttribute("class", "menuitem-iconic");
    542 				menuitem.addEventListener("command", function(e) {
    543 					Zotero_Browser.scrapeThisPage(translator, e);
    544 					e.stopPropagation();
    545 				}, false);
    546 				popup.appendChild(menuitem);
    547 			}
    548 		}
    549 		
    550 		let webPageIcon = tab.getWebPageCaptureIcon(Zotero.hiDPI);
    551 		let menuitem = document.createElement("menuitem");
    552 		menuitem.setAttribute("label", Zotero.getString('ingester.saveToZoteroAsWebPageWithSnapshot'));
    553 		menuitem.setAttribute("image", webPageIcon);
    554 		menuitem.setAttribute("class", "menuitem-iconic");
    555 		menuitem.addEventListener("command", function (event) {
    556 			Zotero_Browser.saveAsWebPage(true);
    557 			event.stopPropagation();
    558 		});
    559 		popup.appendChild(menuitem);
    560 		
    561 		menuitem = document.createElement("menuitem");
    562 		menuitem.setAttribute("label", Zotero.getString('ingester.saveToZoteroAsWebPageWithoutSnapshot'));
    563 		menuitem.setAttribute("image", webPageIcon);
    564 		menuitem.setAttribute("class", "menuitem-iconic");
    565 		menuitem.addEventListener("command", function (event) {
    566 			Zotero_Browser.saveAsWebPage(false);
    567 			event.stopPropagation();
    568 		});
    569 		popup.appendChild(menuitem);
    570 		
    571 		if (captureState == tab.CAPTURE_STATE_TRANSLATABLE) {
    572 			popup.appendChild(document.createElement("menuseparator"));
    573 			
    574 			let menuitem = document.createElement("menuitem");
    575 			menuitem.setAttribute("label", Zotero.getString("locate.libraryLookup.label"));
    576 			menuitem.setAttribute("tooltiptext", Zotero.getString("locate.libraryLookup.tooltip"));
    577 			menuitem.setAttribute("image", "chrome://zotero/skin/locate-library-lookup.png");
    578 			menuitem.setAttribute("class", "menuitem-iconic");
    579 			menuitem.addEventListener("command", _constructLookupFunction(tab, function(event, obj) {
    580 				var urls = [];
    581 				for (let i = 0; i < obj.newItems.length; i++) {
    582 					var url = Zotero.OpenURL.resolve(obj.newItems[i]);
    583 					if(url) urls.push(url);
    584 				}
    585 				ZoteroPane.loadURI(urls, event);
    586 			}), false);
    587 			popup.appendChild(menuitem);		
    588 			
    589 			var locateEngines = Zotero.LocateManager.getVisibleEngines();
    590 			Zotero_LocateMenu.addLocateEngines(popup, locateEngines,
    591 				_constructLookupFunction(tab, function(e, obj) {
    592 				Zotero_LocateMenu.locateItem(e, obj.newItems);
    593 			}), true);
    594 		}
    595 	}
    596 	
    597 	/**
    598 	 * Translates using the specified translation instance. setTranslator() must already
    599 	 * have been called
    600 	 * @param {Zotero.Translate} translate
    601 	 */
    602 	this.performTranslation = Zotero.Promise.coroutine(function* (translate, libraryID, collection) {
    603 		if (Zotero.locked) {
    604 			Zotero_Browser.progress.Translation.operationInProgress();
    605 			return;
    606 		}
    607 		
    608 		if (!Zotero.isConnector && Zotero.DB.inTransaction()) {
    609 			yield Zotero.DB.waitForTransaction();
    610 		}
    611 		
    612 		Zotero_Browser.progress.show();
    613 		Zotero_Browser.isScraping = true;
    614 		
    615 		// Get libraryID and collectionID
    616 		if(libraryID === undefined && ZoteroPane && !Zotero.isConnector) {
    617 			// Save to My Library by default if pane hasn't been opened
    618 			if (!ZoteroPane.collectionsView || !ZoteroPane.collectionsView.selectedTreeRow) {
    619 				libraryID = Zotero.Libraries.userLibraryID;
    620 			}
    621 			else if (!ZoteroPane.collectionsView.editable) {
    622 				Zotero_Browser.progress.Translation.cannotEditCollection();
    623 				return;
    624 			}
    625 			else {
    626 				libraryID = ZoteroPane.getSelectedLibraryID();
    627 			}
    628 			
    629 			collection = ZoteroPane.getSelectedCollection();
    630 		}
    631 		
    632 		if (!Zotero.isConnector) {
    633 			if (ZoteroPane.collectionsView
    634 					&& ZoteroPane.collectionsView
    635 					&& ZoteroPane.collectionsView.selectedTreeRow
    636 					&& ZoteroPane.collectionsView.selectedTreeRow.isPublications()) {
    637 				Zotero_Browser.progress.Translation.cannotAddToPublications();
    638 				return;
    639 			}
    640 			
    641 			if (Zotero.Feeds.get(libraryID)) {
    642 				Zotero_Browser.progress.Translation.cannotAddToFeed();
    643 				return;
    644 			}
    645 		}
    646 		
    647 		Zotero_Browser.progress.Translation.scrapingTo(libraryID, collection);
    648 		
    649 		translate.clearHandlers("done");
    650 		translate.clearHandlers("itemDone");
    651 		translate.clearHandlers("attachmentProgress");
    652 		
    653 		var deferred = Zotero.Promise.defer();
    654 		
    655 		translate.setHandler("done", function() {
    656 			Zotero_Browser.progress.Translation.doneHandler.apply(Zotero_Browser.progress.Translation, arguments);
    657 			Zotero_Browser.isScraping = false;
    658 			deferred.resolve();
    659 		});
    660 		
    661 		translate.setHandler("itemDone", function() {
    662 			let handler = Zotero_Browser.progress.Translation.itemDoneHandler(_attachmentsMap);
    663 			handler.apply(Zotero_Browser.progress.Translation, arguments);
    664 		});
    665 		
    666 		translate.setHandler("attachmentProgress", function() {
    667 			let handler = Zotero_Browser.progress.Translation.attachmentProgressHandler(_attachmentsMap);
    668 			handler.apply(Zotero_Browser.progress.Translation, arguments);
    669 		});
    670 		
    671 		translate.translate({
    672 			libraryID,
    673 			collections: collection ? [collection.id] : false
    674 		});
    675 		
    676 		return deferred.promise;
    677 	});
    678 	
    679 	
    680 	//////////////////////////////////////////////////////////////////////////////
    681 	//
    682 	// Private Zotero_Browser methods
    683 	//
    684 	//////////////////////////////////////////////////////////////////////////////
    685 	
    686 	function _constructLookupFunction(tab, success) {
    687 		return function(e) {
    688 			var page = tab.getPageObject();
    689 			page.translate.setTranslator(page.translators[0]);
    690 			page.translate.clearHandlers("done");
    691 			page.translate.clearHandlers("itemDone");
    692 			page.translate.setHandler("done", function(obj, status) {
    693 				if(status) {
    694 					success(e, obj);
    695 					Zotero_Browser.progress.close();
    696 				} else {
    697 					Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.lookup.error"));
    698 					Zotero_Browser.progress.startCloseTimer(8000);
    699 				}
    700 			});
    701 			
    702 			Zotero_Browser.progress.show();
    703 			Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.lookup.performing"));
    704 			page.translate.translate(false);
    705 			e.stopPropagation();
    706 		}
    707 	}
    708 	
    709 	/*
    710 	 * Gets a data object given a browser window object
    711 	 */
    712 	function _getTabObject(browser) {
    713 		if(!browser) return false;
    714 		var obj = _browserData.get(browser);
    715 		if(!obj) {
    716 			obj = new Zotero_Browser.Tab(browser);
    717 			_browserData.set(browser, obj);
    718 		}
    719 		return obj;
    720 	}
    721 	
    722 	/**
    723 	 * Adds an annotation
    724 	 */
    725 	 function _add(type, e) {
    726 		var tab = _getTabObject(Zotero_Browser.tabbrowser.selectedBrowser);
    727 		
    728 		if(type == "annotation") {
    729 			// ignore click if it's on an existing annotation
    730 			if(e.target.getAttribute("zotero-annotation")) return;
    731 			
    732 			var annotation = tab.getPageObject().annotations.createAnnotation();
    733 			annotation.initWithEvent(e);
    734 			
    735 			// disable add mode, now that we've used it
    736 			toggleMode();
    737 		} else {
    738 			try {
    739 				var selection = Zotero_Browser.tabbrowser.selectedBrowser.contentWindow.getSelection();
    740 			} catch(err) {
    741 				return;
    742 			}
    743 			if(selection.isCollapsed) return;
    744 			
    745 			if(type == "highlight") {
    746 				tab.getPageObject().annotations.highlight(selection.getRangeAt(0));
    747 			} else if(type == "unhighlight") {
    748 				tab.getPageObject().annotations.unhighlight(selection.getRangeAt(0));
    749 			}
    750 			
    751 			selection.removeAllRanges();
    752 		}
    753 		
    754 		// stop propagation
    755 		e.stopPropagation();
    756 		e.preventDefault();
    757 	 }
    758 }
    759 
    760 
    761 //////////////////////////////////////////////////////////////////////////////
    762 //
    763 // Zotero_Browser.Tab
    764 //
    765 //////////////////////////////////////////////////////////////////////////////
    766 
    767 Zotero_Browser.Tab = function(browser) {
    768 	this.browser = browser;
    769 	this.wm = new WeakMap();
    770 }
    771 
    772 Zotero_Browser.Tab.prototype.CAPTURE_STATE_DISABLED = 0;
    773 Zotero_Browser.Tab.prototype.CAPTURE_STATE_GENERIC = 1;
    774 Zotero_Browser.Tab.prototype.CAPTURE_STATE_TRANSLATABLE = 2;
    775 
    776 /**
    777  * Gets page-specific information (stored in WeakMap to prevent holding
    778  * a reference to translate)
    779  */
    780 Zotero_Browser.Tab.prototype.getPageObject = function() {
    781 	var doc = this.browser.contentWindow;
    782 	if(!doc) return null;
    783 	var obj = this.wm.get(doc);
    784 	if(!obj) {
    785 		obj = {};
    786 		this.wm.set(doc, obj);
    787 	}
    788 	return obj;
    789 }
    790 
    791 /*
    792  * Removes page-specific information from WeakMap
    793  */
    794 Zotero_Browser.Tab.prototype.clear = function() {
    795 	this.wm.delete(this.browser.contentWindow);
    796 }
    797 
    798 /*
    799  * detects translators for this browser object
    800  */
    801 Zotero_Browser.Tab.prototype.detectTranslators = Zotero.Promise.coroutine(function* (rootDoc, doc) {
    802 	if (Zotero.Schema && Zotero.Schema.schemaUpdatePromise.isPending()) {
    803 		yield Zotero.Schema.schemaUpdatePromise;
    804 	}
    805 	
    806 	// If document no longer exists after waiting for schema updates (probably because another page has
    807 	// been loaded), bail
    808 	if (Components.utils.isDeadWrapper(doc)) {
    809 		return;
    810 	}
    811 	
    812 	if (doc instanceof HTMLDocument) {
    813 		if (doc.documentURI.startsWith("about:")) {
    814 			return;
    815 		}
    816 		
    817 		// get translators
    818 		var me = this;
    819 		
    820 		var translate = new Zotero.Translate.Web();
    821 		translate.setDocument(doc);
    822 		translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) });
    823 		translate.setHandler("pageModified", function(translate, doc) { Zotero_Browser.itemUpdated(doc) });
    824 		translate.getTranslators(true);
    825 	} else if(doc.documentURI.substr(0, 7) == "file://") {
    826 		this._attemptLocalFileImport(doc);
    827 	}
    828 });
    829 
    830 
    831 /*
    832  * searches for a document in all of the frames of a given document
    833  */
    834 Zotero_Browser.Tab.prototype._searchFrames = function(rootDoc, searchDoc) {
    835 	if(rootDoc == searchDoc) return true;
    836 	var frames = rootDoc.getElementsByTagName("frame");
    837 	for (let i = 0; i < frames.length; i++) {
    838 		let frame = frames[i];
    839 		if(frame.contentDocument &&
    840 				(frame.contentDocument == searchDoc ||
    841 				this._searchFrames(frame.contentDocument, searchDoc))) {
    842 			return true;
    843 		}
    844 	}
    845 	
    846 	var frames = rootDoc.getElementsByTagName("iframe");
    847 	for (let i = 0; i < frames.length; i++) {
    848 		let frame = frames[i];
    849 		if(frame.contentDocument &&
    850 				(frame.contentDocument == searchDoc ||
    851 				this._searchFrames(frame.contentDocument, searchDoc))) {
    852 			return true;
    853 		}
    854 	}
    855 	
    856 	return false;
    857 }
    858 
    859 /*
    860  * Attempts import of a file; to be run on local files only
    861  */
    862 Zotero_Browser.Tab.prototype._attemptLocalFileImport = function(doc) {
    863 	if(doc.documentURI.match(/\.csl(\.xml|\.txt)?$/i)) {
    864 		// read CSL string
    865 		var csl = Zotero.File.getContentsFromURL(doc.documentURI);
    866 		if(csl.indexOf("http://purl.org/net/xbiblio/csl") != -1) {
    867 			// looks like a CSL; try to import
    868 			Zotero.Styles.install(csl, doc.documentURI);
    869 		}
    870 	} else {
    871 		// see if we can import this file
    872 		var file = Components.classes["@mozilla.org/network/protocol;1?name=file"]
    873 									.getService(Components.interfaces.nsIFileProtocolHandler)
    874 									.getFileFromURLSpec(doc.documentURI);
    875 		
    876 		var me = this;
    877 		var translate = new Zotero.Translate.Import();
    878 		translate.setLocation(file);
    879 		translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) });
    880 		translate.getTranslators();
    881 	}
    882 }
    883 
    884 
    885 Zotero_Browser.Tab.prototype.getCaptureState = function () {
    886 	var page = this.getPageObject();
    887 	if (!page.saveEnabled) {
    888 		return this.CAPTURE_STATE_DISABLED;
    889 	}
    890 	if (page.translators && page.translators.length) {
    891 		return this.CAPTURE_STATE_TRANSLATABLE;
    892 	}
    893 	return this.CAPTURE_STATE_GENERIC;
    894 }
    895 
    896 /*
    897  * returns the URL of the image representing the translator to be called on the
    898  * current page, or false if the page cannot be scraped
    899  */
    900 Zotero_Browser.Tab.prototype.getCaptureIcon = function (hiDPI) {
    901 	switch (this.getCaptureState()) {
    902 	case this.CAPTURE_STATE_TRANSLATABLE:
    903 		var itemType = this.getPageObject().translators[0].itemType;
    904 		return (itemType === "multiple"
    905 				? "chrome://zotero/skin/treesource-collection" + Zotero.hiDPISuffix + ".png"
    906 				: Zotero.ItemTypes.getImageSrc(itemType));
    907 	
    908 	default:
    909 		return this.getWebPageCaptureIcon(hiDPI);
    910 	}
    911 }
    912 
    913 // TODO: Show icons for images, PDFs, etc.?
    914 Zotero_Browser.Tab.prototype.getWebPageCaptureIcon = function (hiDPI) {
    915 	return "chrome://zotero/skin/treeitem-webpage" + Zotero.hiDPISuffix + ".png";
    916 }
    917 
    918 Zotero_Browser.Tab.prototype.getCaptureTooltip = function() {
    919 	switch (this.getCaptureState()) {
    920 	case this.CAPTURE_STATE_DISABLED:
    921 		var text = Zotero.getString('ingester.saveToZotero');
    922 		break;
    923 	
    924 	case this.CAPTURE_STATE_TRANSLATABLE:
    925 		var text = Zotero.getString('ingester.saveToZotero');
    926 		var translator = this.getPageObject().translators[0];
    927 		if (translator.itemType == 'multiple') {
    928 			text += '…';
    929 		}
    930 		text += ' (' + translator.label + ')';
    931 		break;
    932 	
    933 	// TODO: Different captions for images, PDFs, etc.?
    934 	default:
    935 		var text = Zotero.getString('ingester.saveToZotero')
    936 			+ " (" + Zotero.getString('itemTypes.webpage') + ")";
    937 	}
    938 	
    939 	var key = Zotero.Keys.getKeyForCommand('saveToZotero');
    940 	if (key) {
    941 		// Add RLE mark in RTL mode to make shortcut render the right way
    942 		text += (Zotero.rtl ? ' \u202B' : ' ') + '('
    943 		+ (Zotero.isMac ? '⇧⌘' : Zotero.getString('general.keys.ctrlShift'))
    944 		+ key
    945 		+ ')';
    946 	}
    947 	
    948 	return text;
    949 }
    950 
    951 Zotero_Browser.Tab.prototype.getCaptureCommand = function () {
    952 	switch (this.getCaptureState()) {
    953 	case this.CAPTURE_STATE_DISABLED:
    954 		return '';
    955 	case this.CAPTURE_STATE_TRANSLATABLE:
    956 		return '';
    957 	default:
    958 		return 'cmd_zotero_newItemFromCurrentPage';
    959 	}
    960 }
    961 
    962 
    963 /**********CALLBACKS**********/
    964 
    965 /*
    966  * called when a user is supposed to select items
    967  */
    968 Zotero_Browser.Tab.prototype._selectItems = function(obj, itemList, callback) {
    969 	// this is kinda ugly, mozillazine made me do it! honest!
    970 	var io = { dataIn:itemList, dataOut:null }
    971 	var newDialog = window.openDialog("chrome://zotero/content/ingester/selectitems.xul",
    972 		"_blank","chrome,modal,centerscreen,resizable=yes", io);
    973 	
    974 	if(!io.dataOut) {	// user selected no items, so close the progress indicatior
    975 		Zotero_Browser.progress.close();
    976 	}
    977 	
    978 	callback(io.dataOut);
    979 }
    980 
    981 /*
    982  * called when translators are available
    983  */
    984 Zotero_Browser.Tab.prototype._translatorsAvailable = Zotero.Promise.coroutine(function* (translate, translators) {
    985 	var page = this.getPageObject();
    986 	if (!page) return;
    987 	page.saveEnabled = true;
    988 	
    989 	if(translators && translators.length) {
    990 		//see if we should keep the previous set of translators
    991 		if(//we already have a translator for part of this page
    992 			page.translators && page.translators.length && page.document.location
    993 			//and the page is still there
    994 			&& page.document.defaultView && !page.document.defaultView.closed
    995 			//this set of translators is not targeting the same URL as a previous set of translators,
    996 			// because otherwise we want to use the newer set,
    997 			// but only if it's not in a subframe of the previous set
    998 			&& (page.document.location.href != translate.document.location.href ||
    999 				Zotero.Utilities.Internal.isIframeOf(translate.document.defaultView, page.document.defaultView))
   1000 				//the best translator we had was of higher priority than the new set
   1001 			&& (page.translators[0].priority < translators[0].priority
   1002 				//or the priority was the same, but...
   1003 				|| (page.translators[0].priority == translators[0].priority
   1004 					//the previous set of translators targets the top frame or the current one does not either
   1005 					&& (page.document.defaultView == page.document.defaultView.top
   1006 						|| translate.document.defaultView !== page.document.defaultView.top)
   1007 			))
   1008 		) {
   1009 			Zotero.debug("Translate: a better translator was already found for this page");
   1010 			return; //keep what we had
   1011 		} else {
   1012 			this.clear(); //clear URL bar icon
   1013 			page = this.getPageObject();
   1014 			page.saveEnabled = true;
   1015 		}
   1016 		
   1017 		Zotero.debug("Translate: found translators for page\n"
   1018 			+ "Best translator: " + translators[0].label + " with priority " + translators[0].priority);
   1019 		page.translate = translate;
   1020 		page.translators = translators;
   1021 		page.document = translate.document;
   1022 	
   1023 		translate.clearHandlers("select");
   1024 		translate.setHandler("select", this._selectItems);
   1025 	} else if(translate.type != "import" && translate.document.documentURI.length > 7
   1026 			&& translate.document.documentURI.substr(0, 7) == "file://") {
   1027 		this._attemptLocalFileImport(translate.document);
   1028 	}
   1029 	
   1030 	if(!translators || !translators.length) Zotero.debug("Translate: No translators found");
   1031 	
   1032 	yield Zotero_Browser.updateStatus();
   1033 	yield Zotero_Browser.resolveDetectCallbacks();
   1034 });
   1035 
   1036 Zotero_Browser.init();