zoteroPane.js (150602B)
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 ***** END LICENSE BLOCK ***** 24 */ 25 26 /* 27 * This object contains the various functions for the interface 28 */ 29 var ZoteroPane = new function() 30 { 31 var _unserialized = false; 32 this.collectionsView = false; 33 this.itemsView = false; 34 this.progressWindow = false; 35 this._listeners = {}; 36 this.__defineGetter__('loaded', function () { return _loaded; }); 37 var _lastSelectedItems = []; 38 39 //Privileged methods 40 this.destroy = destroy; 41 this.isShowing = isShowing; 42 this.isFullScreen = isFullScreen; 43 this.handleKeyDown = handleKeyDown; 44 this.handleKeyUp = handleKeyUp; 45 this.setHighlightedRowsCallback = setHighlightedRowsCallback; 46 this.handleKeyPress = handleKeyPress; 47 this.getSelectedCollection = getSelectedCollection; 48 this.getSelectedSavedSearch = getSelectedSavedSearch; 49 this.getSelectedItems = getSelectedItems; 50 this.getSortedItems = getSortedItems; 51 this.getSortField = getSortField; 52 this.getSortDirection = getSortDirection; 53 this.setItemsPaneMessage = setItemsPaneMessage; 54 this.clearItemsPaneMessage = clearItemsPaneMessage; 55 this.contextPopupShowing = contextPopupShowing; 56 this.viewSelectedAttachment = viewSelectedAttachment; 57 this.reportErrors = reportErrors; 58 this.displayErrorMessage = displayErrorMessage; 59 60 this.document = document; 61 62 const COLLECTIONS_HEIGHT = 32; // minimum height of the collections pane and toolbar 63 64 var self = this, 65 _loaded = false, _madeVisible = false, 66 titlebarcolorState, titleState, observerService, 67 _reloadFunctions = [], _beforeReloadFunctions = []; 68 69 /** 70 * Called when the window containing Zotero pane is open 71 */ 72 this.init = function () { 73 Zotero.debug("Initializing Zotero pane"); 74 75 // For now, keep actions menu in the DOM and show it in Firefox for development 76 if (!Zotero.isStandalone) { 77 document.getElementById('zotero-tb-actions-menu-separator').hidden = false; 78 document.getElementById('zotero-tb-actions-menu').hidden = false; 79 } 80 81 // Set "Report Errors..." label via property rather than DTD entity, 82 // since we need to reference it in script elsewhere 83 document.getElementById('zotero-tb-actions-reportErrors').setAttribute('label', 84 Zotero.getString('errorReport.reportErrors')); 85 // Set key down handler 86 document.getElementById('appcontent').addEventListener('keydown', ZoteroPane_Local.handleKeyDown, true); 87 88 // Hide or show the PDF recognizer button 89 Zotero.RecognizePDF.addListener('empty', function (row) { 90 document.getElementById('zotero-tb-recognize').hidden = true; 91 }); 92 93 Zotero.RecognizePDF.addListener('nonempty', function (row) { 94 document.getElementById('zotero-tb-recognize').hidden = false; 95 }); 96 97 _loaded = true; 98 99 var zp = document.getElementById('zotero-pane'); 100 Zotero.setFontSize(zp); 101 ZoteroPane_Local.updateLayout(); 102 ZoteroPane_Local.updateToolbarPosition(); 103 this.updateWindow(); 104 window.addEventListener("resize", () => { 105 this.updateWindow(); 106 this.updateToolbarPosition(); 107 }); 108 window.setTimeout(ZoteroPane_Local.updateToolbarPosition, 0); 109 110 Zotero.updateQuickSearchBox(document); 111 112 if (Zotero.isMac) { 113 //document.getElementById('zotero-tb-actions-zeroconf-update').setAttribute('hidden', false); 114 document.getElementById('zotero-pane-stack').setAttribute('platform', 'mac'); 115 } else if(Zotero.isWin) { 116 document.getElementById('zotero-pane-stack').setAttribute('platform', 'win'); 117 } 118 119 // Set the sync tooltip label 120 Components.utils.import("resource://zotero/config.js"); 121 document.getElementById('zotero-tb-sync-label').value = Zotero.getString( 122 'sync.syncWith', ZOTERO_CONFIG.DOMAIN_NAME 123 ); 124 125 if (Zotero.isStandalone) { 126 document.getElementById('zotero-tb-feed-add-fromPage').hidden = true; 127 document.getElementById('zotero-tb-feed-add-fromPage-menu').hidden = true; 128 } 129 130 // register an observer for Zotero reload 131 observerService = Components.classes["@mozilla.org/observer-service;1"] 132 .getService(Components.interfaces.nsIObserverService); 133 observerService.addObserver(_reloadObserver, "zotero-reloaded", false); 134 observerService.addObserver(_reloadObserver, "zotero-before-reload", false); 135 this.addBeforeReloadListener(function(newMode) { 136 if(newMode == "connector") { 137 ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('connector.standaloneOpen')); 138 } 139 return; 140 }); 141 this.addReloadListener(_loadPane); 142 143 // continue loading pane 144 _loadPane(); 145 }; 146 147 /** 148 * Called on window load or when pane has been reloaded after switching into or out of connector 149 * mode 150 */ 151 function _loadPane() { 152 if(!Zotero || !Zotero.initialized || Zotero.isConnector) return; 153 154 // Set flags for hi-res displays 155 Zotero.hiDPI = window.devicePixelRatio > 1; 156 Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : ""; 157 158 ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); 159 160 // Add a default progress window 161 ZoteroPane_Local.progressWindow = new Zotero.ProgressWindow({ window }); 162 163 //Initialize collections view 164 ZoteroPane_Local.collectionsView = new Zotero.CollectionTreeView(); 165 // Handle an error in setTree()/refresh() 166 ZoteroPane_Local.collectionsView.onError = function (e) { 167 ZoteroPane_Local.displayErrorMessage(); 168 }; 169 var collectionsTree = document.getElementById('zotero-collections-tree'); 170 collectionsTree.view = ZoteroPane_Local.collectionsView; 171 collectionsTree.controllers.appendController(new Zotero.CollectionTreeCommandController(collectionsTree)); 172 collectionsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true); 173 collectionsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true); 174 175 // Clear items view, so that the load registers as a new selected collection when switching 176 // between modes 177 ZoteroPane_Local.itemsView = null; 178 179 var itemsTree = document.getElementById('zotero-items-tree'); 180 itemsTree.controllers.appendController(new Zotero.ItemTreeCommandController(itemsTree)); 181 itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true); 182 itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true); 183 184 var menu = document.getElementById("contentAreaContextMenu"); 185 menu.addEventListener("popupshowing", ZoteroPane_Local.contextPopupShowing, false); 186 187 var tagSelector = document.getElementById('zotero-tag-selector'); 188 tagSelector.onchange = function () { 189 return ZoteroPane_Local.updateTagFilter(); 190 }; 191 192 Zotero.Keys.windowInit(document); 193 194 if (Zotero.restoreFromServer) { 195 Zotero.restoreFromServer = false; 196 197 setTimeout(function () { 198 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 199 .getService(Components.interfaces.nsIPromptService); 200 var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) 201 + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL); 202 var index = ps.confirmEx( 203 null, 204 "Zotero Restore", 205 "The local Zotero database has been cleared." 206 + " " 207 + "Would you like to restore from the Zotero server now?", 208 buttonFlags, 209 "Sync Now", 210 null, null, null, {} 211 ); 212 213 if (index == 0) { 214 Zotero.Sync.Server.sync({ 215 onSuccess: function () { 216 Zotero.Sync.Runner.updateIcons(); 217 218 ps.alert( 219 null, 220 "Restore Completed", 221 "The local Zotero database has been successfully restored." 222 ); 223 }, 224 225 onError: function (msg) { 226 ps.alert( 227 null, 228 "Restore Failed", 229 "An error occurred while restoring from the server:\n\n" 230 + msg 231 ); 232 233 Zotero.Sync.Runner.error(msg); 234 } 235 }); 236 } 237 }, 1000); 238 } 239 // If the database was initialized or there are no sync credentials and 240 // Zotero hasn't been run before in this profile, display the start page 241 // -- this way the page won't be displayed when they sync their DB to 242 // another profile or if the DB is initialized erroneously (e.g. while 243 // switching data directory locations) 244 else if (Zotero.Prefs.get('firstRun2')) { 245 if (Zotero.Schema.dbInitialized || !Zotero.Sync.Server.enabled) { 246 setTimeout(function () { 247 ZoteroPane_Local.loadURI(ZOTERO_CONFIG.START_URL); 248 }, 400); 249 } 250 Zotero.Prefs.set('firstRun2', false); 251 try { 252 Zotero.Prefs.clear('firstRun'); 253 } 254 catch (e) {} 255 } 256 257 if (Zotero.openPane) { 258 Zotero.openPane = false; 259 setTimeout(function () { 260 ZoteroPane_Local.show(); 261 }, 0); 262 } 263 264 // TEMP: Clean up extra files from Mendeley imports <5.0.51 265 setTimeout(async function () { 266 var needsCleanup = await Zotero.DB.valueQueryAsync( 267 "SELECT COUNT(*) FROM settings WHERE setting='mImport' AND key='cleanup'" 268 ) 269 if (!needsCleanup) return; 270 271 Components.utils.import("chrome://zotero/content/import/mendeley/mendeleyImport.js"); 272 var importer = new Zotero_Import_Mendeley(); 273 importer.deleteNonPrimaryFiles(); 274 }, 10000) 275 } 276 277 278 /* 279 * Create the New Item (+) submenu with each item type 280 */ 281 this.buildItemTypeSubMenu = function () { 282 var moreMenu = document.getElementById('zotero-tb-add-more'); 283 284 while (moreMenu.hasChildNodes()) { 285 moreMenu.removeChild(moreMenu.firstChild); 286 } 287 288 // Sort by localized name 289 var t = Zotero.ItemTypes.getSecondaryTypes(); 290 var itemTypes = []; 291 for (var i=0; i<t.length; i++) { 292 itemTypes.push({ 293 id: t[i].id, 294 name: t[i].name, 295 localized: Zotero.ItemTypes.getLocalizedString(t[i].id) 296 }); 297 } 298 var collation = Zotero.getLocaleCollation(); 299 itemTypes.sort(function(a, b) { 300 return collation.compareString(1, a.localized, b.localized); 301 }); 302 303 for (var i = 0; i<itemTypes.length; i++) { 304 var menuitem = document.createElement("menuitem"); 305 menuitem.setAttribute("label", itemTypes[i].localized); 306 menuitem.setAttribute("tooltiptext", ""); 307 let type = itemTypes[i].id; 308 menuitem.addEventListener("command", function() { ZoteroPane_Local.newItem(type, {}, null, true).done(); }, false); 309 moreMenu.appendChild(menuitem); 310 } 311 } 312 313 314 this.updateNewItemTypes = function () { 315 var addMenu = document.getElementById('zotero-tb-add').firstChild; 316 317 // Remove all nodes so we can regenerate 318 var options = addMenu.getElementsByAttribute("class", "zotero-tb-add"); 319 while (options.length) { 320 var p = options[0].parentNode; 321 p.removeChild(options[0]); 322 } 323 324 var separator = addMenu.firstChild; 325 326 // Sort by localized name 327 var t = Zotero.ItemTypes.getPrimaryTypes(); 328 var itemTypes = []; 329 for (var i=0; i<t.length; i++) { 330 itemTypes.push({ 331 id: t[i].id, 332 name: t[i].name, 333 localized: Zotero.ItemTypes.getLocalizedString(t[i].id) 334 }); 335 } 336 var collation = Zotero.getLocaleCollation(); 337 itemTypes.sort(function(a, b) { 338 return collation.compareString(1, a.localized, b.localized); 339 }); 340 341 for (var i = 0; i<itemTypes.length; i++) { 342 var menuitem = document.createElement("menuitem"); 343 menuitem.setAttribute("label", itemTypes[i].localized); 344 menuitem.setAttribute("tooltiptext", ""); 345 let type = itemTypes[i].id; 346 menuitem.addEventListener("command", function() { ZoteroPane_Local.newItem(type, {}, null, true).done(); }, false); 347 menuitem.className = "zotero-tb-add"; 348 addMenu.insertBefore(menuitem, separator); 349 } 350 } 351 352 353 354 /* 355 * Called when the window closes 356 */ 357 function destroy() 358 { 359 if (!Zotero || !Zotero.initialized || !_loaded) { 360 return; 361 } 362 363 if(this.isShowing()) { 364 this.serializePersist(); 365 } 366 367 var tagSelector = document.getElementById('zotero-tag-selector'); 368 tagSelector.unregister(); 369 370 if(this.collectionsView) this.collectionsView.unregister(); 371 if(this.itemsView) this.itemsView.unregister(); 372 373 observerService.removeObserver(_reloadObserver, "zotero-reloaded"); 374 } 375 376 /** 377 * Called before Zotero pane is to be made visible 378 * @return {Boolean} True if Zotero pane should be loaded, false otherwise (if an error 379 * occurred) 380 */ 381 this.makeVisible = Zotero.Promise.coroutine(function* () { 382 if (Zotero.locked) { 383 Zotero.showZoteroPaneProgressMeter(); 384 } 385 386 yield Zotero.unlockPromise; 387 388 // The items pane is hidden initially to avoid showing column lines 389 document.getElementById('zotero-items-tree').hidden = false; 390 Zotero.hideZoteroPaneOverlays(); 391 392 // If pane not loaded, load it or display an error message 393 if (!ZoteroPane_Local.loaded) { 394 ZoteroPane_Local.init(); 395 } 396 397 // If Zotero could not be initialized, display an error message and return 398 if (!Zotero || Zotero.skipLoading) { 399 this.displayStartupError(); 400 return false; 401 } 402 403 if(!_madeVisible) { 404 this.buildItemTypeSubMenu(); 405 } 406 _madeVisible = true; 407 408 this.unserializePersist(); 409 this.updateLayout(); 410 this.updateToolbarPosition(); 411 this.updateTagSelectorSize(); 412 413 // restore saved row selection (for tab switching) 414 // TODO: Remove now that no tab mode? 415 var containerWindow = window; 416 if(containerWindow.zoteroSavedCollectionSelection) { 417 this.collectionsView.onLoad.addListener(Zotero.Promise.coroutine(function* () { 418 yield this.collectionsView.selectByID(containerWindow.zoteroSavedCollectionSelection); 419 420 if (containerWindow.zoteroSavedItemSelection) { 421 this.itemsView.onLoad.addListener(function () { 422 this.itemsView.rememberSelection(containerWindow.zoteroSavedItemSelection); 423 delete containerWindow.zoteroSavedItemSelection; 424 }.bind(this)); 425 } 426 427 delete containerWindow.zoteroSavedCollectionSelection; 428 }.bind(this))); 429 } 430 431 // Focus the quicksearch on pane open 432 var searchBar = document.getElementById('zotero-tb-search'); 433 setTimeout(function () { 434 searchBar.inputField.select(); 435 }, 1); 436 437 // 438 // TEMP: Remove after people are no longer upgrading from Zotero for Firefox 439 // 440 var showFxProfileWarning = false; 441 var pref = 'firstRun.skipFirefoxProfileAccessCheck'; 442 if (Zotero.fxProfileAccessError != undefined && Zotero.fxProfileAccessError) { 443 showFxProfileWarning = true; 444 } 445 else if (!Zotero.Prefs.get(pref)) { 446 showFxProfileWarning = !(yield Zotero.Profile.checkFirefoxProfileAccess()); 447 } 448 if (showFxProfileWarning) { 449 Zotero.uiReadyPromise.delay(2000).then(function () { 450 var ps = Services.prompt; 451 var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING 452 + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; 453 var text = "Zotero was unable to access your Firefox profile to check for " 454 + "existing Zotero data.\n\n" 455 + "If you’ve upgraded from Zotero 4.0 for Firefox and don’t see the data " 456 + "you expect, it may be located elsewhere on your computer. " 457 + "Click “More Information” for help restoring your previous data.\n\n" 458 + "If you’re new to Zotero, you can ignore this message."; 459 var url = 'https://www.zotero.org/support/kb/data_missing_after_zotero_5_upgrade'; 460 var dontShowAgain = {}; 461 let index = ps.confirmEx(null, 462 Zotero.getString('general.warning'), 463 text, 464 buttonFlags, 465 Zotero.getString('general.moreInformation'), 466 "Ignore", 467 null, 468 Zotero.getString('general.dontShowAgain'), 469 dontShowAgain 470 ); 471 if (dontShowAgain.value) { 472 Zotero.Prefs.set(pref, true) 473 } 474 if (index == 0) { 475 this.loadURI(url); 476 } 477 }.bind(this)); 478 } 479 // Once we successfully find it once, don't bother checking again 480 else { 481 Zotero.Prefs.set(pref, true); 482 } 483 484 // Auto-sync on pane open or if new account 485 if (Zotero.Prefs.get('sync.autoSync') || Zotero.initAutoSync) { 486 yield Zotero.proxyAuthComplete; 487 yield Zotero.uiReadyPromise; 488 489 if (!Zotero.Sync.Runner.enabled) { 490 Zotero.debug('Sync not enabled -- skipping auto-sync', 4); 491 } 492 else if (Zotero.Sync.Runner.syncInProgress) { 493 Zotero.debug('Sync already running -- skipping auto-sync', 4); 494 } 495 else if (Zotero.Sync.Server.manualSyncRequired) { 496 Zotero.debug('Manual sync required -- skipping auto-sync', 4); 497 } 498 else if (showFxProfileWarning) { 499 Zotero.debug('Firefox profile access error -- skipping initial auto-sync', 4); 500 } 501 else { 502 Zotero.Sync.Runner.sync({ 503 background: true 504 }).then(() => Zotero.initAutoSync = false); 505 } 506 } 507 508 // Set sync icon to spinning if there's an existing sync 509 // 510 // We don't bother setting an existing error state at open 511 if (Zotero.Sync.Runner.syncInProgress) { 512 Zotero.Sync.Runner.updateIcons('animate'); 513 } 514 515 return true; 516 }); 517 518 /** 519 * Function to be called before ZoteroPane_Local is hidden. Does not actually hide the Zotero pane. 520 */ 521 this.makeHidden = function() { 522 this.serializePersist(); 523 } 524 525 function isShowing() { 526 var zoteroPane = document.getElementById('zotero-pane-stack'); 527 return zoteroPane 528 && zoteroPane.getAttribute('hidden') != 'true' 529 && zoteroPane.getAttribute('collapsed') != 'true'; 530 } 531 532 function isFullScreen() { 533 return document.getElementById('zotero-pane-stack').getAttribute('fullscreenmode') == 'true'; 534 } 535 536 537 /* 538 * Trigger actions based on keyboard shortcuts 539 */ 540 function handleKeyDown(event, from) { 541 try { 542 // Ignore keystrokes outside of Zotero pane 543 if (!(event.originalTarget.ownerDocument instanceof XULDocument)) { 544 return; 545 } 546 } 547 catch (e) { 548 Zotero.debug(e); 549 } 550 551 if (Zotero.locked) { 552 event.preventDefault(); 553 return; 554 } 555 556 if (from == 'zotero-pane') { 557 // Highlight collections containing selected items 558 // 559 // We use Control (17) on Windows because Alt triggers the menubar; 560 // otherwise we use Alt/Option (18) 561 if ((Zotero.isWin && event.keyCode == 17 && !event.altKey) || 562 (!Zotero.isWin && event.keyCode == 18 && !event.ctrlKey) 563 && !event.shiftKey && !event.metaKey) { 564 565 this.highlightTimer = Components.classes["@mozilla.org/timer;1"]. 566 createInstance(Components.interfaces.nsITimer); 567 // {} implements nsITimerCallback 568 this.highlightTimer.initWithCallback({ 569 notify: ZoteroPane_Local.setHighlightedRowsCallback 570 }, 225, Components.interfaces.nsITimer.TYPE_ONE_SHOT); 571 } 572 // Unhighlight on key up 573 else if ((Zotero.isWin && event.ctrlKey) || 574 (!Zotero.isWin && event.altKey)) { 575 if (this.highlightTimer) { 576 this.highlightTimer.cancel(); 577 this.highlightTimer = null; 578 } 579 ZoteroPane_Local.collectionsView.setHighlightedRows(); 580 } 581 } 582 } 583 584 function handleKeyUp(event) { 585 var from = event.originalTarget.id; 586 if (from == 'zotero-items-tree') { 587 if ((Zotero.isWin && event.keyCode == 17) || 588 (!Zotero.isWin && event.keyCode == 18)) { 589 if (this.highlightTimer) { 590 this.highlightTimer.cancel(); 591 this.highlightTimer = null; 592 } 593 ZoteroPane_Local.collectionsView.setHighlightedRows(); 594 return; 595 } 596 } 597 } 598 599 600 /* 601 * Highlights collections containing selected items on Ctrl (Win) or 602 * Option/Alt (Mac/Linux) press 603 */ 604 function setHighlightedRowsCallback() { 605 var itemIDs = ZoteroPane_Local.getSelectedItems(true); 606 // If no items or an unreasonable number, don't try 607 if (!itemIDs || !itemIDs.length || itemIDs.length > 100) return; 608 609 Zotero.Promise.coroutine(function* () { 610 var collectionIDs = yield Zotero.Collections.getCollectionsContainingItems(itemIDs, true); 611 var ids = collectionIDs.map(id => "C" + id); 612 var userLibraryID = Zotero.Libraries.userLibraryID; 613 var allInPublications = Zotero.Items.get(itemIDs).every((item) => { 614 return item.libraryID == userLibraryID && item.inPublications; 615 }) 616 if (allInPublications) { 617 ids.push("P" + Zotero.Libraries.userLibraryID); 618 } 619 if (ids.length) { 620 ZoteroPane_Local.collectionsView.setHighlightedRows(ids); 621 } 622 })(); 623 } 624 625 626 function handleKeyPress(event) { 627 var from = event.originalTarget.id; 628 629 // Ignore keystrokes if Zotero pane is closed 630 var zoteroPane = document.getElementById('zotero-pane-stack'); 631 if (zoteroPane.getAttribute('hidden') == 'true' || 632 zoteroPane.getAttribute('collapsed') == 'true') { 633 return; 634 } 635 636 if (Zotero.locked) { 637 event.preventDefault(); 638 return; 639 } 640 641 var key = String.fromCharCode(event.which); 642 if (key) { 643 var command = Zotero.Keys.getCommand(key); 644 } 645 646 if (from == 'zotero-collections-tree') { 647 if ((event.keyCode == event.DOM_VK_BACK_SPACE && Zotero.isMac) || 648 event.keyCode == event.DOM_VK_DELETE) { 649 var deleteItems = event.metaKey || (!Zotero.isMac && event.shiftKey); 650 ZoteroPane_Local.deleteSelectedCollection(deleteItems); 651 event.preventDefault(); 652 return; 653 } 654 } 655 else if (from == 'zotero-items-tree') { 656 // Focus TinyMCE explicitly on tab key, since the normal focusing doesn't work right 657 if (!event.shiftKey && event.keyCode == event.DOM_VK_TAB) { 658 var deck = document.getElementById('zotero-item-pane-content'); 659 if (deck.selectedPanel.id == 'zotero-view-note') { 660 document.getElementById('zotero-note-editor').focus(); 661 event.preventDefault(); 662 return; 663 } 664 } 665 else if ((event.keyCode == event.DOM_VK_BACK_SPACE && Zotero.isMac) || 666 event.keyCode == event.DOM_VK_DELETE) { 667 // If Cmd/Shift delete, use forced mode, which does different 668 // things depending on the context 669 var force = event.metaKey || (!Zotero.isMac && event.shiftKey); 670 ZoteroPane_Local.deleteSelectedItems(force); 671 event.preventDefault(); 672 return; 673 } 674 else if (event.keyCode == event.DOM_VK_RETURN) { 675 var items = this.itemsView.getSelectedItems(); 676 // Don't do anything if more than 20 items selected 677 if (!items.length || items.length > 20) { 678 return; 679 } 680 ZoteroPane_Local.viewItems(items, event); 681 // These don't seem to do anything. Instead we override 682 // the tree binding's _handleEnter method in itemTreeView.js. 683 //event.preventDefault(); 684 //event.stopPropagation(); 685 return; 686 } 687 else if (command == 'toggleRead') { 688 // Toggle read/unread 689 let row = this.collectionsView.getRow(this.collectionsView.selection.currentIndex); 690 if (!row || !row.isFeed()) return; 691 this.toggleSelectedItemsRead(); 692 if (itemReadPromise) { 693 itemReadPromise.cancel(); 694 itemReadPromise = null; 695 } 696 return; 697 } 698 } 699 700 // Ignore modifiers other than Ctrl-Shift/Cmd-Shift 701 if (!((Zotero.isMac ? event.metaKey : event.ctrlKey) && event.shiftKey)) { 702 return; 703 } 704 705 if (!key) { 706 Zotero.debug('No key'); 707 return; 708 } 709 710 if (!command) { 711 return; 712 } 713 714 Zotero.debug('Keyboard shortcut: ' + command); 715 716 // Errors don't seem to make it out otherwise 717 try { 718 switch (command) { 719 case 'openZotero': 720 try { 721 // Ignore Cmd-Shift-Z keystroke in text areas 722 if (Zotero.isMac && key == 'Z' && 723 (event.originalTarget.localName == 'input' 724 || event.originalTarget.localName == 'textarea')) { 725 try { 726 var isSearchBar = event.originalTarget.parentNode.parentNode.id == 'zotero-tb-search'; 727 } 728 catch (e) { 729 Zotero.debug(e, 1); 730 Components.utils.reportError(e); 731 } 732 if (!isSearchBar) { 733 Zotero.debug('Ignoring keystroke in text field'); 734 return; 735 } 736 } 737 } 738 catch (e) { 739 Zotero.debug(e); 740 } 741 if (window.ZoteroOverlay) window.ZoteroOverlay.toggleDisplay() 742 break; 743 case 'library': 744 document.getElementById('zotero-collections-tree').focus(); 745 break; 746 case 'quicksearch': 747 document.getElementById('zotero-tb-search').select(); 748 break; 749 case 'newItem': 750 Zotero.Promise.coroutine(function* () { 751 // Default to most recent item type from here or the 752 // New Type menu 753 var mru = Zotero.Prefs.get('newItemTypeMRU'); 754 // Or fall back to 'book' 755 var typeID = mru ? mru.split(',')[0] : 2; 756 yield ZoteroPane_Local.newItem(typeID); 757 let itemBox = document.getElementById('zotero-editpane-item-box'); 758 var menu = itemBox.itemTypeMenu; 759 var self = this; 760 var handleTypeChange = function () { 761 self.addItemTypeToNewItemTypeMRU(this.itemTypeMenu.value); 762 itemBox.removeHandler('itemtypechange', handleTypeChange); 763 }; 764 // Only update the MRU when the menu is opened for the 765 // keyboard shortcut, not on subsequent opens 766 var removeTypeChangeHandler = function () { 767 itemBox.removeHandler('itemtypechange', handleTypeChange); 768 itemBox.itemTypeMenu.firstChild.removeEventListener('popuphiding', removeTypeChangeHandler); 769 // Focus the title field after menu closes 770 itemBox.focusFirstField(); 771 }; 772 itemBox.addHandler('itemtypechange', handleTypeChange); 773 itemBox.itemTypeMenu.firstChild.addEventListener('popuphiding', removeTypeChangeHandler); 774 775 menu.focus(); 776 document.getElementById('zotero-editpane-item-box').itemTypeMenu.menupopup.openPopup(menu, "before_start", 0, 0); 777 })(); 778 break; 779 case 'newNote': 780 // If a regular item is selected, use that as the parent. 781 // If a child item is selected, use its parent as the parent. 782 // Otherwise create a standalone note. 783 var parentKey = false; 784 var items = ZoteroPane_Local.getSelectedItems(); 785 if (items.length == 1) { 786 if (items[0].isRegularItem()) { 787 parentKey = items[0].key; 788 } 789 else { 790 parentKey = items[0].parentItemKey; 791 } 792 } 793 // Use key that's not the modifier as the popup toggle 794 ZoteroPane_Local.newNote(event.altKey, parentKey); 795 break; 796 case 'toggleTagSelector': 797 ZoteroPane_Local.toggleTagSelector(); 798 break; 799 case 'sync': 800 Zotero.Sync.Runner.sync(); 801 break; 802 case 'saveToZotero': 803 var collectionTreeRow = this.collectionsView.selectedTreeRow; 804 if (collectionTreeRow.isFeed()) { 805 ZoteroItemPane.translateSelectedItems(); 806 } else { 807 Zotero.debug(command + ' does not do anything in non-feed views') 808 } 809 break; 810 case 'toggleAllRead': 811 var collectionTreeRow = this.collectionsView.selectedTreeRow; 812 if (collectionTreeRow.isFeed()) { 813 this.markFeedRead(); 814 } 815 break; 816 817 // Handled by <key>s in standalone.js, pointing to <command>s in zoteroPane.xul, 818 // which are enabled or disabled by this.updateQuickCopyCommands(), called by 819 // this.itemSelected() 820 case 'copySelectedItemCitationsToClipboard': 821 case 'copySelectedItemsToClipboard': 822 return; 823 824 default: 825 throw ('Command "' + command + '" not found in ZoteroPane_Local.handleKeyDown()'); 826 } 827 } 828 catch (e) { 829 Zotero.debug(e, 1); 830 Components.utils.reportError(e); 831 } 832 833 event.preventDefault(); 834 } 835 836 837 /* 838 * Create a new item 839 * 840 * _data_ is an optional object with field:value for itemData 841 */ 842 this.newItem = Zotero.Promise.coroutine(function* (typeID, data, row, manual) 843 { 844 if ((row === undefined || row === null) && this.collectionsView.selection) { 845 row = this.collectionsView.selection.currentIndex; 846 847 // Make sure currently selected view is editable 848 if (!this.canEdit(row)) { 849 this.displayCannotEditLibraryMessage(); 850 return; 851 } 852 } 853 854 yield ZoteroItemPane.blurOpenField(); 855 856 if (row !== undefined && row !== null) { 857 var collectionTreeRow = this.collectionsView.getRow(row); 858 var libraryID = collectionTreeRow.ref.libraryID; 859 } 860 else { 861 var libraryID = Zotero.Libraries.userLibraryID; 862 var collectionTreeRow = null; 863 } 864 865 let itemID; 866 yield Zotero.DB.executeTransaction(function* () { 867 var item = new Zotero.Item(typeID); 868 item.libraryID = libraryID; 869 for (var i in data) { 870 item.setField(i, data[i]); 871 } 872 itemID = yield item.save(); 873 874 if (collectionTreeRow && collectionTreeRow.isCollection()) { 875 yield collectionTreeRow.ref.addItem(itemID); 876 } 877 }); 878 879 //set to Info tab 880 document.getElementById('zotero-view-item').selectedIndex = 0; 881 882 if (manual) { 883 // Update most-recently-used list for New Item menu 884 this.addItemTypeToNewItemTypeMRU(typeID); 885 886 // Focus the title field 887 document.getElementById('zotero-editpane-item-box').focusFirstField(); 888 } 889 890 return Zotero.Items.getAsync(itemID); 891 }); 892 893 894 this.addItemTypeToNewItemTypeMRU = function (itemTypeID) { 895 var mru = Zotero.Prefs.get('newItemTypeMRU'); 896 if (mru) { 897 var mru = mru.split(','); 898 var pos = mru.indexOf(itemTypeID + ''); 899 if (pos != -1) { 900 mru.splice(pos, 1); 901 } 902 mru.unshift(itemTypeID); 903 } 904 else { 905 var mru = [itemTypeID + '']; 906 } 907 Zotero.Prefs.set('newItemTypeMRU', mru.slice(0, 5).join(',')); 908 } 909 910 911 this.newCollection = Zotero.Promise.coroutine(function* (parentKey) { 912 if (!this.canEditLibrary()) { 913 this.displayCannotEditLibraryMessage(); 914 return; 915 } 916 917 var libraryID = this.getSelectedLibraryID(); 918 919 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 920 .getService(Components.interfaces.nsIPromptService); 921 var untitled = yield Zotero.DB.getNextName( 922 libraryID, 923 'collections', 924 'collectionName', 925 Zotero.getString('pane.collections.untitled') 926 ); 927 928 var newName = { value: untitled }; 929 var result = promptService.prompt(window, 930 Zotero.getString('pane.collections.newCollection'), 931 Zotero.getString('pane.collections.name'), newName, "", {}); 932 933 if (!result) 934 { 935 return; 936 } 937 938 if (!newName.value) 939 { 940 newName.value = untitled; 941 } 942 943 var collection = new Zotero.Collection; 944 collection.libraryID = libraryID; 945 collection.name = newName.value; 946 collection.parentKey = parentKey; 947 return collection.saveTx(); 948 }); 949 950 this.importFeedsFromOPML = Zotero.Promise.coroutine(function* (event) { 951 var nsIFilePicker = Components.interfaces.nsIFilePicker; 952 while (true) { 953 var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); 954 fp.init(window, Zotero.getString('fileInterface.importOPML'), nsIFilePicker.modeOpen); 955 fp.appendFilter(Zotero.getString('fileInterface.OPMLFeedFilter'), '*.opml; *.xml'); 956 fp.appendFilters(nsIFilePicker.filterAll); 957 if (fp.show() == nsIFilePicker.returnOK) { 958 var contents = yield Zotero.File.getContentsAsync(fp.file.path); 959 var success = yield Zotero.Feeds.importFromOPML(contents); 960 if (success) { 961 return true; 962 } 963 // Try again 964 Zotero.alert(window, Zotero.getString('general.error'), Zotero.getString('fileInterface.unsupportedFormat')); 965 } else { 966 return false; 967 } 968 } 969 }); 970 971 this.newFeedFromPage = Zotero.Promise.coroutine(function* (event) { 972 let data = {unsaved: true}; 973 if (event) { 974 data.url = event.target.getAttribute('feed'); 975 } else { 976 data.url = gBrowser.selectedBrowser.feeds[0].href; 977 } 978 window.openDialog('chrome://zotero/content/feedSettings.xul', 979 null, 'centerscreen, modal', data); 980 if (!data.cancelled) { 981 let feed = new Zotero.Feed(); 982 feed.url = data.url; 983 feed.name = data.title; 984 feed.refreshInterval = data.ttl; 985 feed.cleanupReadAfter = data.cleanupReadAfter; 986 feed.cleanupUnreadAfter = data.cleanupUnreadAfter; 987 yield feed.saveTx(); 988 yield feed.updateFeed(); 989 } 990 }); 991 992 this.newFeedFromURL = Zotero.Promise.coroutine(function* () { 993 let data = {}; 994 window.openDialog('chrome://zotero/content/feedSettings.xul', 995 null, 'centerscreen, modal', data); 996 if (!data.cancelled) { 997 let feed = new Zotero.Feed(); 998 feed.url = data.url; 999 feed.name = data.title; 1000 feed.refreshInterval = data.ttl; 1001 feed.cleanupReadAfter = data.cleanupReadAfter; 1002 feed.cleanupUnreadAfter = data.cleanupUnreadAfter; 1003 yield feed.saveTx(); 1004 yield feed.updateFeed(); 1005 } 1006 }); 1007 1008 this.newGroup = function () { 1009 this.loadURI(Zotero.Groups.addGroupURL); 1010 } 1011 1012 1013 this.newSearch = Zotero.Promise.coroutine(function* () { 1014 if (Zotero.DB.inTransaction()) { 1015 yield Zotero.DB.waitForTransaction(); 1016 } 1017 1018 var s = new Zotero.Search(); 1019 s.libraryID = this.getSelectedLibraryID(); 1020 s.addCondition('title', 'contains', ''); 1021 1022 var untitled = Zotero.getString('pane.collections.untitled'); 1023 untitled = yield Zotero.DB.getNextName( 1024 s.libraryID, 1025 'savedSearches', 1026 'savedSearchName', 1027 Zotero.getString('pane.collections.untitled') 1028 ); 1029 var io = {dataIn: {search: s, name: untitled}, dataOut: null}; 1030 window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io); 1031 if (!io.dataOut) { 1032 return false; 1033 } 1034 s.fromJSON(io.dataOut.json); 1035 yield s.saveTx(); 1036 return s.id; 1037 }); 1038 1039 1040 this.setVirtual = Zotero.Promise.coroutine(function* (libraryID, type, show) { 1041 switch (type) { 1042 case 'duplicates': 1043 var treeViewID = 'D' + libraryID; 1044 break; 1045 1046 case 'unfiled': 1047 var treeViewID = 'U' + libraryID; 1048 break; 1049 1050 default: 1051 throw new Error("Invalid virtual collection type '" + type + "'"); 1052 } 1053 1054 Zotero.Utilities.Internal.setVirtualCollectionStateForLibrary(libraryID, type, show); 1055 1056 var cv = this.collectionsView; 1057 1058 var promise = cv.waitForSelect(); 1059 var selectedRow = cv.selection.currentIndex; 1060 1061 yield cv.refresh(); 1062 1063 // Select new row 1064 if (show) { 1065 yield this.collectionsView.selectByID(treeViewID); 1066 } 1067 // Select next appropriate row after removal 1068 else { 1069 this.collectionsView.selectAfterRowRemoval(selectedRow); 1070 } 1071 1072 this.collectionsView.selection.selectEventsSuppressed = false; 1073 1074 return promise; 1075 }); 1076 1077 1078 this.openLookupWindow = Zotero.Promise.coroutine(function* () { 1079 if (Zotero.DB.inTransaction()) { 1080 yield Zotero.DB.waitForTransaction(); 1081 } 1082 1083 if (!this.canEdit()) { 1084 this.displayCannotEditLibraryMessage(); 1085 return; 1086 } 1087 1088 window.openDialog('chrome://zotero/content/lookup.xul', 'zotero-lookup', 'chrome,modal'); 1089 }); 1090 1091 1092 this.openAdvancedSearchWindow = function () { 1093 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 1094 .getService(Components.interfaces.nsIWindowMediator); 1095 var enumerator = wm.getEnumerator('zotero:search'); 1096 while (enumerator.hasMoreElements()) { 1097 var win = enumerator.getNext(); 1098 } 1099 1100 if (win) { 1101 win.focus(); 1102 return; 1103 } 1104 1105 var s = new Zotero.Search(); 1106 s.libraryID = this.getSelectedLibraryID(); 1107 s.addCondition('title', 'contains', ''); 1108 1109 var io = {dataIn: {search: s}, dataOut: null}; 1110 window.openDialog('chrome://zotero/content/advancedSearch.xul', '', 'chrome,dialog=no,centerscreen', io); 1111 }; 1112 1113 1114 this.toggleTagSelector = Zotero.Promise.coroutine(function* () { 1115 var tagSelector = document.getElementById('zotero-tag-selector'); 1116 1117 var showing = tagSelector.getAttribute('collapsed') == 'true'; 1118 tagSelector.setAttribute('collapsed', !showing); 1119 this.updateTagSelectorSize(); 1120 1121 // If showing, set scope to items in current view 1122 // and focus filter textbox 1123 if (showing) { 1124 yield this.setTagScope(); 1125 tagSelector.focusTextbox(); 1126 } 1127 // If hiding, clear selection 1128 else { 1129 tagSelector.uninit(); 1130 } 1131 }); 1132 1133 1134 this.updateTagSelectorSize = function () { 1135 //Zotero.debug('Updating tag selector size'); 1136 var zoteroPane = document.getElementById('zotero-pane-stack'); 1137 var splitter = document.getElementById('zotero-tags-splitter'); 1138 var tagSelector = document.getElementById('zotero-tag-selector'); 1139 1140 // Nothing should be bigger than appcontent's height 1141 var max = document.getElementById('appcontent').boxObject.height 1142 - splitter.boxObject.height; 1143 1144 // Shrink tag selector to appcontent's height 1145 var maxTS = max - COLLECTIONS_HEIGHT; 1146 if (parseInt(tagSelector.getAttribute("height")) > maxTS) { 1147 //Zotero.debug("Limiting tag selector height to appcontent"); 1148 tagSelector.setAttribute('height', maxTS); 1149 } 1150 1151 var height = tagSelector.boxObject.height; 1152 1153 1154 /*Zotero.debug("tagSelector.boxObject.height: " + tagSelector.boxObject.height); 1155 Zotero.debug("tagSelector.getAttribute('height'): " + tagSelector.getAttribute('height')); 1156 Zotero.debug("zoteroPane.boxObject.height: " + zoteroPane.boxObject.height); 1157 Zotero.debug("zoteroPane.getAttribute('height'): " + zoteroPane.getAttribute('height'));*/ 1158 1159 1160 // Don't let the Z-pane jump back down to its previous height 1161 // (if shrinking or hiding the tag selector let it clear the min-height) 1162 if (zoteroPane.getAttribute('height') < zoteroPane.boxObject.height) { 1163 //Zotero.debug("Setting Zotero pane height attribute to " + zoteroPane.boxObject.height); 1164 zoteroPane.setAttribute('height', zoteroPane.boxObject.height); 1165 } 1166 1167 if (tagSelector.getAttribute('collapsed') == 'true') { 1168 // 32px is the default Z pane min-height in overlay.css 1169 height = 32; 1170 } 1171 else { 1172 // tS.boxObject.height doesn't exist at startup, so get from attribute 1173 if (!height) { 1174 height = parseInt(tagSelector.getAttribute('height')); 1175 } 1176 // 121px seems to be enough room for the toolbar and collections 1177 // tree at minimum height 1178 height = height + COLLECTIONS_HEIGHT; 1179 } 1180 1181 //Zotero.debug('Setting Zotero pane minheight to ' + height); 1182 zoteroPane.setAttribute('minheight', height); 1183 1184 if (this.isShowing() && !this.isFullScreen()) { 1185 zoteroPane.setAttribute('savedHeight', zoteroPane.boxObject.height); 1186 } 1187 1188 // Fix bug whereby resizing the Z pane downward after resizing 1189 // the tag selector up and then down sometimes caused the Z pane to 1190 // stay at a fixed size and get pushed below the bottom 1191 tagSelector.height++; 1192 tagSelector.height--; 1193 } 1194 1195 1196 function getTagSelection() { 1197 var tagSelector = document.getElementById('zotero-tag-selector'); 1198 return tagSelector.selection ? tagSelector.selection : new Set(); 1199 } 1200 1201 1202 this.clearTagSelection = function () { 1203 document.getElementById('zotero-tag-selector').deselectAll(); 1204 } 1205 1206 1207 /* 1208 * Sets the tag filter on the items view 1209 */ 1210 this.updateTagFilter = Zotero.Promise.coroutine(function* () { 1211 if (this.itemsView) { 1212 yield this.itemsView.setFilter('tags', getTagSelection()); 1213 } 1214 }); 1215 1216 1217 this.tagSelectorShown = function () { 1218 var collectionTreeRow = this.getCollectionTreeRow(); 1219 if (!collectionTreeRow) return; 1220 var tagSelector = document.getElementById('zotero-tag-selector'); 1221 return !tagSelector.getAttribute('collapsed') 1222 || tagSelector.getAttribute('collapsed') == 'false'; 1223 }; 1224 1225 1226 /* 1227 * Set the tags scope to the items in the current view 1228 * 1229 * Passed to the items tree to trigger on changes 1230 */ 1231 this.setTagScope = Zotero.Promise.coroutine(function* () { 1232 var collectionTreeRow = this.getCollectionTreeRow(); 1233 var tagSelector = document.getElementById('zotero-tag-selector'); 1234 if (this.tagSelectorShown()) { 1235 Zotero.debug('Updating tag selector with current tags'); 1236 if (collectionTreeRow.editable) { 1237 tagSelector.mode = 'edit'; 1238 } 1239 else { 1240 tagSelector.mode = 'view'; 1241 } 1242 tagSelector.collectionTreeRow = collectionTreeRow; 1243 tagSelector.updateScope = () => this.setTagScope(); 1244 tagSelector.libraryID = collectionTreeRow.ref.libraryID; 1245 tagSelector.scope = yield collectionTreeRow.getChildTags(); 1246 } 1247 }); 1248 1249 1250 this.onCollectionSelected = function () { 1251 return Zotero.spawn(function* () { 1252 var collectionTreeRow = this.getCollectionTreeRow(); 1253 if (!collectionTreeRow) { 1254 return; 1255 } 1256 1257 if (this.itemsView && this.itemsView.collectionTreeRow.id == collectionTreeRow.id) { 1258 Zotero.debug("Collection selection hasn't changed"); 1259 1260 // Update toolbar, in case editability has changed 1261 this._updateToolbarIconsForRow(collectionTreeRow); 1262 return; 1263 } 1264 1265 if (this.itemsView) { 1266 // Wait for existing items view to finish loading before unloading it 1267 // 1268 // TODO: Cancel loading 1269 let promise = this.itemsView.waitForLoad(); 1270 if (promise.isPending()) { 1271 Zotero.debug("Waiting for items view " + this.itemsView.id + " to finish loading"); 1272 yield promise; 1273 } 1274 1275 this.itemsView.unregister(); 1276 document.getElementById('zotero-items-tree').view = this.itemsView = null; 1277 } 1278 1279 if (this.collectionsView.selection.count != 1) { 1280 return; 1281 } 1282 1283 // Clear quick search and tag selector when switching views 1284 document.getElementById('zotero-tb-search').value = ""; 1285 1286 // XBL functions might not yet be available 1287 var tagSelector = document.getElementById('zotero-tag-selector'); 1288 if (tagSelector.deselectAll) { 1289 tagSelector.deselectAll(); 1290 } 1291 1292 // Not necessary with seltype="cell", which calls nsITreeView::isSelectable() 1293 /*if (collectionTreeRow.isSeparator()) { 1294 document.getElementById('zotero-items-tree').view = this.itemsView = null; 1295 return; 1296 }*/ 1297 1298 collectionTreeRow.setSearch(''); 1299 collectionTreeRow.setTags(getTagSelection()); 1300 1301 this._updateToolbarIconsForRow(collectionTreeRow); 1302 1303 this.itemsView = new Zotero.ItemTreeView(collectionTreeRow); 1304 if (collectionTreeRow.isPublications()) { 1305 this.itemsView.collapseAll = true; 1306 } 1307 this.itemsView.onError = function () { 1308 // Don't reload last folder, in case that's the problem 1309 Zotero.Prefs.clear('lastViewedFolder'); 1310 ZoteroPane_Local.displayErrorMessage(); 1311 }; 1312 this.itemsView.onRefresh.addListener(() => this.setTagScope()); 1313 if (this.tagSelectorShown()) { 1314 let tagSelector = document.getElementById('zotero-tag-selector') 1315 let handler = function () { 1316 tagSelector.removeEventListener('refresh', handler); 1317 Zotero.uiIsReady(); 1318 }; 1319 tagSelector.addEventListener('refresh', handler); 1320 } 1321 else { 1322 this.itemsView.onLoad.addListener(() => Zotero.uiIsReady()); 1323 } 1324 1325 // If item data not yet loaded for library, load it now. 1326 // Other data types are loaded at startup 1327 var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID); 1328 if (!library.getDataLoaded('item')) { 1329 Zotero.debug("Waiting for items to load for library " + library.libraryID); 1330 ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); 1331 yield library.waitForDataLoad('item'); 1332 } 1333 1334 document.getElementById('zotero-items-tree').view = this.itemsView; 1335 1336 try { 1337 let tree = document.getElementById('zotero-items-tree'); 1338 let treecols = document.getElementById('zotero-items-columns-header'); 1339 let treecolpicker = treecols.boxObject.firstChild.nextSibling; 1340 let menupopup = treecolpicker.boxObject.firstChild.nextSibling; 1341 // Add events to treecolpicker to update menu before showing/hiding 1342 let attr = menupopup.getAttribute('onpopupshowing'); 1343 if (attr.indexOf('Zotero') == -1) { 1344 menupopup.setAttribute('onpopupshowing', 'ZoteroPane.itemsView.onColumnPickerShowing(event); ' 1345 // Keep whatever else is there 1346 + attr); 1347 menupopup.setAttribute('onpopuphidden', 'ZoteroPane.itemsView.onColumnPickerHidden(event); ' 1348 // Keep whatever else is there 1349 + menupopup.getAttribute('onpopuphidden')); 1350 } 1351 1352 // Items view column visibility for different groups 1353 let prevViewGroup = tree.getAttribute('current-view-group'); 1354 let curViewGroup = collectionTreeRow.visibilityGroup; 1355 tree.setAttribute('current-view-group', curViewGroup); 1356 if (curViewGroup != prevViewGroup) { 1357 let cols = Array.from(treecols.getElementsByTagName('treecol')); 1358 let settings = JSON.parse(Zotero.Prefs.get('itemsView.columnVisibility') || '{}'); 1359 if (prevViewGroup) { 1360 // Store previous view settings 1361 let setting = {}; 1362 for (let col of cols) { 1363 let colType = col.id.substring('zotero-items-column-'.length); 1364 setting[colType] = col.getAttribute('hidden') == 'true' ? 0 : 1 1365 } 1366 settings[prevViewGroup] = setting; 1367 Zotero.Prefs.set('itemsView.columnVisibility', JSON.stringify(settings)); 1368 } 1369 1370 // Recover current view settings 1371 if (settings[curViewGroup]) { 1372 for (let col of cols) { 1373 let colType = col.id.substring('zotero-items-column-'.length); 1374 col.setAttribute('hidden', !settings[curViewGroup][colType]); 1375 } 1376 } else { 1377 cols.forEach((col) => { 1378 col.setAttribute('hidden', !(col.hasAttribute('default-in') && 1379 col.getAttribute('default-in').split(' ').indexOf(curViewGroup) != -1) 1380 ) 1381 }) 1382 } 1383 } 1384 } 1385 catch (e) { 1386 Zotero.debug(e); 1387 } 1388 1389 Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id); 1390 }, this) 1391 .finally(function () { 1392 return this.collectionsView.runListeners('select'); 1393 }.bind(this)); 1394 }; 1395 1396 1397 /** 1398 * Enable or disable toolbar icons and menu options as necessary 1399 */ 1400 this._updateToolbarIconsForRow = function (collectionTreeRow) { 1401 const disableIfNoEdit = [ 1402 "cmd_zotero_newCollection", 1403 "cmd_zotero_newSavedSearch", 1404 "zotero-tb-add", 1405 "cmd_zotero_newItemFromCurrentPage", 1406 "zotero-tb-lookup", 1407 "cmd_zotero_newStandaloneNote", 1408 "zotero-tb-note-add", 1409 "zotero-tb-attachment-add" 1410 ]; 1411 for (let i = 0; i < disableIfNoEdit.length; i++) { 1412 let command = disableIfNoEdit[i]; 1413 let el = document.getElementById(command); 1414 1415 // If a trash is selected, new collection depends on the 1416 // editability of the library 1417 if (collectionTreeRow.isTrash() && command == 'cmd_zotero_newCollection') { 1418 var overrideEditable = Zotero.Libraries.get(collectionTreeRow.ref.libraryID).editable; 1419 } 1420 else { 1421 var overrideEditable = false; 1422 } 1423 1424 // Don't allow normal buttons in My Publications, because things need to 1425 // be dragged and go through the wizard 1426 let forceDisable = collectionTreeRow.isPublications() && command != 'zotero-tb-note-add'; 1427 1428 if ((collectionTreeRow.editable || overrideEditable) && !forceDisable) { 1429 if(el.hasAttribute("disabled")) el.removeAttribute("disabled"); 1430 } else { 1431 el.setAttribute("disabled", "true"); 1432 } 1433 } 1434 }; 1435 1436 1437 this.getCollectionTreeRow = function () { 1438 if (!this.collectionsView || !this.collectionsView.selection.count) { 1439 return false; 1440 } 1441 return this.collectionsView.getRow(this.collectionsView.selection.currentIndex); 1442 } 1443 1444 1445 /** 1446 * @return {Promise<Boolean>} - Promise that resolves to true if an item was selected, 1447 * or false if not (used for tests, though there could possibly 1448 * be a better test for whether the item pane changed) 1449 */ 1450 this.itemSelected = function (event) { 1451 return Zotero.Promise.coroutine(function* () { 1452 // Don't select item until items list has loaded 1453 // 1454 // This avoids an error if New Item is used while the pane is first loading. 1455 var promise = this.itemsView.waitForLoad(); 1456 if (promise.isPending()) { 1457 yield promise; 1458 } 1459 1460 if (!this.itemsView || !this.itemsView.selection) { 1461 Zotero.debug("Items view not available in itemSelected", 2); 1462 return false; 1463 } 1464 1465 var selectedItems = this.itemsView.getSelectedItems(); 1466 1467 // Display buttons at top of item pane depending on context. This needs to run even if the 1468 // selection hasn't changed, because the selected items might have been modified. 1469 this.updateItemPaneButtons(selectedItems); 1470 1471 this.updateQuickCopyCommands(selectedItems); 1472 1473 // Check if selection has actually changed. The onselect event that calls this 1474 // can be called in various situations where the selection didn't actually change, 1475 // such as whenever selectEventsSuppressed is set to false. 1476 var ids = selectedItems.map(item => item.id); 1477 ids.sort(); 1478 if (ids.length && Zotero.Utilities.arrayEquals(_lastSelectedItems, ids)) { 1479 return false; 1480 } 1481 _lastSelectedItems = ids; 1482 1483 var tabs = document.getElementById('zotero-view-tabbox'); 1484 1485 // save note when switching from a note 1486 if(document.getElementById('zotero-item-pane-content').selectedIndex == 2) { 1487 // TODO: only try to save when selected item is different 1488 yield document.getElementById('zotero-note-editor').save(); 1489 } 1490 1491 var collectionTreeRow = this.getCollectionTreeRow(); 1492 // I don't think this happens in normal usage, but it can happen during tests 1493 if (!collectionTreeRow) { 1494 return false; 1495 } 1496 1497 // Single item selected 1498 if (selectedItems.length == 1) { 1499 var item = selectedItems[0]; 1500 1501 if (item.isNote()) { 1502 ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable); 1503 } 1504 1505 else if (item.isAttachment()) { 1506 var attachmentBox = document.getElementById('zotero-attachment-box'); 1507 attachmentBox.mode = this.collectionsView.editable ? 'edit' : 'view'; 1508 attachmentBox.item = item; 1509 1510 document.getElementById('zotero-item-pane-content').selectedIndex = 3; 1511 } 1512 1513 // Regular item 1514 else { 1515 var isCommons = collectionTreeRow.isBucket(); 1516 1517 document.getElementById('zotero-item-pane-content').selectedIndex = 1; 1518 var tabBox = document.getElementById('zotero-view-tabbox'); 1519 1520 // Reset tab when viewing a feed item, which only has the info tab 1521 if (item.isFeedItem) { 1522 tabBox.selectedIndex = 0; 1523 } 1524 1525 var pane = tabBox.selectedIndex; 1526 tabBox.firstChild.hidden = isCommons; 1527 1528 var button = document.getElementById('zotero-item-show-original'); 1529 if (isCommons) { 1530 button.hidden = false; 1531 button.disabled = !this.getOriginalItem(); 1532 } 1533 else { 1534 button.hidden = true; 1535 } 1536 1537 if (this.collectionsView.editable) { 1538 yield ZoteroItemPane.viewItem(item, null, pane); 1539 tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex; 1540 } 1541 else { 1542 yield ZoteroItemPane.viewItem(item, 'view', pane); 1543 tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex; 1544 } 1545 1546 if (item.isFeedItem) { 1547 // Too slow for now 1548 // if (!item.isTranslated) { 1549 // item.translate(); 1550 // } 1551 this.updateReadLabel(); 1552 this.startItemReadTimeout(item.id); 1553 } 1554 } 1555 } 1556 // Zero or multiple items selected 1557 else { 1558 if (collectionTreeRow.isFeed()) { 1559 this.updateReadLabel(); 1560 } 1561 1562 let count = selectedItems.length; 1563 1564 // Display duplicates merge interface in item pane 1565 if (collectionTreeRow.isDuplicates()) { 1566 if (!collectionTreeRow.editable) { 1567 if (count) { 1568 var msg = Zotero.getString('pane.item.duplicates.writeAccessRequired'); 1569 } 1570 else { 1571 var msg = Zotero.getString('pane.item.selected.zero'); 1572 } 1573 this.setItemPaneMessage(msg); 1574 } 1575 else if (count) { 1576 document.getElementById('zotero-item-pane-content').selectedIndex = 4; 1577 1578 // Load duplicates UI code 1579 if (typeof Zotero_Duplicates_Pane == 'undefined') { 1580 Zotero.debug("Loading duplicatesMerge.js"); 1581 Components.classes["@mozilla.org/moz/jssubscript-loader;1"] 1582 .getService(Components.interfaces.mozIJSSubScriptLoader) 1583 .loadSubScript("chrome://zotero/content/duplicatesMerge.js"); 1584 } 1585 1586 // On a Select All of more than a few items, display a row 1587 // count instead of the usual item type mismatch error 1588 var displayNumItemsOnTypeError = count > 5 && count == this.itemsView.rowCount; 1589 1590 // Initialize the merge pane with the selected items 1591 Zotero_Duplicates_Pane.setItems(selectedItems, displayNumItemsOnTypeError); 1592 } 1593 else { 1594 var msg = Zotero.getString('pane.item.duplicates.selectToMerge'); 1595 this.setItemPaneMessage(msg); 1596 } 1597 } 1598 // Display label in the middle of the item pane 1599 else { 1600 if (count) { 1601 var msg = Zotero.getString('pane.item.selected.multiple', count); 1602 } 1603 else { 1604 var rowCount = this.itemsView.rowCount; 1605 var str = 'pane.item.unselected.'; 1606 switch (rowCount){ 1607 case 0: 1608 str += 'zero'; 1609 break; 1610 case 1: 1611 str += 'singular'; 1612 break; 1613 default: 1614 str += 'plural'; 1615 break; 1616 } 1617 var msg = Zotero.getString(str, [rowCount]); 1618 } 1619 1620 this.setItemPaneMessage(msg); 1621 1622 return false; 1623 } 1624 } 1625 1626 return true; 1627 }.bind(this))() 1628 .catch(function (e) { 1629 this.displayErrorMessage(); 1630 throw e; 1631 }.bind(this)) 1632 .finally(function () { 1633 return this.itemsView.runListeners('select'); 1634 }.bind(this)); 1635 } 1636 1637 1638 /** 1639 * Display buttons at top of item pane depending on context 1640 * 1641 * @param {Zotero.Item[]} 1642 */ 1643 this.updateItemPaneButtons = function (selectedItems) { 1644 if (!selectedItems.length) { 1645 document.querySelectorAll('.zotero-item-pane-top-buttons').forEach(x => x.hidden = true); 1646 return; 1647 } 1648 1649 // My Publications buttons 1650 var isPublications = this.getCollectionTreeRow().isPublications(); 1651 // Show in My Publications view if selected items are all notes or non-linked-file attachments 1652 var showMyPublicationsButtons = isPublications 1653 && selectedItems.every((item) => { 1654 return item.isNote() 1655 || (item.isAttachment() 1656 && item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE); 1657 }); 1658 var myPublicationsButtons = document.getElementById('zotero-item-pane-top-buttons-my-publications'); 1659 myPublicationsButtons.hidden = !showMyPublicationsButtons; 1660 if (showMyPublicationsButtons) { 1661 let button = myPublicationsButtons.firstChild; 1662 let hiddenItemsSelected = selectedItems.some(item => !item.inPublications); 1663 let str, onclick; 1664 if (hiddenItemsSelected) { 1665 str = 'showInMyPublications'; 1666 onclick = () => Zotero.Items.addToPublications(selectedItems); 1667 } 1668 else { 1669 str = 'hideFromMyPublications'; 1670 onclick = () => Zotero.Items.removeFromPublications(selectedItems); 1671 } 1672 button.label = Zotero.getString('pane.item.' + str); 1673 button.onclick = onclick; 1674 } 1675 1676 // Trash button 1677 let nonDeletedItemsSelected = selectedItems.some(item => !item.deleted); 1678 document.getElementById('zotero-item-pane-top-buttons-trash').hidden 1679 = !this.getCollectionTreeRow().isTrash() || nonDeletedItemsSelected; 1680 1681 // Feed buttons 1682 document.getElementById('zotero-item-pane-top-buttons-feed').hidden 1683 = !this.getCollectionTreeRow().isFeed() 1684 }; 1685 1686 1687 /** 1688 * @return {Promise} 1689 */ 1690 this.updateNoteButtonMenu = function () { 1691 var items = ZoteroPane_Local.getSelectedItems(); 1692 var cmd = document.getElementById('cmd_zotero_newChildNote'); 1693 cmd.setAttribute("disabled", !this.canEdit() || 1694 !(items.length == 1 && (items[0].isRegularItem() || !items[0].isTopLevelItem()))); 1695 } 1696 1697 1698 this.updateAttachmentButtonMenu = function (popup) { 1699 var items = ZoteroPane_Local.getSelectedItems(); 1700 1701 var disabled = !this.canEdit() || !(items.length == 1 && items[0].isRegularItem()); 1702 1703 if (disabled) { 1704 for (let node of popup.childNodes) { 1705 node.disabled = true; 1706 } 1707 return; 1708 } 1709 1710 var collectionTreeRow = this.collectionsView.selectedTreeRow; 1711 var canEditFiles = this.canEditFiles(); 1712 1713 var prefix = "menuitem-iconic zotero-menuitem-attachments-"; 1714 1715 for (var i=0; i<popup.childNodes.length; i++) { 1716 var node = popup.childNodes[i]; 1717 var className = node.className.replace('standalone-no-display', '').trim(); 1718 1719 switch (className) { 1720 case prefix + 'link': 1721 node.disabled = collectionTreeRow.isWithinGroup(); 1722 break; 1723 1724 case prefix + 'snapshot': 1725 case prefix + 'file': 1726 node.disabled = !canEditFiles; 1727 break; 1728 1729 case prefix + 'web-link': 1730 node.disabled = false; 1731 break; 1732 1733 default: 1734 throw ("Invalid class name '" + className + "' in ZoteroPane_Local.updateAttachmentButtonMenu()"); 1735 } 1736 } 1737 } 1738 1739 1740 /** 1741 * Update the <command> elements that control the shortcut keys and the enabled state of the 1742 * "Copy Citation"/"Copy Bibliography"/"Copy as" menu options. When disabled, the shortcuts are 1743 * still caught in handleKeyPress so that we can show an alert about not having references selected. 1744 */ 1745 this.updateQuickCopyCommands = function (selectedItems) { 1746 var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL); 1747 format = Zotero.QuickCopy.unserializeSetting(format); 1748 if (format.mode == 'bibliography') { 1749 var canCopy = selectedItems.some(item => item.isRegularItem()); 1750 } 1751 else { 1752 var canCopy = true; 1753 } 1754 document.getElementById('cmd_zotero_copyCitation').setAttribute('disabled', !canCopy); 1755 document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', !canCopy); 1756 }; 1757 1758 1759 /** 1760 * @return {Promise} 1761 */ 1762 this.reindexItem = Zotero.Promise.coroutine(function* () { 1763 var items = this.getSelectedItems(); 1764 if (!items) { 1765 return; 1766 } 1767 1768 var itemIDs = []; 1769 1770 for (var i=0; i<items.length; i++) { 1771 itemIDs.push(items[i].id); 1772 } 1773 1774 yield Zotero.Fulltext.indexItems(itemIDs, true); 1775 yield document.getElementById('zotero-attachment-box').updateItemIndexedState(); 1776 }); 1777 1778 1779 /** 1780 * @return {Promise<Zotero.Item>} - The new Zotero.Item 1781 */ 1782 this.duplicateSelectedItem = Zotero.Promise.coroutine(function* () { 1783 var self = this; 1784 if (!self.canEdit()) { 1785 self.displayCannotEditLibraryMessage(); 1786 return; 1787 } 1788 1789 var item = self.getSelectedItems()[0]; 1790 var newItem; 1791 1792 yield Zotero.DB.executeTransaction(function* () { 1793 newItem = item.clone(); 1794 // If in a collection, add new item to it 1795 if (self.collectionsView.selectedTreeRow.isCollection() && newItem.isTopLevelItem()) { 1796 newItem.setCollections([self.collectionsView.selectedTreeRow.ref.id]); 1797 } 1798 yield newItem.save(); 1799 for (let relItemKey of item.relatedItems) { 1800 try { 1801 let relItem = yield Zotero.Items.getByLibraryAndKeyAsync(item.libraryID, relItemKey); 1802 if (relItem.addRelatedItem(newItem)) { 1803 yield relItem.save({ 1804 skipDateModifiedUpdate: true 1805 }); 1806 } 1807 } 1808 catch (e) { 1809 Zotero.logError(e); 1810 } 1811 } 1812 }); 1813 1814 yield self.selectItem(newItem.id); 1815 1816 return newItem; 1817 }); 1818 1819 1820 this.deleteSelectedItem = function () { 1821 Zotero.debug("ZoteroPane_Local.deleteSelectedItem() is deprecated -- use ZoteroPane_Local.deleteSelectedItems()"); 1822 this.deleteSelectedItems(); 1823 } 1824 1825 /* 1826 * Remove, trash, or delete item(s), depending on context 1827 * 1828 * @param {Boolean} [force=false] Trash or delete even if in a collection or search, 1829 * or trash without prompt in library 1830 * @param {Boolean} [fromMenu=false] If triggered from context menu, which always prompts for deletes 1831 */ 1832 this.deleteSelectedItems = function (force, fromMenu) { 1833 if (!this.itemsView || !this.itemsView.selection.count) { 1834 return; 1835 } 1836 var collectionTreeRow = this.collectionsView.selectedTreeRow; 1837 1838 if (!collectionTreeRow.isTrash() && !collectionTreeRow.isBucket() && !this.canEdit()) { 1839 this.displayCannotEditLibraryMessage(); 1840 return; 1841 } 1842 1843 var toTrash = { 1844 title: Zotero.getString('pane.items.trash.title'), 1845 text: Zotero.getString( 1846 'pane.items.trash' + (this.itemsView.selection.count > 1 ? '.multiple' : '') 1847 ) 1848 }; 1849 var toDelete = { 1850 title: Zotero.getString('pane.items.delete.title'), 1851 text: Zotero.getString( 1852 'pane.items.delete' + (this.itemsView.selection.count > 1 ? '.multiple' : '') 1853 ) 1854 }; 1855 var toRemove = { 1856 title: Zotero.getString('pane.items.remove.title'), 1857 text: Zotero.getString( 1858 'pane.items.remove' + (this.itemsView.selection.count > 1 ? '.multiple' : '') 1859 ) 1860 }; 1861 1862 if (collectionTreeRow.isPublications()) { 1863 let toRemoveFromPublications = { 1864 title: Zotero.getString('pane.items.removeFromPublications.title'), 1865 text: Zotero.getString( 1866 'pane.items.removeFromPublications' + (this.itemsView.selection.count > 1 ? '.multiple' : '') 1867 ) 1868 }; 1869 var prompt = force ? toTrash : toRemoveFromPublications; 1870 } 1871 else if (collectionTreeRow.isLibrary(true)) { 1872 // In library, don't prompt if meta key was pressed 1873 var prompt = (force && !fromMenu) ? false : toTrash; 1874 } 1875 else if (collectionTreeRow.isCollection()) { 1876 1877 // Ignore unmodified action if only child items are selected 1878 if (!force && this.itemsView.getSelectedItems().every(item => !item.isTopLevelItem())) { 1879 return; 1880 } 1881 1882 var prompt = force ? toTrash : toRemove; 1883 } 1884 else if (collectionTreeRow.isSearch() || collectionTreeRow.isUnfiled() || collectionTreeRow.isDuplicates()) { 1885 if (!force) { 1886 return; 1887 } 1888 var prompt = toTrash; 1889 } 1890 // Do nothing in trash view if any non-deleted items are selected 1891 else if (collectionTreeRow.isTrash()) { 1892 var start = {}; 1893 var end = {}; 1894 for (var i=0, len=this.itemsView.selection.getRangeCount(); i<len; i++) { 1895 this.itemsView.selection.getRangeAt(i, start, end); 1896 for (var j=start.value; j<=end.value; j++) { 1897 if (!this.itemsView.getRow(j).ref.deleted) { 1898 return; 1899 } 1900 } 1901 } 1902 var prompt = toDelete; 1903 } 1904 else if (collectionTreeRow.isBucket()) { 1905 var prompt = toDelete; 1906 } 1907 // Do nothing in share views 1908 else if (collectionTreeRow.isShare()) { 1909 return; 1910 } 1911 1912 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 1913 .getService(Components.interfaces.nsIPromptService); 1914 if (!prompt || promptService.confirm(window, prompt.title, prompt.text)) { 1915 this.itemsView.deleteSelection(force); 1916 } 1917 } 1918 1919 1920 this.mergeSelectedItems = function () { 1921 if (!this.canEdit()) { 1922 this.displayCannotEditLibraryMessage(); 1923 return; 1924 } 1925 1926 document.getElementById('zotero-item-pane-content').selectedIndex = 4; 1927 1928 if (typeof Zotero_Duplicates_Pane == 'undefined') { 1929 Zotero.debug("Loading duplicatesMerge.js"); 1930 Components.classes["@mozilla.org/moz/jssubscript-loader;1"] 1931 .getService(Components.interfaces.mozIJSSubScriptLoader) 1932 .loadSubScript("chrome://zotero/content/duplicatesMerge.js"); 1933 } 1934 1935 // Initialize the merge pane with the selected items 1936 Zotero_Duplicates_Pane.setItems(this.getSelectedItems()); 1937 } 1938 1939 1940 this.deleteSelectedCollection = function (deleteItems) { 1941 var collectionTreeRow = this.getCollectionTreeRow(); 1942 1943 // Don't allow deleting libraries 1944 if (collectionTreeRow.isLibrary(true) && !collectionTreeRow.isFeed()) { 1945 return; 1946 } 1947 1948 // Remove virtual duplicates collection 1949 if (collectionTreeRow.isDuplicates()) { 1950 this.setVirtual(collectionTreeRow.ref.libraryID, 'duplicates', false); 1951 return; 1952 } 1953 // Remove virtual unfiled collection 1954 else if (collectionTreeRow.isUnfiled()) { 1955 this.setVirtual(collectionTreeRow.ref.libraryID, 'unfiled', false); 1956 return; 1957 } 1958 1959 if (!this.canEdit() && !collectionTreeRow.isFeed()) { 1960 this.displayCannotEditLibraryMessage(); 1961 return; 1962 } 1963 1964 1965 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 1966 .getService(Components.interfaces.nsIPromptService); 1967 buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING 1968 + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL; 1969 if (this.collectionsView.selection.count == 1) { 1970 var title, message; 1971 // Work out the required title and message 1972 if (collectionTreeRow.isCollection()) { 1973 if (deleteItems) { 1974 title = Zotero.getString('pane.collections.deleteWithItems.title'); 1975 message = Zotero.getString('pane.collections.deleteWithItems'); 1976 } 1977 else { 1978 title = Zotero.getString('pane.collections.delete.title'); 1979 message = Zotero.getString('pane.collections.delete') 1980 + "\n\n" 1981 + Zotero.getString('pane.collections.delete.keepItems'); 1982 } 1983 } 1984 else if (collectionTreeRow.isFeed()) { 1985 title = Zotero.getString('pane.feed.deleteWithItems.title'); 1986 message = Zotero.getString('pane.feed.deleteWithItems'); 1987 } 1988 else if (collectionTreeRow.isSearch()) { 1989 title = Zotero.getString('pane.collections.deleteSearch.title'); 1990 message = Zotero.getString('pane.collections.deleteSearch'); 1991 } 1992 1993 // Display prompt 1994 var index = ps.confirmEx( 1995 null, 1996 title, 1997 message, 1998 buttonFlags, 1999 title, 2000 "", "", "", {} 2001 ); 2002 if (index == 0) { 2003 return this.collectionsView.deleteSelection(deleteItems); 2004 } 2005 } 2006 } 2007 2008 2009 // Currently used only for Commons to find original linked item 2010 this.getOriginalItem = function () { 2011 var item = this.getSelectedItems()[0]; 2012 var collectionTreeRow = this.getCollectionTreeRow(); 2013 // TEMP: Commons buckets only 2014 return collectionTreeRow.ref.getLocalItem(item); 2015 } 2016 2017 2018 this.showOriginalItem = function () { 2019 var item = this.getOriginalItem(); 2020 if (!item) { 2021 Zotero.debug("Original item not found"); 2022 return; 2023 } 2024 this.selectItem(item.id).done(); 2025 } 2026 2027 2028 /** 2029 * @return {Promise} 2030 */ 2031 this.restoreSelectedItems = Zotero.Promise.coroutine(function* () { 2032 var items = this.getSelectedItems(); 2033 if (!items) { 2034 return; 2035 } 2036 2037 yield Zotero.DB.executeTransaction(function* () { 2038 for (let i=0; i<items.length; i++) { 2039 items[i].deleted = false; 2040 yield items[i].save({ 2041 skipDateModifiedUpdate: true 2042 }); 2043 } 2044 }.bind(this)); 2045 }); 2046 2047 2048 /** 2049 * @return {Promise} 2050 */ 2051 this.emptyTrash = Zotero.Promise.coroutine(function* () { 2052 var libraryID = this.getSelectedLibraryID(); 2053 2054 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 2055 .getService(Components.interfaces.nsIPromptService); 2056 2057 var result = ps.confirm( 2058 null, 2059 "", 2060 Zotero.getString('pane.collections.emptyTrash') + "\n\n" 2061 + Zotero.getString('general.actionCannotBeUndone') 2062 ); 2063 if (result) { 2064 Zotero.showZoteroPaneProgressMeter(null, true); 2065 try { 2066 let deleted = yield Zotero.Items.emptyTrash( 2067 libraryID, 2068 { 2069 onProgress: (progress, progressMax) => { 2070 var percentage = Math.round((progress / progressMax) * 100); 2071 Zotero.updateZoteroPaneProgressMeter(percentage); 2072 } 2073 } 2074 ); 2075 } 2076 finally { 2077 Zotero.hideZoteroPaneOverlays(); 2078 } 2079 yield Zotero.purgeDataObjects(); 2080 } 2081 }); 2082 2083 2084 this.editSelectedCollection = Zotero.Promise.coroutine(function* () { 2085 if (!this.canEdit()) { 2086 this.displayCannotEditLibraryMessage(); 2087 return; 2088 } 2089 2090 if (this.collectionsView.selection.count > 0) { 2091 var row = this.collectionsView.selectedTreeRow; 2092 2093 if (row.isCollection()) { 2094 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 2095 .getService(Components.interfaces.nsIPromptService); 2096 2097 var newName = { value: row.getName() }; 2098 var result = promptService.prompt(window, "", 2099 Zotero.getString('pane.collections.rename'), newName, "", {}); 2100 2101 if (result && newName.value) { 2102 row.ref.name = newName.value; 2103 row.ref.saveTx(); 2104 } 2105 } 2106 else { 2107 let s = row.ref.clone(); 2108 let groups = []; 2109 // Promises don't work in the modal dialog, so get the group name here, if 2110 // applicable, and pass it in. We only need the group that this search belongs 2111 // to, if any, since the library drop-down is disabled for saved searches. 2112 if (Zotero.Libraries.get(s.libraryID).libraryType == 'group') { 2113 groups.push(Zotero.Groups.getByLibraryID(s.libraryID)); 2114 } 2115 var io = { 2116 dataIn: { 2117 search: s, 2118 name: row.getName(), 2119 groups: groups 2120 }, 2121 dataOut: null 2122 }; 2123 window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io); 2124 if (io.dataOut) { 2125 row.ref.fromJSON(io.dataOut.json); 2126 yield row.ref.saveTx(); 2127 } 2128 } 2129 } 2130 }); 2131 2132 this.toggleSelectedItemsRead = Zotero.Promise.coroutine(function* () { 2133 yield Zotero.FeedItems.toggleReadByID(this.getSelectedItems(true)); 2134 }); 2135 2136 this.markFeedRead = Zotero.Promise.coroutine(function* () { 2137 if (!this.collectionsView.selection.count) return; 2138 2139 let feed = this.collectionsView.selectedTreeRow.ref; 2140 let feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID, true, false, true); 2141 yield Zotero.FeedItems.toggleReadByID(feedItemIDs, true); 2142 }); 2143 2144 2145 this.editSelectedFeed = Zotero.Promise.coroutine(function* () { 2146 if (!this.collectionsView.selection.count) return; 2147 2148 let feed = this.collectionsView.selectedTreeRow.ref; 2149 let data = { 2150 url: feed.url, 2151 title: feed.name, 2152 ttl: feed.refreshInterval, 2153 cleanupReadAfter: feed.cleanupReadAfter, 2154 cleanupUnreadAfter: feed.cleanupUnreadAfter 2155 }; 2156 2157 window.openDialog('chrome://zotero/content/feedSettings.xul', 2158 null, 'centerscreen, modal', data); 2159 if (data.cancelled) return; 2160 2161 feed.name = data.title; 2162 feed.refreshInterval = data.ttl; 2163 feed.cleanupReadAfter = data.cleanupReadAfter; 2164 feed.cleanupUnreadAfter = data.cleanupUnreadAfter; 2165 yield feed.saveTx(); 2166 }); 2167 2168 this.refreshFeed = function() { 2169 if (!this.collectionsView.selection.count) return; 2170 2171 let feed = this.collectionsView.selectedTreeRow.ref; 2172 2173 return feed.updateFeed(); 2174 } 2175 2176 2177 this.copySelectedItemsToClipboard = function (asCitations) { 2178 var items = this.getSelectedItems(); 2179 if (!items.length) { 2180 return; 2181 } 2182 2183 var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL); 2184 format = Zotero.QuickCopy.unserializeSetting(format); 2185 2186 // In bibliography mode, remove notes and attachments 2187 if (format.mode == 'bibliography') { 2188 items = items.filter(item => item.isRegularItem()); 2189 } 2190 2191 // DEBUG: We could copy notes via keyboard shortcut if we altered 2192 // Z_F_I.copyItemsToClipboard() to use Z.QuickCopy.getContentFromItems(), 2193 // but 1) we'd need to override that function's drag limit and 2) when I 2194 // tried it the OS X clipboard seemed to be getting text vs. HTML wrong, 2195 // automatically converting text/html to plaintext rather than using 2196 // text/unicode. (That may be fixable, however.) 2197 // 2198 // This isn't currently shown, because the commands are disabled when not relevant, so this 2199 // function isn't called 2200 if (!items.length) { 2201 let ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 2202 .getService(Components.interfaces.nsIPromptService); 2203 ps.alert(null, "", Zotero.getString("fileInterface.noReferencesError")); 2204 return; 2205 } 2206 2207 // determine locale preference 2208 var locale = format.locale ? format.locale : Zotero.Prefs.get('export.quickCopy.locale'); 2209 2210 if (format.mode == 'bibliography') { 2211 Zotero_File_Interface.copyItemsToClipboard( 2212 items, format.id, locale, format.contentType == 'html', asCitations 2213 ); 2214 } 2215 else if (format.mode == 'export') { 2216 // Copy citations doesn't work in export mode 2217 if (asCitations) { 2218 return; 2219 } 2220 else { 2221 Zotero_File_Interface.exportItemsToClipboard(items, format.id); 2222 } 2223 } 2224 } 2225 2226 2227 this.clearQuicksearch = Zotero.Promise.coroutine(function* () { 2228 var search = document.getElementById('zotero-tb-search'); 2229 if (search.value !== '') { 2230 search.value = ''; 2231 yield this.search(); 2232 return true; 2233 } 2234 return false; 2235 }); 2236 2237 2238 /** 2239 * Some keys trigger an immediate search 2240 */ 2241 this.handleSearchKeypress = function (textbox, event) { 2242 if (event.keyCode == event.DOM_VK_ESCAPE) { 2243 textbox.value = ''; 2244 this.search(); 2245 } 2246 else if (event.keyCode == event.DOM_VK_RETURN) { 2247 this.search(true); 2248 } 2249 } 2250 2251 2252 this.handleSearchInput = function (textbox, event) { 2253 if (textbox.value.indexOf('"') != -1) { 2254 this.setItemsPaneMessage(Zotero.getString('advancedSearchMode')); 2255 } 2256 } 2257 2258 2259 /** 2260 * @return {Promise} 2261 */ 2262 this.search = Zotero.Promise.coroutine(function* (runAdvanced) { 2263 if (!this.itemsView) { 2264 return; 2265 } 2266 var search = document.getElementById('zotero-tb-search'); 2267 if (!runAdvanced && search.value.indexOf('"') != -1) { 2268 return; 2269 } 2270 var spinner = document.getElementById('zotero-tb-search-spinner'); 2271 spinner.style.display = 'inline'; 2272 var searchVal = search.value; 2273 yield this.itemsView.setFilter('search', searchVal); 2274 spinner.style.display = 'none'; 2275 if (runAdvanced) { 2276 this.clearItemsPaneMessage(); 2277 } 2278 }); 2279 2280 2281 this.selectItem = Zotero.Promise.coroutine(function* (itemID, inLibraryRoot, expand) { 2282 if (!itemID) { 2283 return false; 2284 } 2285 2286 var item = yield Zotero.Items.getAsync(itemID); 2287 if (!item) { 2288 return false; 2289 } 2290 2291 // Restore window if it's in the dock 2292 if (window.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) { 2293 window.restore(); 2294 } 2295 2296 if (!this.collectionsView) { 2297 throw new Error("Collections view not loaded"); 2298 } 2299 2300 var found = yield this.collectionsView.selectItem(itemID, inLibraryRoot, expand); 2301 2302 // Focus the items pane 2303 if (found) { 2304 document.getElementById('zotero-items-tree').focus(); 2305 } 2306 2307 // open Zotero pane 2308 this.show(); 2309 }); 2310 2311 2312 this.getSelectedLibraryID = function () { 2313 return this.collectionsView.getSelectedLibraryID(); 2314 } 2315 2316 2317 function getSelectedCollection(asID) { 2318 return this.collectionsView ? this.collectionsView.getSelectedCollection(asID) : false; 2319 } 2320 2321 2322 function getSelectedSavedSearch(asID) 2323 { 2324 if (this.collectionsView.selection.count > 0 && this.collectionsView.selection.currentIndex != -1) { 2325 var collection = this.collectionsView.getRow(this.collectionsView.selection.currentIndex); 2326 if (collection && collection.isSearch()) { 2327 return asID ? collection.ref.id : collection.ref; 2328 } 2329 } 2330 return false; 2331 } 2332 2333 2334 /* 2335 * Return an array of Item objects for selected items 2336 * 2337 * If asIDs is true, return an array of itemIDs instead 2338 */ 2339 function getSelectedItems(asIDs) 2340 { 2341 if (!this.itemsView) { 2342 return []; 2343 } 2344 2345 return this.itemsView.getSelectedItems(asIDs); 2346 } 2347 2348 2349 this.getSelectedGroup = function (asID) { 2350 if (this.collectionsView.selection 2351 && this.collectionsView.selection.count > 0 2352 && this.collectionsView.selection.currentIndex != -1) { 2353 2354 var collectionTreeRow = this.getCollectionTreeRow(); 2355 if (collectionTreeRow && collectionTreeRow.isGroup()) { 2356 return asID ? collectionTreeRow.ref.id : collectionTreeRow.ref; 2357 } 2358 } 2359 return false; 2360 } 2361 2362 2363 /* 2364 * Returns an array of Zotero.Item objects of visible items in current sort order 2365 * 2366 * If asIDs is true, return an array of itemIDs instead 2367 */ 2368 function getSortedItems(asIDs) { 2369 if (!this.itemsView) { 2370 return []; 2371 } 2372 2373 return this.itemsView.getSortedItems(asIDs); 2374 } 2375 2376 2377 function getSortField() { 2378 if (!this.itemsView) { 2379 return false; 2380 } 2381 2382 return this.itemsView.getSortField(); 2383 } 2384 2385 2386 function getSortDirection() { 2387 if (!this.itemsView) { 2388 return false; 2389 } 2390 2391 return this.itemsView.getSortDirection(); 2392 } 2393 2394 2395 /** 2396 * Show context menu once it's ready 2397 */ 2398 this.onCollectionsContextMenuOpen = async function (event) { 2399 await ZoteroPane.buildCollectionContextMenu(); 2400 document.getElementById('zotero-collectionmenu').openPopup( 2401 null, null, event.clientX + 1, event.clientY + 1, true, false, event 2402 ); 2403 }; 2404 2405 2406 /** 2407 * Show context menu once it's ready 2408 */ 2409 this.onItemsContextMenuOpen = async function (event) { 2410 await ZoteroPane.buildItemContextMenu() 2411 document.getElementById('zotero-itemmenu').openPopup( 2412 null, null, event.clientX + 1, event.clientY + 1, true, false, event 2413 ); 2414 }; 2415 2416 2417 this.onCollectionContextMenuSelect = function (event) { 2418 event.stopPropagation(); 2419 var o = _collectionContextMenuOptions.find(o => o.id == event.target.id) 2420 if (o.oncommand) { 2421 o.oncommand(); 2422 } 2423 }; 2424 2425 2426 // menuitem configuration 2427 // 2428 // This has to be kept in sync with zotero-collectionmenu in zoteroPane.xul. We could do this 2429 // entirely in JS, but various localized strings are only in zotero.dtd, and they're used in 2430 // standalone.xul as well, so for now they have to remain as XML entities. 2431 var _collectionContextMenuOptions = [ 2432 { 2433 id: "sync", 2434 label: Zotero.getString('sync.sync'), 2435 oncommand: () => { 2436 Zotero.Sync.Runner.sync({ 2437 libraries: [this.getSelectedLibraryID()], 2438 }); 2439 } 2440 }, 2441 { 2442 id: "sep1", 2443 }, 2444 { 2445 id: "newCollection", 2446 command: "cmd_zotero_newCollection" 2447 }, 2448 { 2449 id: "newSavedSearch", 2450 command: "cmd_zotero_newSavedSearch" 2451 }, 2452 { 2453 id: "newSubcollection", 2454 oncommand: () => { 2455 this.newCollection(this.getSelectedCollection().key); 2456 } 2457 }, 2458 { 2459 id: "refreshFeed", 2460 oncommand: () => this.refreshFeed() 2461 }, 2462 { 2463 id: "sep2", 2464 }, 2465 { 2466 id: "showDuplicates", 2467 oncommand: () => { 2468 this.setVirtual(this.getSelectedLibraryID(), 'duplicates', true); 2469 } 2470 }, 2471 { 2472 id: "showUnfiled", 2473 oncommand: () => { 2474 this.setVirtual(this.getSelectedLibraryID(), 'unfiled', true); 2475 } 2476 }, 2477 { 2478 id: "editSelectedCollection", 2479 oncommand: () => this.editSelectedCollection() 2480 }, 2481 { 2482 id: "markReadFeed", 2483 oncommand: () => this.markFeedRead() 2484 }, 2485 { 2486 id: "editSelectedFeed", 2487 oncommand: () => this.editSelectedFeed() 2488 }, 2489 { 2490 id: "deleteCollection", 2491 oncommand: () => this.deleteSelectedCollection() 2492 }, 2493 { 2494 id: "deleteCollectionAndItems", 2495 oncommand: () => this.deleteSelectedCollection(true) 2496 }, 2497 { 2498 id: "sep3", 2499 }, 2500 { 2501 id: "exportCollection", 2502 oncommand: () => Zotero_File_Interface.exportCollection() 2503 }, 2504 { 2505 id: "createBibCollection", 2506 oncommand: () => Zotero_File_Interface.bibliographyFromCollection() 2507 }, 2508 { 2509 id: "exportFile", 2510 oncommand: () => Zotero_File_Interface.exportFile() 2511 }, 2512 { 2513 id: "loadReport", 2514 oncommand: event => Zotero_Report_Interface.loadCollectionReport(event) 2515 }, 2516 { 2517 id: "emptyTrash", 2518 oncommand: () => this.emptyTrash() 2519 }, 2520 { 2521 id: "removeLibrary", 2522 label: Zotero.getString('pane.collections.menu.remove.library'), 2523 oncommand: () => { 2524 let library = Zotero.Libraries.get(this.getSelectedLibraryID()); 2525 let ps = Services.prompt; 2526 let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) 2527 + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL); 2528 let index = ps.confirmEx( 2529 null, 2530 Zotero.getString('pane.collections.removeLibrary'), 2531 Zotero.getString('pane.collections.removeLibrary.text', library.name), 2532 buttonFlags, 2533 Zotero.getString('general.remove'), 2534 null, 2535 null, null, {} 2536 ); 2537 if (index == 0) { 2538 library.eraseTx(); 2539 } 2540 } 2541 }, 2542 ]; 2543 2544 this.buildCollectionContextMenu = async function () { 2545 var libraryID = this.getSelectedLibraryID(); 2546 var options = _collectionContextMenuOptions; 2547 2548 var collectionTreeRow = this.collectionsView.selectedTreeRow; 2549 // This can happen if selection is changing during delayed second call below 2550 if (!collectionTreeRow) { 2551 return; 2552 } 2553 2554 // If the items view isn't initialized, this was a right-click on a different collection 2555 // and the new collection's items are still loading, so continue menu after loading is 2556 // done. This causes some menu items (e.g., export/createBib/loadReport) to appear gray 2557 // in the menu at first and then turn black once there are items 2558 if (!collectionTreeRow.isHeader() && !this.itemsView.initialized) { 2559 await new Promise((resolve) => { 2560 this.itemsView.onLoad.addListener(() => { 2561 resolve(); 2562 }); 2563 }); 2564 } 2565 2566 // Set attributes on the menu from the configuration object 2567 var menu = document.getElementById('zotero-collectionmenu'); 2568 var m = {}; 2569 for (let i = 0; i < options.length; i++) { 2570 let option = options[i]; 2571 let menuitem = menu.childNodes[i]; 2572 m[option.id] = menuitem; 2573 2574 menuitem.id = option.id; 2575 if (!menuitem.classList.contains('menuitem-iconic')) { 2576 menuitem.classList.add('menuitem-iconic'); 2577 } 2578 if (option.label) { 2579 menuitem.setAttribute('label', option.label); 2580 } 2581 if (option.command) { 2582 menuitem.setAttribute('command', option.command); 2583 } 2584 } 2585 2586 // By default things are hidden and visible, so we only need to record 2587 // when things are visible and when they're visible but disabled 2588 var show = [], disable = []; 2589 2590 if (collectionTreeRow.isCollection()) { 2591 show = [ 2592 'newSubcollection', 2593 'sep2', 2594 'editSelectedCollection', 2595 'deleteCollection', 2596 'deleteCollectionAndItems', 2597 'sep3', 2598 'exportCollection', 2599 'createBibCollection', 2600 'loadReport' 2601 ]; 2602 2603 if (!this.itemsView.rowCount) { 2604 disable = ['createBibCollection', 'loadReport']; 2605 2606 // If no items in subcollections either, disable export 2607 if (!(await collectionTreeRow.ref.getDescendents(false, 'item', false).length)) { 2608 disable.push('exportCollection'); 2609 } 2610 } 2611 2612 // Adjust labels 2613 m.editSelectedCollection.setAttribute('label', Zotero.getString('pane.collections.menu.rename.collection')); 2614 m.deleteCollection.setAttribute('label', Zotero.getString('pane.collections.menu.delete.collection')); 2615 m.deleteCollectionAndItems.setAttribute('label', Zotero.getString('pane.collections.menu.delete.collectionAndItems')); 2616 m.exportCollection.setAttribute('label', Zotero.getString('pane.collections.menu.export.collection')); 2617 m.createBibCollection.setAttribute('label', Zotero.getString('pane.collections.menu.createBib.collection')); 2618 m.loadReport.setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.collection')); 2619 } 2620 else if (collectionTreeRow.isFeed()) { 2621 show = [ 2622 'refreshFeed', 2623 'sep2', 2624 'markReadFeed', 2625 'editSelectedFeed', 2626 'deleteCollectionAndItems' 2627 ]; 2628 2629 if (collectionTreeRow.ref.unreadCount == 0) { 2630 disable = ['markReadFeed']; 2631 } 2632 2633 // Adjust labels 2634 m.deleteCollectionAndItems.setAttribute('label', Zotero.getString('pane.collections.menu.delete.feedAndItems')); 2635 } 2636 else if (collectionTreeRow.isSearch()) { 2637 show = [ 2638 'editSelectedCollection', 2639 'deleteCollection', 2640 'sep3', 2641 'exportCollection', 2642 'createBibCollection', 2643 'loadReport' 2644 ]; 2645 2646 m.deleteCollection.setAttribute('label', Zotero.getString('pane.collections.menu.delete.savedSearch')); 2647 2648 if (!this.itemsView.rowCount) { 2649 disable.push('exportCollection', 'createBibCollection', 'loadReport'); 2650 } 2651 2652 // Adjust labels 2653 m.editSelectedCollection.setAttribute('label', Zotero.getString('pane.collections.menu.edit.savedSearch')); 2654 m.exportCollection.setAttribute('label', Zotero.getString('pane.collections.menu.export.savedSearch')); 2655 m.createBibCollection.setAttribute('label', Zotero.getString('pane.collections.menu.createBib.savedSearch')); 2656 m.loadReport.setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.savedSearch')); 2657 } 2658 else if (collectionTreeRow.isTrash()) { 2659 show = ['emptyTrash']; 2660 } 2661 else if (collectionTreeRow.isDuplicates() || collectionTreeRow.isUnfiled()) { 2662 show = ['deleteCollection']; 2663 2664 m.deleteCollection.setAttribute('label', Zotero.getString('general.hide')); 2665 } 2666 else if (collectionTreeRow.isHeader()) { 2667 } 2668 else if (collectionTreeRow.isPublications()) { 2669 show = [ 2670 'exportFile' 2671 ]; 2672 } 2673 // Library 2674 else { 2675 let library = Zotero.Libraries.get(libraryID); 2676 show = []; 2677 if (!library.archived) { 2678 show.push( 2679 'sync', 2680 'sep1', 2681 'newCollection', 2682 'newSavedSearch' 2683 ); 2684 } 2685 // Only show "Show Duplicates" and "Show Unfiled Items" if rows are hidden 2686 let duplicates = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary( 2687 libraryID, 'duplicates' 2688 ); 2689 let unfiled = Zotero.Utilities.Internal.getVirtualCollectionStateForLibrary( 2690 libraryID, 'unfiled' 2691 ); 2692 if (!duplicates || !unfiled) { 2693 if (!library.archived) { 2694 show.push('sep2'); 2695 } 2696 if (!duplicates) { 2697 show.push('showDuplicates'); 2698 } 2699 if (!unfiled) { 2700 show.push('showUnfiled'); 2701 } 2702 } 2703 if (!library.archived) { 2704 show.push('sep3'); 2705 } 2706 show.push( 2707 'exportFile' 2708 ); 2709 if (library.archived) { 2710 show.push('removeLibrary'); 2711 } 2712 } 2713 2714 // Disable some actions if user doesn't have write access 2715 // 2716 // Some actions are disabled via their commands in onCollectionSelected() 2717 if (collectionTreeRow.isWithinGroup() && !collectionTreeRow.editable && !collectionTreeRow.isDuplicates() && !collectionTreeRow.isUnfiled()) { 2718 disable.push( 2719 'newSubcollection', 2720 'editSelectedCollection', 2721 'deleteCollection', 2722 'deleteCollectionAndItems' 2723 ); 2724 } 2725 2726 // If within non-editable group or trash it empty, disable Empty Trash 2727 if (collectionTreeRow.isTrash()) { 2728 if ((collectionTreeRow.isWithinGroup() && !collectionTreeRow.isWithinEditableGroup()) || !this.itemsView.rowCount) { 2729 disable.push('emptyTrash'); 2730 } 2731 } 2732 2733 // Hide and enable all actions by default (so if they're shown they're enabled) 2734 for (let i in m) { 2735 m[i].setAttribute('hidden', true); 2736 m[i].setAttribute('disabled', false); 2737 } 2738 2739 for (let id of show) { 2740 m[id].setAttribute('hidden', false); 2741 } 2742 2743 for (let id of disable) { 2744 m[id].setAttribute('disabled', true); 2745 } 2746 }; 2747 2748 2749 this.buildItemContextMenu = Zotero.Promise.coroutine(function* () { 2750 var options = [ 2751 'showInLibrary', 2752 'sep1', 2753 'addNote', 2754 'addAttachments', 2755 'sep2', 2756 'toggleRead', 2757 'duplicateItem', 2758 'removeItems', 2759 'restoreToLibrary', 2760 'moveToTrash', 2761 'deleteFromLibrary', 2762 'mergeItems', 2763 'sep3', 2764 'exportItems', 2765 'createBib', 2766 'loadReport', 2767 'sep4', 2768 'recognizePDF', 2769 'unrecognize', 2770 'reportMetadata', 2771 'createParent', 2772 'renameAttachments', 2773 'reindexItem' 2774 ]; 2775 2776 var m = {}; 2777 for (let i = 0; i < options.length; i++) { 2778 m[options[i]] = i; 2779 } 2780 2781 var menu = document.getElementById('zotero-itemmenu'); 2782 2783 // remove old locate menu items 2784 while(menu.firstChild && menu.firstChild.getAttribute("zotero-locate")) { 2785 menu.removeChild(menu.firstChild); 2786 } 2787 2788 var disable = [], show = [], multiple = ''; 2789 2790 if (!this.itemsView) { 2791 return; 2792 } 2793 2794 var collectionTreeRow = this.getCollectionTreeRow(); 2795 var isTrash = collectionTreeRow.isTrash(); 2796 2797 if (isTrash) { 2798 show.push(m.deleteFromLibrary); 2799 show.push(m.restoreToLibrary); 2800 } 2801 else if (!collectionTreeRow.isFeed()) { 2802 show.push(m.moveToTrash); 2803 } 2804 2805 if(!collectionTreeRow.isFeed()) { 2806 show.push(m.sep3, m.exportItems, m.createBib, m.loadReport); 2807 } 2808 2809 var items = this.getSelectedItems(); 2810 2811 if (items.length > 0) { 2812 // Multiple items selected 2813 if (items.length > 1) { 2814 var multiple = '.multiple'; 2815 2816 var canMerge = true, canIndex = true, canRecognize = true, canUnrecognize = true, canRename = true; 2817 var canMarkRead = collectionTreeRow.isFeed(); 2818 var markUnread = true; 2819 2820 for (let i = 0; i < items.length; i++) { 2821 let item = items[i]; 2822 if (canMerge && !item.isRegularItem() || item.isFeedItem || collectionTreeRow.isDuplicates()) { 2823 canMerge = false; 2824 } 2825 2826 if (canIndex && !(yield Zotero.Fulltext.canReindex(item))) { 2827 canIndex = false; 2828 } 2829 2830 if (canRecognize && !Zotero.RecognizePDF.canRecognize(item)) { 2831 canRecognize = false; 2832 } 2833 2834 if (canUnrecognize && !Zotero.RecognizePDF.canUnrecognize(item)) { 2835 canUnrecognize = false; 2836 } 2837 2838 // Show rename option only if all items are child attachments 2839 if (canRename && (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) { 2840 canRename = false; 2841 } 2842 2843 if(canMarkRead && markUnread && !item.isRead) { 2844 markUnread = false; 2845 } 2846 } 2847 2848 if (canMerge) { 2849 show.push(m.mergeItems); 2850 } 2851 2852 if (canIndex) { 2853 show.push(m.reindexItem); 2854 } 2855 2856 if (canRecognize) { 2857 show.push(m.recognizePDF); 2858 } 2859 2860 if (canUnrecognize) { 2861 show.push(m.unrecognize); 2862 } 2863 2864 if (canMarkRead) { 2865 show.push(m.toggleRead); 2866 if (markUnread) { 2867 menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsUnread')); 2868 } else { 2869 menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsRead')); 2870 } 2871 } 2872 2873 var canCreateParent = true; 2874 for (let i = 0; i < items.length; i++) { 2875 let item = items[i]; 2876 if (!item.isTopLevelItem() || !item.isAttachment() || item.isFeedItem) { 2877 canCreateParent = false; 2878 break; 2879 } 2880 } 2881 if (canCreateParent) { 2882 show.push(m.createParent); 2883 } 2884 2885 if (canRename) { 2886 show.push(m.renameAttachments); 2887 } 2888 2889 // Add in attachment separator 2890 if (canCreateParent || canRecognize || canUnrecognize || canRename || canIndex) { 2891 show.push(m.sep4); 2892 } 2893 2894 // Block certain actions on files if no access and at least one item 2895 // is an imported attachment 2896 if (!collectionTreeRow.filesEditable) { 2897 var hasImportedAttachment = false; 2898 for (var i=0; i<items.length; i++) { 2899 var item = items[i]; 2900 if (item.isImportedAttachment()) { 2901 hasImportedAttachment = true; 2902 break; 2903 } 2904 } 2905 if (hasImportedAttachment) { 2906 disable.push(m.moveToTrash, m.createParent, m.renameAttachments); 2907 } 2908 } 2909 } 2910 2911 // Single item selected 2912 else 2913 { 2914 let item = items[0]; 2915 menu.setAttribute('itemID', item.id); 2916 menu.setAttribute('itemKey', item.key); 2917 2918 if (!isTrash) { 2919 // Show in Library 2920 if (!collectionTreeRow.isLibrary(true)) { 2921 show.push(m.showInLibrary, m.sep1); 2922 } 2923 2924 if (item.isRegularItem() && !item.isFeedItem) { 2925 show.push(m.addNote, m.addAttachments, m.sep2); 2926 } 2927 2928 if (Zotero.RecognizePDF.canUnrecognize(item)) { 2929 show.push(m.sep4, m.unrecognize, m.reportMetadata); 2930 } 2931 2932 if (item.isAttachment()) { 2933 var showSep4 = false; 2934 2935 if (Zotero.RecognizePDF.canRecognize(item)) { 2936 show.push(m.recognizePDF); 2937 showSep4 = true; 2938 } 2939 2940 // Allow parent item creation for standalone attachments 2941 if (item.isTopLevelItem()) { 2942 show.push(m.createParent); 2943 showSep4 = true; 2944 } 2945 2946 // Attachment rename option 2947 if (!item.isTopLevelItem() && item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { 2948 show.push(m.renameAttachments); 2949 showSep4 = true; 2950 } 2951 2952 // If not linked URL, show reindex line 2953 if (yield Zotero.Fulltext.canReindex(item)) { 2954 show.push(m.reindexItem); 2955 showSep4 = true; 2956 } 2957 2958 if (showSep4) { 2959 show.push(m.sep4); 2960 } 2961 } 2962 else if (item.isFeedItem) { 2963 show.push(m.toggleRead); 2964 if (item.isRead) { 2965 menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsUnread')); 2966 } else { 2967 menu.childNodes[m.toggleRead].setAttribute('label', Zotero.getString('pane.item.markAsRead')); 2968 } 2969 } 2970 else if (!collectionTreeRow.isPublications()) { 2971 show.push(m.duplicateItem); 2972 } 2973 } 2974 2975 // Update attachment submenu 2976 var popup = document.getElementById('zotero-add-attachment-popup') 2977 this.updateAttachmentButtonMenu(popup); 2978 2979 // Block certain actions on files if no access 2980 if (item.isImportedAttachment() && !collectionTreeRow.filesEditable) { 2981 [m.moveToTrash, m.createParent, m.renameAttachments].forEach(function (x) { 2982 disable.push(x); 2983 }); 2984 } 2985 } 2986 } 2987 // No items selected 2988 else 2989 { 2990 // Show in Library 2991 if (!collectionTreeRow.isLibrary()) { 2992 show.push(m.showInLibrary, m.sep1); 2993 } 2994 2995 disable.push(m.showInLibrary, m.duplicateItem, m.removeItems, 2996 m.moveToTrash, m.deleteFromLibrary, m.exportItems, m.createBib, m.loadReport); 2997 } 2998 2999 if ((!collectionTreeRow.editable || collectionTreeRow.isPublications()) && !collectionTreeRow.isFeed()) { 3000 for (let i in m) { 3001 // Still allow some options for non-editable views 3002 switch (i) { 3003 case 'showInLibrary': 3004 case 'exportItems': 3005 case 'createBib': 3006 case 'loadReport': 3007 case 'toggleRead': 3008 continue; 3009 } 3010 if (isTrash) { 3011 switch (i) { 3012 case 'restoreToLibrary': 3013 case 'deleteFromLibrary': 3014 continue; 3015 } 3016 } 3017 else if (collectionTreeRow.isPublications()) { 3018 switch (i) { 3019 case 'addNote': 3020 case 'removeItems': 3021 case 'moveToTrash': 3022 continue; 3023 } 3024 } 3025 disable.push(m[i]); 3026 } 3027 } 3028 3029 // Remove from collection 3030 if (collectionTreeRow.isCollection() && items.every(item => item.isTopLevelItem())) { 3031 menu.childNodes[m.removeItems].setAttribute('label', Zotero.getString('pane.items.menu.remove' + multiple)); 3032 show.push(m.removeItems); 3033 } 3034 else if (collectionTreeRow.isPublications()) { 3035 menu.childNodes[m.removeItems].setAttribute('label', Zotero.getString('pane.items.menu.removeFromPublications' + multiple)); 3036 show.push(m.removeItems); 3037 } 3038 3039 // Set labels, plural if necessary 3040 menu.childNodes[m.moveToTrash].setAttribute('label', Zotero.getString('pane.items.menu.moveToTrash' + multiple)); 3041 menu.childNodes[m.deleteFromLibrary].setAttribute('label', Zotero.getString('pane.items.menu.delete' + multiple)); 3042 menu.childNodes[m.exportItems].setAttribute('label', Zotero.getString('pane.items.menu.export' + multiple)); 3043 menu.childNodes[m.createBib].setAttribute('label', Zotero.getString('pane.items.menu.createBib' + multiple)); 3044 menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.items.menu.generateReport' + multiple)); 3045 menu.childNodes[m.createParent].setAttribute('label', Zotero.getString('pane.items.menu.createParent' + multiple)); 3046 menu.childNodes[m.recognizePDF].setAttribute('label', Zotero.getString('pane.items.menu.recognizePDF' + multiple)); 3047 menu.childNodes[m.renameAttachments].setAttribute('label', Zotero.getString('pane.items.menu.renameAttachments' + multiple)); 3048 menu.childNodes[m.reindexItem].setAttribute('label', Zotero.getString('pane.items.menu.reindexItem' + multiple)); 3049 3050 // Hide and enable all actions by default (so if they're shown they're enabled) 3051 for (let i in m) { 3052 let pos = m[i]; 3053 menu.childNodes[pos].setAttribute('hidden', true); 3054 menu.childNodes[pos].setAttribute('disabled', false); 3055 } 3056 3057 for (var i in disable) 3058 { 3059 menu.childNodes[disable[i]].setAttribute('disabled', true); 3060 } 3061 3062 for (var i in show) 3063 { 3064 menu.childNodes[show[i]].setAttribute('hidden', false); 3065 } 3066 3067 // add locate menu options 3068 yield Zotero_LocateMenu.buildContextMenu(menu, true); 3069 }); 3070 3071 3072 this.onTreeMouseDown = function (event) { 3073 var t = event.originalTarget; 3074 var tree = t.parentNode; 3075 3076 // Ignore click on column headers 3077 if (!tree.treeBoxObject) { 3078 return; 3079 } 3080 3081 var row = {}, col = {}, obj = {}; 3082 tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); 3083 if (row.value == -1) { 3084 return; 3085 } 3086 3087 if (tree.id == 'zotero-collections-tree') { 3088 let collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row.value); 3089 3090 // Prevent the tree's select event from being called for a click 3091 // on a library sync error icon 3092 if (collectionTreeRow.isLibrary(true)) { 3093 if (col.value.id == 'zotero-collections-sync-status-column') { 3094 var errors = Zotero.Sync.Runner.getErrors(collectionTreeRow.ref.libraryID); 3095 if (errors) { 3096 event.stopPropagation(); 3097 return; 3098 } 3099 } 3100 } 3101 } 3102 else if (tree.id == 'zotero-items-tree') { 3103 let collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow(); 3104 3105 // Automatically select all equivalent items when clicking on an item 3106 // in duplicates view 3107 if (collectionTreeRow.isDuplicates()) { 3108 // Trigger only on primary-button single clicks without modifiers 3109 // (so that items can still be selected and deselected manually) 3110 if (!event || event.detail != 1 || event.button != 0 || event.metaKey 3111 || event.shiftKey || event.altKey || event.ctrlKey) { 3112 return; 3113 } 3114 3115 var t = event.originalTarget; 3116 3117 if (t.localName != 'treechildren') { 3118 return; 3119 } 3120 3121 var tree = t.parentNode; 3122 3123 var row = {}, col = {}, obj = {}; 3124 tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); 3125 3126 // obj.value == 'cell'/'text'/'image'/'twisty' 3127 if (!obj.value) { 3128 return; 3129 } 3130 3131 // Duplicated in itemTreeView.js::notify() 3132 var itemID = ZoteroPane_Local.itemsView.getRow(row.value).ref.id; 3133 var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID); 3134 ZoteroPane_Local.itemsView.selectItems(setItemIDs); 3135 3136 // Prevent the tree's select event from being called here, 3137 // since it's triggered by the multi-select 3138 event.stopPropagation(); 3139 } 3140 } 3141 } 3142 3143 3144 // Adapted from: http://www.xulplanet.com/references/elemref/ref_tree.html#cmnote-9 3145 this.onTreeClick = function (event) { 3146 var t = event.originalTarget; 3147 3148 if (t.localName != 'treechildren') { 3149 return; 3150 } 3151 3152 var tree = t.parentNode; 3153 3154 var row = {}, col = {}, obj = {}; 3155 tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj); 3156 3157 // We care only about primary-button double and triple clicks 3158 if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) { 3159 if (row.value == -1) { 3160 return; 3161 } 3162 3163 if (tree.id == 'zotero-collections-tree') { 3164 let collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row.value); 3165 3166 // Show the error panel when clicking a library-specific 3167 // sync error icon 3168 if (collectionTreeRow.isLibrary(true)) { 3169 if (col.value.id == 'zotero-collections-sync-status-column') { 3170 var errors = Zotero.Sync.Runner.getErrors(collectionTreeRow.ref.libraryID); 3171 if (!errors) { 3172 return; 3173 } 3174 3175 var panel = Zotero.Sync.Runner.updateErrorPanel(window.document, errors); 3176 3177 var anchor = document.getElementById('zotero-collections-tree-shim'); 3178 3179 var x = {}, y = {}, width = {}, height = {}; 3180 tree.treeBoxObject.getCoordsForCellItem(row.value, col.value, 'image', x, y, width, height); 3181 3182 x = x.value + Math.round(width.value / 2); 3183 y = y.value + height.value + 3; 3184 3185 panel.openPopup(anchor, "after_start", x, y, false, false); 3186 } 3187 return; 3188 } 3189 } 3190 3191 // The Mozilla tree binding fires select() in mousedown(), 3192 // but if when it gets to click() the selection differs from 3193 // what it expects (say, because multiple items had been 3194 // selected during mousedown(), as is the case in duplicates mode), 3195 // it fires select() again. We prevent that here. 3196 else if (tree.id == 'zotero-items-tree') { 3197 let collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow(); 3198 if (collectionTreeRow.isDuplicates()) { 3199 if (event.button != 0 || event.metaKey || event.shiftKey 3200 || event.altKey || event.ctrlKey) { 3201 return; 3202 } 3203 3204 if (obj.value == 'twisty') { 3205 return; 3206 } 3207 3208 event.stopPropagation(); 3209 event.preventDefault(); 3210 } 3211 } 3212 3213 return; 3214 } 3215 3216 var collectionTreeRow = ZoteroPane_Local.getCollectionTreeRow(); 3217 3218 // Ignore double-clicks in duplicates view on everything except attachments 3219 if (collectionTreeRow.isDuplicates()) { 3220 var items = ZoteroPane_Local.getSelectedItems(); 3221 if (items.length != 1 || !items[0].isAttachment()) { 3222 event.stopPropagation(); 3223 event.preventDefault(); 3224 return; 3225 } 3226 } 3227 3228 // obj.value == 'cell'/'text'/'image' 3229 if (!obj.value) { 3230 return; 3231 } 3232 3233 if (tree.id == 'zotero-collections-tree') { 3234 // Ignore triple clicks for collections 3235 if (event.detail != 2) { 3236 return; 3237 } 3238 3239 if (collectionTreeRow.isLibrary()) { 3240 var uri = Zotero.URI.getCurrentUserLibraryURI(); 3241 if (uri) { 3242 ZoteroPane_Local.loadURI(uri); 3243 event.stopPropagation(); 3244 } 3245 return; 3246 } 3247 3248 if (collectionTreeRow.isSearch()) { 3249 ZoteroPane_Local.editSelectedCollection(); 3250 return; 3251 } 3252 3253 if (collectionTreeRow.isGroup()) { 3254 var uri = Zotero.URI.getGroupURI(collectionTreeRow.ref, true); 3255 ZoteroPane_Local.loadURI(uri); 3256 event.stopPropagation(); 3257 return; 3258 } 3259 3260 // Ignore double-clicks on Unfiled Items source row 3261 if (collectionTreeRow.isUnfiled()) { 3262 return; 3263 } 3264 3265 if (collectionTreeRow.isHeader()) { 3266 if (collectionTreeRow.ref.id == 'group-libraries-header') { 3267 var uri = Zotero.URI.getGroupsURL(); 3268 ZoteroPane_Local.loadURI(uri); 3269 event.stopPropagation(); 3270 } 3271 return; 3272 } 3273 3274 if (collectionTreeRow.isBucket()) { 3275 ZoteroPane_Local.loadURI(collectionTreeRow.ref.uri); 3276 event.stopPropagation(); 3277 } 3278 } 3279 else if (tree.id == 'zotero-items-tree') { 3280 var viewOnDoubleClick = Zotero.Prefs.get('viewOnDoubleClick'); 3281 if (viewOnDoubleClick) { 3282 // Expand/collapse on triple-click, though the double-click 3283 // will still trigger 3284 if (event.detail == 3) { 3285 tree.view.toggleOpenState(tree.view.selection.currentIndex); 3286 return; 3287 } 3288 3289 // Don't expand/collapse on double-click 3290 event.stopPropagation(); 3291 } 3292 3293 if (tree.view && tree.view.selection.currentIndex > -1) { 3294 var item = ZoteroPane_Local.getSelectedItems()[0]; 3295 if (item) { 3296 if (!viewOnDoubleClick && item.isRegularItem()) { 3297 return; 3298 } 3299 ZoteroPane_Local.viewItems([item], event); 3300 } 3301 } 3302 } 3303 } 3304 3305 3306 this.openPreferences = function (paneID, action) { 3307 Zotero.warn("ZoteroPane.openPreferences() is deprecated" 3308 + " -- use Zotero.Utilities.Internal.openPreferences() instead"); 3309 Zotero.Utilities.Internal.openPreferences(paneID, { action }); 3310 } 3311 3312 3313 /* 3314 * Loads a URL following the standard modifier key behavior 3315 * (e.g. meta-click == new background tab, meta-shift-click == new front tab, 3316 * shift-click == new window, no modifier == frontmost tab 3317 */ 3318 this.loadURI = function (uris, event) { 3319 if(typeof uris === "string") { 3320 uris = [uris]; 3321 } 3322 3323 for (let i = 0; i < uris.length; i++) { 3324 let uri = uris[i]; 3325 // Ignore javascript: and data: URIs 3326 if (uri.match(/^(javascript|data):/)) { 3327 return; 3328 } 3329 3330 if (Zotero.isStandalone) { 3331 if(uri.match(/^https?/)) { 3332 this.launchURL(uri); 3333 continue; 3334 } 3335 3336 // Handle no-content zotero: URLs (e.g., zotero://select) without opening viewer 3337 if (uri.startsWith('zotero:')) { 3338 let nsIURI = Services.io.newURI(uri, null, null); 3339 let handler = Components.classes["@mozilla.org/network/protocol;1?name=zotero"] 3340 .getService(); 3341 let extension = handler.wrappedJSObject.getExtension(nsIURI); 3342 if (extension.noContent) { 3343 extension.doAction(nsIURI); 3344 return; 3345 } 3346 } 3347 3348 Zotero.openInViewer(uri); 3349 return; 3350 } 3351 3352 // Open in new tab 3353 var openInNewTab = event && (event.metaKey || (!Zotero.isMac && event.ctrlKey)); 3354 if (event && event.shiftKey && !openInNewTab) { 3355 window.open(uri, "zotero-loaded-page", 3356 "menubar=yes,location=yes,toolbar=yes,personalbar=yes,resizable=yes,scrollbars=yes,status=yes"); 3357 } 3358 else if (openInNewTab || !window.loadURI || uris.length > 1) { 3359 // if no gBrowser, find it 3360 if(!gBrowser) { 3361 let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); 3362 var gBrowser = browserWindow.gBrowser; 3363 } 3364 3365 // load in a new tab 3366 var tab = gBrowser.addTab(uri); 3367 var browser = gBrowser.getBrowserForTab(tab); 3368 3369 if (event && event.shiftKey || !openInNewTab) { 3370 // if shift key is down, or we are opening in a new tab because there is no loadURI, 3371 // select new tab 3372 gBrowser.selectedTab = tab; 3373 } 3374 } 3375 else { 3376 window.loadURI(uri); 3377 } 3378 } 3379 } 3380 3381 3382 function setItemsPaneMessage(content, lock) { 3383 var elem = document.getElementById('zotero-items-pane-message-box'); 3384 3385 if (elem.getAttribute('locked') == 'true') { 3386 return; 3387 } 3388 3389 elem.textContent = ''; 3390 if (typeof content == 'string') { 3391 let contentParts = content.split("\n\n"); 3392 for (let part of contentParts) { 3393 var desc = document.createElement('description'); 3394 desc.appendChild(document.createTextNode(part)); 3395 elem.appendChild(desc); 3396 } 3397 } 3398 else { 3399 elem.appendChild(content); 3400 } 3401 3402 // Make message permanent 3403 if (lock) { 3404 elem.setAttribute('locked', true); 3405 } 3406 3407 document.getElementById('zotero-items-pane-content').selectedIndex = 1; 3408 } 3409 3410 3411 function clearItemsPaneMessage() { 3412 // If message box is locked, don't clear 3413 var box = document.getElementById('zotero-items-pane-message-box'); 3414 if (box.getAttribute('locked') == 'true') { 3415 return; 3416 } 3417 3418 document.getElementById('zotero-items-pane-content').selectedIndex = 0; 3419 } 3420 3421 3422 this.setItemPaneMessage = function (content) { 3423 document.getElementById('zotero-item-pane-content').selectedIndex = 0; 3424 3425 var elem = document.getElementById('zotero-item-pane-message-box'); 3426 elem.textContent = ''; 3427 if (typeof content == 'string') { 3428 let contentParts = content.split("\n\n"); 3429 for (let part of contentParts) { 3430 let desc = document.createElement('description'); 3431 desc.appendChild(document.createTextNode(part)); 3432 elem.appendChild(desc); 3433 } 3434 } 3435 else { 3436 elem.appendChild(content); 3437 } 3438 } 3439 3440 3441 // Updates browser context menu options 3442 function contextPopupShowing() 3443 { 3444 if (!Zotero.Prefs.get('browserContentContextMenu')) { 3445 return; 3446 } 3447 3448 var menuitem = document.getElementById("zotero-context-add-to-current-note"); 3449 if (menuitem){ 3450 var items = ZoteroPane_Local.getSelectedItems(); 3451 if (ZoteroPane_Local.itemsView.selection && ZoteroPane_Local.itemsView.selection.count==1 3452 && items[0] && items[0].isNote() 3453 && window.gContextMenu.isTextSelected) 3454 { 3455 menuitem.hidden = false; 3456 } 3457 else 3458 { 3459 menuitem.hidden = true; 3460 } 3461 } 3462 3463 var menuitem = document.getElementById("zotero-context-add-to-new-note"); 3464 if (menuitem){ 3465 if (window.gContextMenu.isTextSelected) 3466 { 3467 menuitem.hidden = false; 3468 } 3469 else 3470 { 3471 menuitem.hidden = true; 3472 } 3473 } 3474 3475 var menuitem = document.getElementById("zotero-context-save-link-as-item"); 3476 if (menuitem) { 3477 if (window.gContextMenu.onLink) { 3478 menuitem.hidden = false; 3479 } 3480 else { 3481 menuitem.hidden = true; 3482 } 3483 } 3484 3485 var menuitem = document.getElementById("zotero-context-save-image-as-item"); 3486 if (menuitem) { 3487 // Not using window.gContextMenu.hasBGImage -- if the user wants it, 3488 // they can use the Firefox option to view and then import from there 3489 if (window.gContextMenu.onImage) { 3490 menuitem.hidden = false; 3491 } 3492 else { 3493 menuitem.hidden = true; 3494 } 3495 } 3496 3497 // If Zotero is locked or library is read-only, disable menu items 3498 var menu = document.getElementById('zotero-content-area-context-menu'); 3499 var disabled = Zotero.locked; 3500 if (!disabled && self.collectionsView.selection && self.collectionsView.selection.count) { 3501 var collectionTreeRow = self.collectionsView.selectedTreeRow; 3502 disabled = !collectionTreeRow.editable; 3503 } 3504 for (let menuitem of menu.firstChild.childNodes) { 3505 menuitem.disabled = disabled; 3506 } 3507 } 3508 3509 /** 3510 * @return {Promise<Integer|null|false>} - The id of the new note in non-popup mode, null in 3511 * popup mode (where a note isn't created immediately), or false if library isn't editable 3512 */ 3513 this.newNote = Zotero.Promise.coroutine(function* (popup, parentKey, text, citeURI) { 3514 if (!this.canEdit()) { 3515 this.displayCannotEditLibraryMessage(); 3516 return false; 3517 } 3518 3519 if (popup) { 3520 // TODO: _text_ 3521 var c = this.getSelectedCollection(); 3522 if (c) { 3523 this.openNoteWindow(null, c.id, parentKey); 3524 } 3525 else { 3526 this.openNoteWindow(null, null, parentKey); 3527 } 3528 return null; 3529 } 3530 3531 if (!text) { 3532 text = ''; 3533 } 3534 text = text.trim(); 3535 3536 if (text) { 3537 text = '<blockquote' 3538 + (citeURI ? ' cite="' + citeURI + '"' : '') 3539 + '>' + Zotero.Utilities.text2html(text) + "</blockquote>"; 3540 } 3541 3542 var item = new Zotero.Item('note'); 3543 item.libraryID = this.getSelectedLibraryID(); 3544 item.setNote(text); 3545 if (parentKey) { 3546 item.parentKey = parentKey; 3547 } 3548 else if (this.collectionsView.selectedTreeRow.isCollection()) { 3549 item.addToCollection(this.collectionsView.selectedTreeRow.ref.id); 3550 } 3551 var itemID = yield item.saveTx(); 3552 3553 yield this.selectItem(itemID); 3554 3555 document.getElementById('zotero-note-editor').focus(); 3556 3557 return itemID; 3558 }); 3559 3560 3561 /** 3562 * Creates a child note for the selected item or the selected item's parent 3563 * 3564 * @return {Promise} 3565 */ 3566 this.newChildNote = function (popup) { 3567 var selected = this.getSelectedItems()[0]; 3568 var parentKey = selected.parentItemKey; 3569 parentKey = parentKey ? parentKey : selected.key; 3570 this.newNote(popup, parentKey); 3571 } 3572 3573 3574 this.addSelectedTextToCurrentNote = Zotero.Promise.coroutine(function* () { 3575 if (!this.canEdit()) { 3576 this.displayCannotEditLibraryMessage(); 3577 return; 3578 } 3579 3580 var text = event.currentTarget.ownerDocument.popupNode.ownerDocument.defaultView.getSelection().toString(); 3581 var uri = event.currentTarget.ownerDocument.popupNode.ownerDocument.location.href; 3582 3583 if (!text) { 3584 return false; 3585 } 3586 3587 text = text.trim(); 3588 3589 if (!text.length) { 3590 return false; 3591 } 3592 3593 text = '<blockquote' + (uri ? ' cite="' + uri + '"' : '') + '>' 3594 + Zotero.Utilities.text2html(text) + "</blockquote>"; 3595 3596 var items = this.getSelectedItems(); 3597 3598 if (this.itemsView.selection.count == 1 && items[0] && items[0].isNote()) { 3599 var note = items[0].getNote() 3600 3601 items[0].setNote(note + text); 3602 yield items[0].saveTx(); 3603 3604 var noteElem = document.getElementById('zotero-note-editor') 3605 noteElem.focus(); 3606 return true; 3607 } 3608 3609 return false; 3610 }); 3611 3612 3613 this.createItemAndNoteFromSelectedText = Zotero.Promise.coroutine(function* (event) { 3614 var str = event.currentTarget.ownerDocument.popupNode.ownerDocument.defaultView.getSelection().toString(); 3615 var uri = event.currentTarget.ownerDocument.popupNode.ownerDocument.location.href; 3616 var item = yield ZoteroPane.addItemFromPage(); 3617 if (item) { 3618 return ZoteroPane.newNote(false, item.key, str, uri) 3619 } 3620 }); 3621 3622 3623 3624 this.openNoteWindow = function (itemID, col, parentKey) { 3625 if (!this.canEdit()) { 3626 this.displayCannotEditLibraryMessage(); 3627 return; 3628 } 3629 3630 var name = null; 3631 3632 if (itemID) { 3633 let w = this.findNoteWindow(itemID); 3634 if (w) { 3635 w.focus(); 3636 return; 3637 } 3638 3639 // Create a name for this window so we can focus it later 3640 // 3641 // Collection is only used on new notes, so we don't need to 3642 // include it in the name 3643 name = 'zotero-note-' + itemID; 3644 } 3645 3646 var io = { itemID: itemID, collectionID: col, parentItemKey: parentKey }; 3647 window.openDialog('chrome://zotero/content/note.xul', name, 'chrome,resizable,centerscreen,dialog=false', io); 3648 } 3649 3650 3651 this.findNoteWindow = function (itemID) { 3652 var name = 'zotero-note-' + itemID; 3653 var wm = Services.wm; 3654 var e = wm.getEnumerator('zotero:note'); 3655 while (e.hasMoreElements()) { 3656 var w = e.getNext(); 3657 if (w.name == name) { 3658 return w; 3659 } 3660 } 3661 }; 3662 3663 3664 this.onNoteWindowClosed = async function (itemID, noteText) { 3665 var item = Zotero.Items.get(itemID); 3666 item.setNote(noteText); 3667 await item.saveTx(); 3668 3669 // If note is still selected, show the editor again when the note window closes 3670 var selectedItems = this.getSelectedItems(true); 3671 if (selectedItems.length == 1 && itemID == selectedItems[0]) { 3672 ZoteroItemPane.onNoteSelected(item, this.collectionsView.editable); 3673 } 3674 }; 3675 3676 3677 this.addAttachmentFromURI = Zotero.Promise.method(function (link, itemID) { 3678 if (!this.canEdit()) { 3679 this.displayCannotEditLibraryMessage(); 3680 return; 3681 } 3682 3683 var io = {}; 3684 window.openDialog('chrome://zotero/content/attachLink.xul', 3685 'zotero-attach-uri-dialog', 'centerscreen, modal', io); 3686 if (!io.out) return; 3687 return Zotero.Attachments.linkFromURL({ 3688 url: io.out.link, 3689 parentItemID: itemID, 3690 title: io.out.title 3691 }); 3692 }); 3693 3694 3695 this.addAttachmentFromDialog = Zotero.Promise.coroutine(function* (link, parentItemID) { 3696 if (!this.canEdit()) { 3697 this.displayCannotEditLibraryMessage(); 3698 return; 3699 } 3700 3701 var collectionTreeRow = this.getCollectionTreeRow(); 3702 if (link) { 3703 if (collectionTreeRow.isWithinGroup()) { 3704 Zotero.alert(null, "", "Linked files cannot be added to group libraries."); 3705 return; 3706 } 3707 else if (collectionTreeRow.isPublications()) { 3708 Zotero.alert( 3709 null, 3710 Zotero.getString('general.error'), 3711 Zotero.getString('publications.error.linkedFilesCannotBeAdded') 3712 ); 3713 return; 3714 } 3715 } 3716 3717 // TODO: disable in menu 3718 if (!this.canEditFiles()) { 3719 this.displayCannotEditLibraryFilesMessage(); 3720 return; 3721 } 3722 3723 var libraryID = collectionTreeRow.ref.libraryID; 3724 3725 var nsIFilePicker = Components.interfaces.nsIFilePicker; 3726 var fp = Components.classes["@mozilla.org/filepicker;1"] 3727 .createInstance(nsIFilePicker); 3728 fp.init(window, Zotero.getString('pane.item.attachments.select'), nsIFilePicker.modeOpenMultiple); 3729 fp.appendFilters(nsIFilePicker.filterAll); 3730 3731 if (fp.show() != nsIFilePicker.returnOK) { 3732 return; 3733 } 3734 3735 var enumerator = fp.files; 3736 var files = []; 3737 while (enumerator.hasMoreElements()) { 3738 let file = enumerator.getNext(); 3739 file.QueryInterface(Components.interfaces.nsIFile); 3740 files.push(file.path); 3741 } 3742 3743 var addedItems = []; 3744 var collection; 3745 var fileBaseName; 3746 if (parentItemID) { 3747 // If only one item is being added, automatic renaming is enabled, and the parent item 3748 // doesn't have any other non-HTML file attachments, rename the file. 3749 // This should be kept in sync with itemTreeView::drop(). 3750 if (files.length == 1 && Zotero.Prefs.get('autoRenameFiles')) { 3751 let parentItem = Zotero.Items.get(parentItemID); 3752 if (!parentItem.numNonHTMLFileAttachments()) { 3753 fileBaseName = yield Zotero.Attachments.getRenamedFileBaseNameIfAllowedType( 3754 parentItem, files[0] 3755 ); 3756 } 3757 } 3758 } 3759 // If not adding to an item, add to the current collection 3760 else { 3761 collection = this.getSelectedCollection(true); 3762 } 3763 3764 for (let file of files) { 3765 let item; 3766 3767 if (link) { 3768 // Rename linked file, with unique suffix if necessary 3769 try { 3770 if (fileBaseName) { 3771 let ext = Zotero.File.getExtension(file); 3772 let newName = yield Zotero.File.rename( 3773 file, 3774 fileBaseName + (ext ? '.' + ext : ''), 3775 { 3776 unique: true 3777 } 3778 ); 3779 // Update path in case the name was changed to be unique 3780 file = OS.Path.join(OS.Path.dirname(file), newName); 3781 } 3782 } 3783 catch (e) { 3784 Zotero.logError(e); 3785 } 3786 3787 item = yield Zotero.Attachments.linkFromFile({ 3788 file, 3789 parentItemID, 3790 collections: collection ? [collection] : undefined 3791 }); 3792 } 3793 else { 3794 if (file.endsWith(".lnk")) { 3795 let win = Services.wm.getMostRecentWindow("navigator:browser"); 3796 win.ZoteroPane.displayCannotAddShortcutMessage(file); 3797 continue; 3798 } 3799 3800 item = yield Zotero.Attachments.importFromFile({ 3801 file, 3802 libraryID, 3803 fileBaseName, 3804 parentItemID, 3805 collections: collection ? [collection] : undefined 3806 }); 3807 } 3808 3809 addedItems.push(item); 3810 } 3811 3812 // Automatically retrieve metadata for top-level PDFs 3813 if (!parentItemID) { 3814 Zotero.RecognizePDF.autoRecognizeItems(addedItems); 3815 } 3816 }); 3817 3818 3819 /** 3820 * @return {Promise<Zotero.Item>|false} 3821 */ 3822 this.addItemFromPage = Zotero.Promise.method(function (itemType, saveSnapshot, row) { 3823 if (row == undefined && this.collectionsView && this.collectionsView.selection) { 3824 row = this.collectionsView.selection.currentIndex; 3825 } 3826 3827 if (row !== undefined) { 3828 if (!this.canEdit(row)) { 3829 this.displayCannotEditLibraryMessage(); 3830 return false; 3831 } 3832 3833 var collectionTreeRow = this.collectionsView.getRow(row); 3834 if (collectionTreeRow.isPublications()) { 3835 this.displayCannotAddToMyPublicationsMessage(); 3836 return false; 3837 } 3838 } 3839 3840 return this.addItemFromDocument(window.content.document, itemType, saveSnapshot, row); 3841 }); 3842 3843 /** 3844 * Shows progress dialog for a webpage/snapshot save request 3845 */ 3846 function _showPageSaveStatus(title) { 3847 var progressWin = new Zotero.ProgressWindow(); 3848 progressWin.changeHeadline(Zotero.getString('ingester.scraping')); 3849 var icon = 'chrome://zotero/skin/treeitem-webpage.png'; 3850 progressWin.addLines(title, icon) 3851 progressWin.show(); 3852 progressWin.startCloseTimer(); 3853 } 3854 3855 /** 3856 * @param {Document} doc 3857 * @param {String|Integer} [itemType='webpage'] Item type id or name 3858 * @param {Boolean} [saveSnapshot] Force saving or non-saving of a snapshot, 3859 * regardless of automaticSnapshots pref 3860 * @return {Promise<Zotero.Item>|false} 3861 */ 3862 this.addItemFromDocument = Zotero.Promise.coroutine(function* (doc, itemType, saveSnapshot, row) { 3863 _showPageSaveStatus(doc.title); 3864 3865 // Save snapshot if explicitly enabled or automatically pref is set and not explicitly disabled 3866 saveSnapshot = saveSnapshot || (saveSnapshot !== false && Zotero.Prefs.get('automaticSnapshots')); 3867 3868 // TODO: this, needless to say, is a temporary hack 3869 if (itemType == 'temporaryPDFHack') { 3870 itemType = null; 3871 var isPDF = false; 3872 if (doc.title.indexOf('application/pdf') != -1 || Zotero.Attachments.isPDFJS(doc) 3873 || doc.contentType == 'application/pdf') { 3874 isPDF = true; 3875 } 3876 else { 3877 var ios = Components.classes["@mozilla.org/network/io-service;1"]. 3878 getService(Components.interfaces.nsIIOService); 3879 try { 3880 var uri = ios.newURI(doc.location, null, null); 3881 if (uri.fileName && uri.fileName.match(/pdf$/)) { 3882 isPDF = true; 3883 } 3884 } 3885 catch (e) { 3886 Zotero.debug(e); 3887 Components.utils.reportError(e); 3888 } 3889 } 3890 3891 if (isPDF && saveSnapshot) { 3892 // 3893 // Duplicate newItem() checks here 3894 // 3895 if (Zotero.DB.inTransaction()) { 3896 yield Zotero.DB.waitForTransaction(); 3897 } 3898 3899 // Currently selected row 3900 if (row === undefined && this.collectionsView && this.collectionsView.selection) { 3901 row = this.collectionsView.selection.currentIndex; 3902 } 3903 3904 if (row && !this.canEdit(row)) { 3905 this.displayCannotEditLibraryMessage(); 3906 return false; 3907 } 3908 3909 if (row !== undefined) { 3910 var collectionTreeRow = this.collectionsView.getRow(row); 3911 var libraryID = collectionTreeRow.ref.libraryID; 3912 } 3913 else { 3914 var libraryID = Zotero.Libraries.userLibraryID; 3915 var collectionTreeRow = null; 3916 } 3917 // 3918 // 3919 // 3920 3921 if (row && !this.canEditFiles(row)) { 3922 this.displayCannotEditLibraryFilesMessage(); 3923 return false; 3924 } 3925 3926 if (collectionTreeRow && collectionTreeRow.isCollection()) { 3927 var collectionID = collectionTreeRow.ref.id; 3928 } 3929 else { 3930 var collectionID = false; 3931 } 3932 3933 let item = yield Zotero.Attachments.importFromDocument({ 3934 libraryID: libraryID, 3935 document: doc, 3936 collections: collectionID ? [collectionID] : [] 3937 }); 3938 3939 yield this.selectItem(item.id); 3940 return false; 3941 } 3942 } 3943 3944 // Save web page item by default 3945 if (!itemType) { 3946 itemType = 'webpage'; 3947 } 3948 var data = { 3949 title: doc.title, 3950 url: doc.location.href, 3951 accessDate: "CURRENT_TIMESTAMP" 3952 } 3953 itemType = Zotero.ItemTypes.getID(itemType); 3954 var item = yield this.newItem(itemType, data, row); 3955 var filesEditable = Zotero.Libraries.get(item.libraryID).filesEditable; 3956 3957 if (saveSnapshot) { 3958 var link = false; 3959 3960 if (link) { 3961 yield Zotero.Attachments.linkFromDocument({ 3962 document: doc, 3963 parentItemID: item.id 3964 }); 3965 } 3966 else if (filesEditable) { 3967 yield Zotero.Attachments.importFromDocument({ 3968 document: doc, 3969 parentItemID: item.id 3970 }); 3971 } 3972 } 3973 3974 return item; 3975 }); 3976 3977 3978 /** 3979 * @return {Zotero.Item|false} - The saved item, or false if item can't be saved 3980 */ 3981 this.addItemFromURL = Zotero.Promise.coroutine(function* (url, itemType, saveSnapshot, row) { 3982 if (window.content && url == window.content.document.location.href) { 3983 return this.addItemFromPage(itemType, saveSnapshot, row); 3984 } 3985 3986 url = Zotero.Utilities.resolveIntermediateURL(url); 3987 3988 let [mimeType, hasNativeHandler] = yield Zotero.MIME.getMIMETypeFromURL(url); 3989 3990 // If native type, save using a hidden browser 3991 if (hasNativeHandler) { 3992 var deferred = Zotero.Promise.defer(); 3993 3994 var processor = function (doc) { 3995 return ZoteroPane_Local.addItemFromDocument(doc, itemType, saveSnapshot, row) 3996 .then(function (item) { 3997 deferred.resolve(item) 3998 }); 3999 }; 4000 var done = function () {} 4001 var exception = function (e) { 4002 Zotero.debug(e, 1); 4003 deferred.reject(e); 4004 } 4005 Zotero.HTTP.loadDocuments([url], processor, done, exception); 4006 4007 return deferred.promise; 4008 } 4009 // Otherwise create placeholder item, attach attachment, and update from that 4010 else { 4011 // TODO: this, needless to say, is a temporary hack 4012 if (itemType == 'temporaryPDFHack') { 4013 itemType = null; 4014 4015 if (mimeType == 'application/pdf') { 4016 // 4017 // Duplicate newItem() checks here 4018 // 4019 if (Zotero.DB.inTransaction()) { 4020 yield Zotero.DB.waitForTransaction(); 4021 } 4022 4023 // Currently selected row 4024 if (row === undefined) { 4025 row = ZoteroPane_Local.collectionsView.selection.currentIndex; 4026 } 4027 4028 if (!ZoteroPane_Local.canEdit(row)) { 4029 ZoteroPane_Local.displayCannotEditLibraryMessage(); 4030 return false; 4031 } 4032 4033 if (row !== undefined) { 4034 var collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row); 4035 var libraryID = collectionTreeRow.ref.libraryID; 4036 } 4037 else { 4038 var libraryID = Zotero.Libraries.userLibraryID; 4039 var collectionTreeRow = null; 4040 } 4041 // 4042 // 4043 // 4044 4045 if (!ZoteroPane_Local.canEditFiles(row)) { 4046 ZoteroPane_Local.displayCannotEditLibraryFilesMessage(); 4047 return false; 4048 } 4049 4050 if (collectionTreeRow && collectionTreeRow.isCollection()) { 4051 var collectionID = collectionTreeRow.ref.id; 4052 } 4053 else { 4054 var collectionID = false; 4055 } 4056 4057 let attachmentItem = yield Zotero.Attachments.importFromURL({ 4058 libraryID, 4059 url, 4060 collections: collectionID ? [collectionID] : undefined, 4061 contentType: mimeType 4062 }); 4063 this.selectItem(attachmentItem.id) 4064 return attachmentItem; 4065 } 4066 } 4067 4068 if (!itemType) { 4069 itemType = 'webpage'; 4070 } 4071 4072 var item = yield ZoteroPane_Local.newItem(itemType, {}, row) 4073 var filesEditable = Zotero.Libraries.get(item.libraryID).filesEditable; 4074 4075 // Save snapshot if explicitly enabled or automatically pref is set and not explicitly disabled 4076 if (saveSnapshot || (saveSnapshot !== false && Zotero.Prefs.get('automaticSnapshots'))) { 4077 var link = false; 4078 4079 if (link) { 4080 //Zotero.Attachments.linkFromURL(doc, item.id); 4081 } 4082 else if (filesEditable) { 4083 var attachmentItem = yield Zotero.Attachments.importFromURL({ 4084 url, 4085 parentItemID: item.id, 4086 contentType: mimeType 4087 }); 4088 if (attachmentItem) { 4089 item.setField('title', attachmentItem.getField('title')); 4090 item.setField('url', attachmentItem.getField('url')); 4091 item.setField('accessDate', attachmentItem.getField('accessDate')); 4092 yield item.saveTx(); 4093 } 4094 } 4095 } 4096 4097 return item; 4098 } 4099 }); 4100 4101 4102 /* 4103 * Create an attachment from the current page 4104 * 4105 * |itemID| -- itemID of parent item 4106 * |link| -- create web link instead of snapshot 4107 */ 4108 this.addAttachmentFromPage = Zotero.Promise.coroutine(function* (link, itemID) { 4109 if (Zotero.DB.inTransaction()) { 4110 yield Zotero.DB.waitForTransaction(); 4111 } 4112 4113 if (typeof itemID != 'number') { 4114 throw new Error("itemID must be an integer"); 4115 } 4116 4117 var progressWin = new Zotero.ProgressWindow(); 4118 progressWin.changeHeadline(Zotero.getString('save.' + (link ? 'link' : 'attachment'))); 4119 var type = link ? 'web-link' : 'snapshot'; 4120 var icon = 'chrome://zotero/skin/treeitem-attachment-' + type + '.png'; 4121 progressWin.addLines(window.content.document.title, icon) 4122 progressWin.show(); 4123 progressWin.startCloseTimer(); 4124 4125 if (link) { 4126 return Zotero.Attachments.linkFromDocument({ 4127 document: window.content.document, 4128 parentItemID: itemID 4129 }); 4130 } 4131 return Zotero.Attachments.importFromDocument({ 4132 document: window.content.document, 4133 parentItemID: itemID 4134 }); 4135 }); 4136 4137 4138 this.viewItems = Zotero.Promise.coroutine(function* (items, event) { 4139 if (items.length > 1) { 4140 if (!event || (!event.metaKey && !event.shiftKey)) { 4141 event = { metaKey: true, shiftKey: true }; 4142 } 4143 } 4144 4145 for (let i = 0; i < items.length; i++) { 4146 let item = items[i]; 4147 if (item.isRegularItem()) { 4148 // Prefer local file attachments 4149 var uri = Components.classes["@mozilla.org/network/standard-url;1"] 4150 .createInstance(Components.interfaces.nsIURI); 4151 let attachment = yield item.getBestAttachment(); 4152 if (attachment) { 4153 yield this.viewAttachment(attachment.id, event); 4154 continue; 4155 } 4156 4157 // Fall back to URI field, then DOI 4158 var uri = item.getField('url'); 4159 if (!uri) { 4160 var doi = item.getField('DOI'); 4161 if (doi) { 4162 // Pull out DOI, in case there's a prefix 4163 doi = Zotero.Utilities.cleanDOI(doi); 4164 if (doi) { 4165 uri = "http://dx.doi.org/" + encodeURIComponent(doi); 4166 } 4167 } 4168 } 4169 4170 // Fall back to first attachment link 4171 if (!uri) { 4172 let attachmentID = item.getAttachments()[0]; 4173 if (attachmentID) { 4174 let attachment = yield Zotero.Items.getAsync(attachmentID); 4175 if (attachment) uri = attachment.getField('url'); 4176 } 4177 } 4178 4179 if (uri) { 4180 this.loadURI(uri, event); 4181 } 4182 } 4183 else if (item.isNote()) { 4184 if (!this.collectionsView.editable) { 4185 continue; 4186 } 4187 document.getElementById('zotero-view-note-button').doCommand(); 4188 } 4189 else if (item.isAttachment()) { 4190 yield this.viewAttachment(item.id, event); 4191 } 4192 } 4193 }); 4194 4195 4196 this.viewAttachment = Zotero.serial(Zotero.Promise.coroutine(function* (itemIDs, event, noLocateOnMissing, forceExternalViewer) { 4197 // If view isn't editable, don't show Locate button, since the updated 4198 // path couldn't be sent back up 4199 if (!this.collectionsView.editable) { 4200 noLocateOnMissing = true; 4201 } 4202 4203 if(typeof itemIDs != "object") itemIDs = [itemIDs]; 4204 4205 // If multiple items, set up event so we open in new tab 4206 if(itemIDs.length > 1) { 4207 if(!event || (!event.metaKey && !event.shiftKey)) { 4208 event = {"metaKey":true, "shiftKey":true}; 4209 } 4210 } 4211 4212 for (let i = 0; i < itemIDs.length; i++) { 4213 let itemID = itemIDs[i]; 4214 var item = yield Zotero.Items.getAsync(itemID); 4215 if (!item.isAttachment()) { 4216 throw new Error("Item " + itemID + " is not an attachment"); 4217 } 4218 4219 if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { 4220 this.loadURI(item.getField('url'), event); 4221 continue; 4222 } 4223 4224 var path = yield item.getFilePathAsync(); 4225 if (path) { 4226 let file = Zotero.File.pathToFile(path); 4227 4228 Zotero.debug("Opening " + path); 4229 4230 if(forceExternalViewer !== undefined) { 4231 var externalViewer = forceExternalViewer; 4232 } else { 4233 var mimeType = yield Zotero.MIME.getMIMETypeFromFile(file); 4234 4235 //var mimeType = attachment.attachmentMIMEType; 4236 // TODO: update DB with new info if changed? 4237 4238 var ext = Zotero.File.getExtension(file); 4239 var externalViewer = Zotero.isStandalone || (!Zotero.MIME.hasNativeHandler(mimeType, ext) && 4240 (!Zotero.MIME.hasInternalHandler(mimeType, ext) || Zotero.Prefs.get('launchNonNativeFiles'))); 4241 } 4242 4243 if (!externalViewer) { 4244 let url = Services.io.newFileURI(file).spec; 4245 this.loadURI(url, event); 4246 } 4247 else { 4248 Zotero.Notifier.trigger('open', 'file', itemID); 4249 4250 // Custom PDF handler 4251 if (item.attachmentContentType === 'application/pdf') { 4252 let pdfHandler = Zotero.Prefs.get("fileHandler.pdf"); 4253 if (pdfHandler) { 4254 if (yield OS.File.exists(pdfHandler)) { 4255 Zotero.launchFileWithApplication(file.path, pdfHandler); 4256 continue; 4257 } 4258 else { 4259 Zotero.logError(`${pdfHandler} not found -- launching file normally`); 4260 } 4261 } 4262 } 4263 4264 Zotero.launchFile(file); 4265 } 4266 } 4267 else { 4268 if (!item.isImportedAttachment() 4269 || !Zotero.Sync.Storage.Local.getEnabledForLibrary(item.libraryID)) { 4270 this.showAttachmentNotFoundDialog(itemID, noLocateOnMissing); 4271 return; 4272 } 4273 4274 try { 4275 yield Zotero.Sync.Runner.downloadFile(item); 4276 } 4277 catch (e) { 4278 // TODO: show error somewhere else 4279 Zotero.debug(e, 1); 4280 ZoteroPane_Local.syncAlert(e); 4281 return; 4282 } 4283 4284 if (!(yield item.getFilePathAsync())) { 4285 ZoteroPane_Local.showAttachmentNotFoundDialog(item.id, noLocateOnMissing, true); 4286 return; 4287 } 4288 4289 // check if unchanged? 4290 // maybe not necessary, since we'll get an error if there's an error 4291 4292 Zotero.Notifier.trigger('redraw', 'item', []); 4293 // Retry after download 4294 i--; 4295 } 4296 } 4297 })); 4298 4299 4300 /** 4301 * @deprecated 4302 */ 4303 this.launchFile = function (file) { 4304 Zotero.debug("ZoteroPane.launchFile() is deprecated -- use Zotero.launchFile()", 2); 4305 Zotero.launchFile(file); 4306 } 4307 4308 4309 /** 4310 * @deprecated 4311 */ 4312 this.launchURL = function (url) { 4313 Zotero.debug("ZoteroPane.launchURL() is deprecated -- use Zotero.launchURL()", 2); 4314 return Zotero.launchURL(url); 4315 } 4316 4317 4318 function viewSelectedAttachment(event, noLocateOnMissing) 4319 { 4320 if (this.itemsView && this.itemsView.selection.count == 1) { 4321 this.viewAttachment(this.getSelectedItems(true)[0], event, noLocateOnMissing); 4322 } 4323 } 4324 4325 4326 this.showAttachmentInFilesystem = Zotero.Promise.coroutine(function* (itemID, noLocateOnMissing) { 4327 var attachment = yield Zotero.Items.getAsync(itemID) 4328 if (attachment.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { 4329 var path = yield attachment.getFilePathAsync(); 4330 if (path) { 4331 let file = Zotero.File.pathToFile(path); 4332 try { 4333 Zotero.debug("Revealing " + file.path); 4334 file.reveal(); 4335 } 4336 catch (e) { 4337 // On platforms that don't support nsILocalFile.reveal() (e.g. Linux), 4338 // launch the parent directory 4339 var parent = file.parent.QueryInterface(Components.interfaces.nsILocalFile); 4340 Zotero.launchFile(parent); 4341 } 4342 Zotero.Notifier.trigger('open', 'file', attachment.id); 4343 } 4344 else { 4345 this.showAttachmentNotFoundDialog(attachment.id, noLocateOnMissing) 4346 } 4347 } 4348 }); 4349 4350 4351 this.showPublicationsWizard = function (items) { 4352 var io = { 4353 hasFiles: false, 4354 hasNotes: false, 4355 hasRights: null // 'all', 'some', or 'none' 4356 }; 4357 var allItemsHaveRights = true; 4358 var noItemsHaveRights = true; 4359 // Determine whether any/all items have files, notes, or Rights values 4360 for (let i = 0; i < items.length; i++) { 4361 let item = items[i]; 4362 4363 // Files 4364 if (!io.hasFiles && item.numAttachments()) { 4365 let attachmentIDs = item.getAttachments(); 4366 io.hasFiles = Zotero.Items.get(attachmentIDs).some( 4367 attachment => attachment.isFileAttachment() 4368 ); 4369 } 4370 // Notes 4371 if (!io.hasNotes && item.numNotes()) { 4372 io.hasNotes = true; 4373 } 4374 // Rights 4375 if (item.getField('rights')) { 4376 noItemsHaveRights = false; 4377 } 4378 else { 4379 allItemsHaveRights = false; 4380 } 4381 } 4382 io.hasRights = allItemsHaveRights ? 'all' : (noItemsHaveRights ? 'none' : 'some'); 4383 window.openDialog('chrome://zotero/content/publicationsDialog.xul','','chrome,modal', io); 4384 return io.license ? io : false; 4385 }; 4386 4387 4388 /** 4389 * Test if the user can edit the currently selected view 4390 * 4391 * @param {Integer} [row] 4392 * 4393 * @return {Boolean} TRUE if user can edit, FALSE if not 4394 */ 4395 this.canEdit = function (row) { 4396 // Currently selected row 4397 if (row === undefined) { 4398 row = this.collectionsView.selection.currentIndex; 4399 } 4400 4401 var collectionTreeRow = this.collectionsView.getRow(row); 4402 return collectionTreeRow.editable; 4403 } 4404 4405 4406 /** 4407 * Test if the user can edit the parent library of the selected view 4408 * 4409 * @param {Integer} [row] 4410 * @return {Boolean} TRUE if user can edit, FALSE if not 4411 */ 4412 this.canEditLibrary = function (row) { 4413 // Currently selected row 4414 if (row === undefined) { 4415 row = this.collectionsView.selection.currentIndex; 4416 } 4417 4418 var collectionTreeRow = this.collectionsView.getRow(row); 4419 return Zotero.Libraries.get(collectionTreeRow.ref.libraryID).editable; 4420 } 4421 4422 4423 /** 4424 * Test if the user can edit the currently selected library/collection 4425 * 4426 * @param {Integer} [row] 4427 * 4428 * @return {Boolean} TRUE if user can edit, FALSE if not 4429 */ 4430 this.canEditFiles = function (row) { 4431 // Currently selected row 4432 if (row === undefined) { 4433 row = this.collectionsView.selection.currentIndex; 4434 } 4435 4436 var collectionTreeRow = this.collectionsView.getRow(row); 4437 return collectionTreeRow.filesEditable; 4438 } 4439 4440 4441 this.displayCannotEditLibraryMessage = function () { 4442 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 4443 .getService(Components.interfaces.nsIPromptService); 4444 ps.alert(null, "", Zotero.getString('save.error.cannotMakeChangesToCollection')); 4445 } 4446 4447 4448 this.displayCannotEditLibraryFilesMessage = function () { 4449 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 4450 .getService(Components.interfaces.nsIPromptService); 4451 ps.alert(null, "", Zotero.getString('save.error.cannotAddFilesToCollection')); 4452 } 4453 4454 4455 this.displayCannotAddToMyPublicationsMessage = function () { 4456 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 4457 .getService(Components.interfaces.nsIPromptService); 4458 ps.alert(null, "", Zotero.getString('save.error.cannotAddToMyPublications')); 4459 } 4460 4461 4462 // TODO: Figure out a functioning way to get the original path and just copy the real file 4463 this.displayCannotAddShortcutMessage = function (path) { 4464 Zotero.alert( 4465 null, 4466 Zotero.getString("general.error"), 4467 Zotero.getString("file.error.cannotAddShortcut") + (path ? "\n\n" + path : "") 4468 ); 4469 } 4470 4471 4472 this.showAttachmentNotFoundDialog = function (itemID, noLocate, notOnServer) { 4473 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. 4474 createInstance(Components.interfaces.nsIPromptService); 4475 4476 var title = Zotero.getString('pane.item.attachments.fileNotFound.title'); 4477 var text = Zotero.getString('pane.item.attachments.fileNotFound.text1') + "\n\n" 4478 + Zotero.getString( 4479 'pane.item.attachments.fileNotFound.text2' + (notOnServer ? '.notOnServer' : ''), 4480 [ZOTERO_CONFIG.CLIENT_NAME, ZOTERO_CONFIG.DOMAIN_NAME] 4481 ); 4482 var supportURL = Zotero.getString('pane.item.attachments.fileNotFound.supportURL'); 4483 4484 // Don't show Locate button 4485 if (noLocate) { 4486 let buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK) 4487 + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING); 4488 let index = ps.confirmEx(null, 4489 title, 4490 text, 4491 buttonFlags, 4492 null, 4493 Zotero.getString('general.moreInformation'), 4494 null, null, {} 4495 ); 4496 if (index == 1) { 4497 this.loadURI(supportURL, { metaKey: true, shiftKey: true }); 4498 } 4499 return; 4500 } 4501 4502 var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) 4503 + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL) 4504 + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING); 4505 var index = ps.confirmEx(null, 4506 title, 4507 text, 4508 buttonFlags, 4509 Zotero.getString('general.locate'), 4510 null, 4511 Zotero.getString('general.moreInformation'), null, {} 4512 ); 4513 4514 if (index == 0) { 4515 this.relinkAttachment(itemID); 4516 } 4517 else if (index == 2) { 4518 this.loadURI(supportURL, { metaKey: true, shiftKey: true }); 4519 } 4520 } 4521 4522 4523 this.syncAlert = function (e) { 4524 e = Zotero.Sync.Runner.parseError(e); 4525 4526 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 4527 .getService(Components.interfaces.nsIPromptService); 4528 var buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK 4529 + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING; 4530 4531 // Warning 4532 if (e.errorType == 'warning') { 4533 var title = Zotero.getString('general.warning'); 4534 4535 // If secondary button not specified, just use an alert 4536 if (e.buttonText) { 4537 var buttonText = e.buttonText; 4538 } 4539 else { 4540 ps.alert(null, title, e.message); 4541 return; 4542 } 4543 4544 var index = ps.confirmEx( 4545 null, 4546 title, 4547 e.message, 4548 buttonFlags, 4549 "", 4550 buttonText, 4551 "", null, {} 4552 ); 4553 4554 if (index == 1) { 4555 setTimeout(function () { buttonCallback(); }, 1); 4556 } 4557 } 4558 // Error 4559 else if (e.errorType == 'error') { 4560 var title = Zotero.getString('general.error'); 4561 4562 // If secondary button is explicitly null, just use an alert 4563 if (buttonText === null) { 4564 ps.alert(null, title, e.message); 4565 return; 4566 } 4567 4568 if (typeof buttonText == 'undefined') { 4569 var buttonText = Zotero.getString('errorReport.reportError'); 4570 var buttonCallback = function () { 4571 ZoteroPane.reportErrors(); 4572 }; 4573 } 4574 else { 4575 var buttonText = e.buttonText; 4576 var buttonCallback = e.buttonCallback; 4577 } 4578 4579 var index = ps.confirmEx( 4580 null, 4581 title, 4582 e.message, 4583 buttonFlags, 4584 "", 4585 buttonText, 4586 "", null, {} 4587 ); 4588 4589 if (index == 1) { 4590 setTimeout(function () { buttonCallback(); }, 1); 4591 } 4592 } 4593 // Upgrade 4594 else if (e.errorType == 'upgrade') { 4595 ps.alert(null, "", e.message); 4596 } 4597 }; 4598 4599 4600 this.recognizeSelected = function() { 4601 Zotero.RecognizePDF.recognizeItems(ZoteroPane.getSelectedItems()); 4602 Zotero_RecognizePDF_Dialog.open(); 4603 }; 4604 4605 4606 this.unrecognizeSelected = async function () { 4607 var items = ZoteroPane.getSelectedItems(); 4608 for (let item of items) { 4609 await Zotero.RecognizePDF.unrecognize(item); 4610 } 4611 }; 4612 4613 4614 this.reportMetadataForSelected = async function () { 4615 let items = ZoteroPane.getSelectedItems(); 4616 if(!items.length) return; 4617 4618 let input = {value: ''}; 4619 Services.prompt.prompt( 4620 null, 4621 Zotero.getString('recognizePDF.reportMetadata'), 4622 Zotero.getString('general.describeProblem'), 4623 input, null, {} 4624 ); 4625 4626 try { 4627 await Zotero.RecognizePDF.report(items[0], input.value); 4628 Zotero.alert( 4629 window, 4630 Zotero.getString('general.submitted'), 4631 Zotero.getString('general.thanksForHelpingImprove', Zotero.clientName) 4632 ); 4633 } 4634 catch (e) { 4635 Zotero.logError(e); 4636 Zotero.alert( 4637 window, 4638 Zotero.getString('general.error'), 4639 Zotero.getString('general.invalidResponseServer') 4640 ); 4641 } 4642 }; 4643 4644 4645 this.createParentItemsFromSelected = Zotero.Promise.coroutine(function* () { 4646 if (!this.canEdit()) { 4647 this.displayCannotEditLibraryMessage(); 4648 return; 4649 } 4650 4651 var items = this.getSelectedItems(); 4652 for (var i=0; i<items.length; i++) { 4653 var item = items[i]; 4654 if (!item.isTopLevelItem() || item.isRegularItem()) { 4655 throw('Item ' + itemID + ' is not a top-level attachment or note in ZoteroPane_Local.createParentItemsFromSelected()'); 4656 } 4657 4658 yield Zotero.DB.executeTransaction(function* () { 4659 // TODO: remove once there are no top-level web attachments 4660 if (item.isWebAttachment()) { 4661 var parent = new Zotero.Item('webpage'); 4662 } 4663 else { 4664 var parent = new Zotero.Item('document'); 4665 } 4666 parent.libraryID = item.libraryID; 4667 parent.setField('title', item.getField('title')); 4668 if (item.isWebAttachment()) { 4669 parent.setField('accessDate', item.getField('accessDate')); 4670 parent.setField('url', item.getField('url')); 4671 } 4672 var itemID = yield parent.save(); 4673 item.parentID = itemID; 4674 yield item.save(); 4675 }); 4676 } 4677 }); 4678 4679 4680 this.renameSelectedAttachmentsFromParents = Zotero.Promise.coroutine(function* () { 4681 // TEMP: fix 4682 4683 if (!this.canEdit()) { 4684 this.displayCannotEditLibraryMessage(); 4685 return; 4686 } 4687 4688 var items = this.getSelectedItems(); 4689 4690 for (var i=0; i<items.length; i++) { 4691 var item = items[i]; 4692 4693 if (!item.isAttachment() || item.isTopLevelItem() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { 4694 throw('Item ' + itemID + ' is not a child file attachment in ZoteroPane_Local.renameAttachmentFromParent()'); 4695 } 4696 4697 var file = item.getFile(); 4698 if (!file) { 4699 continue; 4700 } 4701 4702 let parentItemID = item.parentItemID; 4703 let parentItem = yield Zotero.Items.getAsync(parentItemID); 4704 var newName = Zotero.Attachments.getFileBaseNameFromItem(parentItem); 4705 4706 var ext = file.leafName.match(/\.[^\.]+$/); 4707 if (ext) { 4708 newName = newName + ext; 4709 } 4710 4711 var renamed = yield item.renameAttachmentFile(newName, false, true); 4712 if (renamed !== true) { 4713 Zotero.debug("Could not rename file (" + renamed + ")"); 4714 continue; 4715 } 4716 4717 item.setField('title', newName); 4718 yield item.saveTx(); 4719 } 4720 4721 return true; 4722 }); 4723 4724 4725 this.relinkAttachment = Zotero.Promise.coroutine(function* (itemID) { 4726 if (!this.canEdit()) { 4727 this.displayCannotEditLibraryMessage(); 4728 return; 4729 } 4730 4731 var item = Zotero.Items.get(itemID); 4732 if (!item) { 4733 throw new Error('Item ' + itemID + ' not found in ZoteroPane_Local.relinkAttachment()'); 4734 } 4735 4736 while (true) { 4737 var nsIFilePicker = Components.interfaces.nsIFilePicker; 4738 var fp = Components.classes["@mozilla.org/filepicker;1"] 4739 .createInstance(nsIFilePicker); 4740 fp.init(window, Zotero.getString('pane.item.attachments.select'), nsIFilePicker.modeOpen); 4741 4742 var file = item.getFilePath(); 4743 if (!file) { 4744 Zotero.debug("Invalid path", 2); 4745 break; 4746 } 4747 4748 var dir = yield Zotero.File.getClosestDirectory(file); 4749 if (dir) { 4750 fp.displayDirectory = Zotero.File.pathToFile(dir); 4751 } 4752 4753 fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll); 4754 4755 if (fp.show() == nsIFilePicker.returnOK) { 4756 let file = fp.file; 4757 file.QueryInterface(Components.interfaces.nsILocalFile); 4758 4759 // Disallow hidden files 4760 // TODO: Display a message 4761 if (file.leafName.startsWith('.')) { 4762 continue; 4763 } 4764 4765 // Disallow Windows shortcuts 4766 if (file.leafName.endsWith(".lnk")) { 4767 this.displayCannotAddShortcutMessage(file.path); 4768 continue; 4769 } 4770 4771 yield item.relinkAttachmentFile(file.path); 4772 break; 4773 } 4774 4775 break; 4776 } 4777 }); 4778 4779 4780 this.updateReadLabel = function () { 4781 var items = this.getSelectedItems(); 4782 var isUnread = false; 4783 for (let item of items) { 4784 if (!item.isRead) { 4785 isUnread = true; 4786 break; 4787 } 4788 } 4789 ZoteroItemPane.setReadLabel(!isUnread); 4790 }; 4791 4792 4793 var itemReadPromise; 4794 this.startItemReadTimeout = function (feedItemID) { 4795 if (itemReadPromise) { 4796 itemReadPromise.cancel(); 4797 } 4798 4799 const FEED_READ_TIMEOUT = 1000; 4800 4801 itemReadPromise = Zotero.Promise.delay(FEED_READ_TIMEOUT) 4802 .then(async function () { 4803 itemReadPromise = null; 4804 4805 // Check to make sure we're still on the same item 4806 var items = this.getSelectedItems(); 4807 if (items.length != 1 || items[0].id != feedItemID) { 4808 Zotero.debug(items.length); 4809 Zotero.debug(items[0].id); 4810 Zotero.debug(feedItemID); 4811 4812 return; 4813 } 4814 var feedItem = items[0]; 4815 if (!(feedItem instanceof Zotero.FeedItem)) { 4816 throw new Zotero.Promise.CancellationError('Not a FeedItem'); 4817 } 4818 if (feedItem.isRead) { 4819 return; 4820 } 4821 4822 await feedItem.toggleRead(true); 4823 ZoteroItemPane.setReadLabel(true); 4824 }.bind(this)) 4825 .catch(function (e) { 4826 if (e instanceof Zotero.Promise.CancellationError) { 4827 Zotero.debug(e.message); 4828 return; 4829 } 4830 Zotero.logError(e); 4831 }); 4832 } 4833 4834 4835 function reportErrors() { 4836 var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 4837 .getService(Components.interfaces.nsIWindowWatcher); 4838 var data = { 4839 msg: Zotero.getString('errorReport.followingReportWillBeSubmitted'), 4840 errorData: Zotero.getErrors(true), 4841 askForSteps: true 4842 }; 4843 var io = { wrappedJSObject: { Zotero: Zotero, data: data } }; 4844 var win = ww.openWindow(null, "chrome://zotero/content/errorReport.xul", 4845 "zotero-error-report", "chrome,centerscreen,modal", io); 4846 } 4847 4848 /* 4849 * Display an error message saying that an error has occurred and Firefox 4850 * needs to be restarted. 4851 * 4852 * If |popup| is TRUE, display in popup progress window; otherwise, display 4853 * as items pane message 4854 */ 4855 function displayErrorMessage(popup) { 4856 var reportErrorsStr = Zotero.getString('errorReport.reportErrors'); 4857 var reportInstructions = 4858 Zotero.getString('errorReport.reportInstructions', reportErrorsStr) 4859 4860 // Display as popup progress window 4861 if (popup) { 4862 var pw = new Zotero.ProgressWindow(); 4863 pw.changeHeadline(Zotero.getString('general.errorHasOccurred')); 4864 var msg = Zotero.getString('general.pleaseRestart', Zotero.appName) + ' ' 4865 + reportInstructions; 4866 pw.addDescription(msg); 4867 pw.show(); 4868 pw.startCloseTimer(8000); 4869 } 4870 // Display as items pane message 4871 else { 4872 var msg = Zotero.getString('general.errorHasOccurred') + ' ' 4873 + Zotero.getString('general.pleaseRestart', Zotero.appName) + '\n\n' 4874 + reportInstructions; 4875 self.setItemsPaneMessage(msg, true); 4876 } 4877 Zotero.debug(msg, 1); 4878 } 4879 4880 this.displayStartupError = function(asPaneMessage) { 4881 if (Zotero) { 4882 var errMsg = Zotero.startupError; 4883 var errFunc = Zotero.startupErrorHandler; 4884 } 4885 4886 var stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"] 4887 .getService(Components.interfaces.nsIStringBundleService); 4888 var src = 'chrome://zotero/locale/zotero.properties'; 4889 var stringBundle = stringBundleService.createBundle(src); 4890 4891 var title = stringBundle.GetStringFromName('general.error'); 4892 if (!errMsg) { 4893 var errMsg = stringBundle.GetStringFromName('startupError'); 4894 } 4895 4896 if (errFunc) { 4897 errFunc(); 4898 } 4899 else { 4900 // TODO: Add a better error page/window here with reporting 4901 // instructions 4902 // window.loadURI('chrome://zotero/content/error.xul'); 4903 //if(asPaneMessage) { 4904 // ZoteroPane_Local.setItemsPaneMessage(errMsg, true); 4905 //} else { 4906 var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 4907 .getService(Components.interfaces.nsIPromptService); 4908 ps.alert(null, title, errMsg); 4909 //} 4910 } 4911 } 4912 4913 /** 4914 * Sets the layout to either a three-vertical-pane layout and a layout where itemsPane is above itemPane 4915 */ 4916 this.updateLayout = function() { 4917 var layoutSwitcher = document.getElementById("zotero-layout-switcher"); 4918 var itemsSplitter = document.getElementById("zotero-items-splitter"); 4919 4920 if(Zotero.Prefs.get("layout") === "stacked") { // itemsPane above itemPane 4921 layoutSwitcher.setAttribute("orient", "vertical"); 4922 itemsSplitter.setAttribute("orient", "vertical"); 4923 } else { // three-vertical-pane 4924 layoutSwitcher.setAttribute("orient", "horizontal"); 4925 itemsSplitter.setAttribute("orient", "horizontal"); 4926 } 4927 4928 this.updateToolbarPosition(); 4929 } 4930 /** 4931 * Shows the Zotero pane, making it visible if it is not and switching to the appropriate tab 4932 * if necessary. 4933 */ 4934 this.show = function() { 4935 if(window.ZoteroOverlay) { 4936 if (!this.isShowing()) { 4937 ZoteroOverlay.toggleDisplay(); 4938 } 4939 } 4940 } 4941 4942 /** 4943 * Unserializes zotero-persist elements from preferences 4944 */ 4945 this.unserializePersist = function () { 4946 _unserialized = true; 4947 var serializedValues = Zotero.Prefs.get("pane.persist"); 4948 if(!serializedValues) return; 4949 serializedValues = JSON.parse(serializedValues); 4950 for(var id in serializedValues) { 4951 var el = document.getElementById(id); 4952 if(!el) return; 4953 var elValues = serializedValues[id]; 4954 for(var attr in elValues) { 4955 // TEMP: For now, ignore persisted collapsed state for item pane splitter 4956 if (el.id == 'zotero-items-splitter' && attr == 'state') continue; 4957 // And don't restore to min-width if splitter was collapsed 4958 if (el.id == 'zotero-item-pane' && attr == 'width' && elValues[attr] == 250 4959 && 'zotero-items-splitter' in serializedValues 4960 && serializedValues['zotero-items-splitter'].state == 'collapsed') { 4961 continue; 4962 } 4963 el.setAttribute(attr, elValues[attr]); 4964 } 4965 } 4966 4967 if(this.itemsView) { 4968 // may not yet be initialized 4969 try { 4970 this.itemsView.sort(); 4971 } catch(e) {}; 4972 } 4973 }; 4974 4975 /** 4976 * Serializes zotero-persist elements to preferences 4977 */ 4978 this.serializePersist = function() { 4979 if(!_unserialized) return; 4980 var serializedValues = {}; 4981 for (let el of document.getElementsByAttribute("zotero-persist", "*")) { 4982 if(!el.getAttribute) continue; 4983 var id = el.getAttribute("id"); 4984 if(!id) continue; 4985 var elValues = {}; 4986 for (let attr of el.getAttribute("zotero-persist").split(/[\s,]+/)) { 4987 if (el.hasAttribute(attr)) { 4988 elValues[attr] = el.getAttribute(attr); 4989 } 4990 } 4991 serializedValues[id] = elValues; 4992 } 4993 Zotero.Prefs.set("pane.persist", JSON.stringify(serializedValues)); 4994 } 4995 4996 4997 this.updateWindow = function () { 4998 var zoteroPane = document.getElementById('zotero-pane'); 4999 // Must match value in overlay.css 5000 var breakpoint = 1000; 5001 var className = `width-${breakpoint}`; 5002 if (window.innerWidth >= breakpoint) { 5003 zoteroPane.classList.add(className); 5004 } 5005 else { 5006 zoteroPane.classList.remove(className); 5007 } 5008 }; 5009 5010 5011 /** 5012 * Moves around the toolbar when the user moves around the pane 5013 */ 5014 this.updateToolbarPosition = function() { 5015 var paneStack = document.getElementById("zotero-pane-stack"); 5016 if(paneStack.hidden) return; 5017 5018 var stackedLayout = Zotero.Prefs.get("layout") === "stacked"; 5019 5020 var collectionsPane = document.getElementById("zotero-collections-pane"); 5021 var collectionsToolbar = document.getElementById("zotero-collections-toolbar"); 5022 var itemsPane = document.getElementById("zotero-items-pane"); 5023 var itemsToolbar = document.getElementById("zotero-items-toolbar"); 5024 var itemPane = document.getElementById("zotero-item-pane"); 5025 var itemToolbar = document.getElementById("zotero-item-toolbar"); 5026 5027 collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px'; 5028 5029 if (stackedLayout || itemPane.collapsed) { 5030 // The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing). 5031 itemsToolbar.setAttribute("flex", "1"); 5032 itemToolbar.setAttribute("flex", "0"); 5033 } else { 5034 var itemsToolbarWidth = itemsPane.boxObject.width; 5035 5036 if (collectionsPane.collapsed) { 5037 itemsToolbarWidth -= collectionsToolbar.boxObject.width; 5038 } 5039 // Not sure why this is necessary, but it keeps the search bar from overflowing into the 5040 // right-hand pane 5041 else { 5042 itemsToolbarWidth -= 8; 5043 } 5044 5045 itemsToolbar.style.width = itemsToolbarWidth + "px"; 5046 itemsToolbar.setAttribute("flex", "0"); 5047 itemToolbar.setAttribute("flex", "1"); 5048 } 5049 5050 // Allow item pane to shrink to available height in stacked mode, but don't expand to be too 5051 // wide when there's no persisted width in non-stacked mode 5052 itemPane.setAttribute("flex", stackedLayout ? 1 : 0); 5053 } 5054 5055 /** 5056 * Opens the about dialog 5057 */ 5058 this.openAboutDialog = function() { 5059 window.openDialog('chrome://zotero/content/about.xul', 'about', 'chrome'); 5060 } 5061 5062 /** 5063 * Adds or removes a function to be called when Zotero is reloaded by switching into or out of 5064 * the connector 5065 */ 5066 this.addReloadListener = function(/** @param {Function} **/func) { 5067 if(_reloadFunctions.indexOf(func) === -1) _reloadFunctions.push(func); 5068 } 5069 5070 /** 5071 * Adds or removes a function to be called just before Zotero is reloaded by switching into or 5072 * out of the connector 5073 */ 5074 this.addBeforeReloadListener = function(/** @param {Function} **/func) { 5075 if(_beforeReloadFunctions.indexOf(func) === -1) _beforeReloadFunctions.push(func); 5076 } 5077 5078 /** 5079 * Implements nsIObserver for Zotero reload 5080 */ 5081 var _reloadObserver = { 5082 /** 5083 * Called when Zotero is reloaded (i.e., if it is switched into or out of connector mode) 5084 */ 5085 "observe":function(aSubject, aTopic, aData) { 5086 if(aTopic == "zotero-reloaded") { 5087 Zotero.debug("Reloading Zotero pane"); 5088 for (let func of _reloadFunctions) func(aData); 5089 } else if(aTopic == "zotero-before-reload") { 5090 Zotero.debug("Zotero pane caught before-reload event"); 5091 for (let func of _beforeReloadFunctions) func(aData); 5092 } 5093 } 5094 }; 5095 } 5096 5097 /** 5098 * Keep track of which ZoteroPane was local (since ZoteroPane object might get swapped out for a 5099 * tab's ZoteroPane) 5100 */ 5101 var ZoteroPane_Local = ZoteroPane;