diff --git a/chrome/content/zotero/xpcom/api.js b/chrome/content/zotero/xpcom/api.js index c3225dc7b..2a38a569b 100644 --- a/chrome/content/zotero/xpcom/api.js +++ b/chrome/content/zotero/xpcom/api.js @@ -36,89 +36,109 @@ Zotero.API = { getResultsFromParams: Zotero.Promise.coroutine(function* (params) { - var results; - switch (params.scopeObject) { - case 'collections': - if (params.scopeObjectKey) { - var col = yield Zotero.Collections.getByLibraryAndKeyAsync( - params.libraryID, params.scopeObjectKey - ); - } - else { - var col = yield Zotero.Collections.getAsync(params.scopeObjectID); - } - if (!col) { - throw new Error('Invalid collection ID or key'); - } - yield col.loadChildItems(); - results = col.getChildItems(); - break; - - case 'searches': - if (params.scopeObjectKey) { - var s = yield Zotero.Searches.getByLibraryAndKeyAsync( - params.libraryID, params.scopeObjectKey - ); - } - else { - var s = yield Zotero.Searches.getAsync(params.scopeObjectID); - } - if (!s) { - throw new Error('Invalid search ID or key'); - } - - // FIXME: Hack to exclude group libraries for now - var s2 = new Zotero.Search(); - s2.setScope(s); - var groups = Zotero.Groups.getAll(); - for each(var group in groups) { - yield s2.addCondition('libraryID', 'isNot', group.libraryID); - } - var ids = yield s2.search(); - break; - - default: - if (params.scopeObject) { - throw new Error("Invalid scope object '" + params.scopeObject + "'"); - } - - if (params.itemKey) { - var s = new Zotero.Search; - yield s.addCondition('libraryID', 'is', params.libraryID); - yield s.addCondition('blockStart'); - for (let i=0; i<params.itemKey.length; i++) { - let itemKey = params.itemKey[i]; - yield s.addCondition('key', 'is', itemKey); - } - yield s.addCondition('blockEnd'); - var ids = yield s.search(); - } - else { - // Display all items - var s = new Zotero.Search(); - yield s.addCondition('libraryID', 'is', params.libraryID); - yield s.addCondition('noChildren', 'true'); - var ids = yield s.search(); - } + if (!params.objectType) { + throw new Error("objectType not specified"); } - if (results) { - // Filter results by item key - if (params.itemKey) { - results = results.filter(function (result) { - return params.itemKey.indexOf(result.key) !== -1; - }); + var results; + + if (params.objectType == 'item') { + switch (params.scopeObject) { + case 'collections': + if (params.scopeObjectKey) { + var col = yield Zotero.Collections.getByLibraryAndKeyAsync( + params.libraryID, params.scopeObjectKey + ); + } + else { + var col = yield Zotero.Collections.getAsync(params.scopeObjectID); + } + if (!col) { + throw new Error('Invalid collection ID or key'); + } + yield col.loadChildItems(); + results = col.getChildItems(); + break; + + case 'searches': + if (params.scopeObjectKey) { + var s = yield Zotero.Searches.getByLibraryAndKeyAsync( + params.libraryID, params.scopeObjectKey + ); + } + else { + var s = yield Zotero.Searches.getAsync(params.scopeObjectID); + } + if (!s) { + throw new Error('Invalid search ID or key'); + } + + // FIXME: Hack to exclude group libraries for now + var s2 = new Zotero.Search(); + s2.setScope(s); + var groups = Zotero.Groups.getAll(); + for each(var group in groups) { + yield s2.addCondition('libraryID', 'isNot', group.libraryID); + } + var ids = yield s2.search(); + break; + + default: + if (params.scopeObject) { + throw new Error("Invalid scope object '" + params.scopeObject + "'"); + } + + var s = new Zotero.Search; + if (params.libraryID !== undefined) { + yield s.addCondition('libraryID', 'is', params.libraryID); + } + + if (params.objectKey) { + yield s.addCondition('key', 'is', params.objectKey); + } + else if (params.objectID) { + Zotero.debug('adding ' + params.objectID); + yield s.addCondition('itemID', 'is', params.objectID); + } + + if (params.itemKey) { + yield s.addCondition('blockStart'); + for (let i=0; i<params.itemKey.length; i++) { + let itemKey = params.itemKey[i]; + yield s.addCondition('key', 'is', itemKey); + } + yield s.addCondition('blockEnd'); + } + + // Display all top-level items + /*if (params.onlyTopLevel) { + yield s.addCondition('noChildren', 'true'); + }*/ + + var ids = yield s.search(); + } + + if (results) { + // Filter results by item key + if (params.itemKey) { + results = results.filter(function (result) { + return params.itemKey.indexOf(result.key) !== -1; + }); + } + } + else if (ids) { + // Filter results by item key + if (params.itemKey) { + ids = ids.filter(function (id) { + var [libraryID, key] = Zotero.Items.getLibraryAndKeyFromID(id); + return params.itemKey.indexOf(key) !== -1; + }); + } + results = yield Zotero.Items.getAsync(ids); } } - else if (ids) { - // Filter results by item key - if (params.itemKey) { - ids = ids.filter(function (id) { - var [libraryID, key] = Zotero.Items.getLibraryAndKeyFromID(id); - return params.itemKey.indexOf(key) !== -1; - }); - } - results = yield Zotero.Items.getAsync(ids); + else { + throw new Error("Unsupported object type '" + params.objectType + "'"); } return results; diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js index ceda5de17..30a2eaa11 100644 --- a/chrome/content/zotero/xpcom/collectionTreeView.js +++ b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -36,10 +36,11 @@ */ Zotero.CollectionTreeView = function() { + Zotero.LibraryTreeView.apply(this); + this.itemToSelect = null; this.hideSources = []; - this._treebox = null; this._highlightedRows = {}; this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share', 'group', 'trash', 'bucket'], 'collectionTreeView'); this._containerState = {}; @@ -57,6 +58,8 @@ Object.defineProperty(Zotero.CollectionTreeView.prototype, "selectedTreeRow", { } }); + + /* * Called by the tree itself */ @@ -93,6 +96,9 @@ Zotero.CollectionTreeView.prototype.setTree = Zotero.Promise.coroutine(function* var row = yield this.getLastViewedRow(); this.selection.select(row); this._treebox.ensureRowIsVisible(row); + + yield this._runListeners('load'); + this._initialized = true; } catch (e) { Zotero.debug(e, 1); diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js index 01a162d14..28f165be2 100644 --- a/chrome/content/zotero/xpcom/itemTreeView.js +++ b/chrome/content/zotero/xpcom/itemTreeView.js @@ -35,18 +35,16 @@ * Constructor for the ItemTreeView object */ Zotero.ItemTreeView = function (collectionTreeRow, sourcesOnly) { + Zotero.LibraryTreeView.apply(this); + this.wrappedJSObject = this; this.rowCount = 0; this.collectionTreeRow = collectionTreeRow; - this._initialized = false; this._skipKeypress = false; this._sourcesOnly = sourcesOnly; - this._callbacks = []; - - this._treebox = null; this._ownerDocument = null; this._needsSort = false; @@ -62,17 +60,6 @@ Zotero.ItemTreeView = function (collectionTreeRow, sourcesOnly) { Zotero.ItemTreeView.prototype = Object.create(Zotero.LibraryTreeView.prototype); Zotero.ItemTreeView.prototype.type = 'item'; -Zotero.ItemTreeView.prototype.addCallback = function(callback) { - this._callbacks.push(callback); -} - - -Zotero.ItemTreeView.prototype._runCallbacks = Zotero.Promise.coroutine(function* () { - for each(var cb in this._callbacks) { - yield Zotero.Promise.resolve(cb()); - } -}); - /** * Called by the tree itself @@ -251,12 +238,12 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree yield this.sort(); // Only yield if there are callbacks; otherwise, we're almost done - if(this._callbacks.length && this._waitAfter && Date.now() > this._waitAfter) yield Zotero.Promise.resolve(); + if (this._listeners.load.length && this._waitAfter && Date.now() > this._waitAfter) yield Zotero.Promise.resolve(); yield this.expandMatchParents(); - //Zotero.debug('Running callbacks in itemTreeView.setTree()', 4); - yield this._runCallbacks(); + yield this._runListeners('load'); + this._initialized = true; if (this._ownerDocument.defaultView.ZoteroPane_Local) { this._ownerDocument.defaultView.ZoteroPane_Local.clearItemsPaneMessage(); @@ -1805,8 +1792,7 @@ Zotero.ItemTreeView.prototype.setFilter = Zotero.Promise.coroutine(function* (ty //this._treebox.endUpdateBatch(); this.selection.selectEventsSuppressed = false; - //Zotero.debug('Running callbacks in itemTreeView.setFilter()', 4); - yield this._runCallbacks(); + yield this._runListeners('load'); }); diff --git a/chrome/content/zotero/xpcom/libraryTreeView.js b/chrome/content/zotero/xpcom/libraryTreeView.js index 76229df00..91f608e88 100644 --- a/chrome/content/zotero/xpcom/libraryTreeView.js +++ b/chrome/content/zotero/xpcom/libraryTreeView.js @@ -23,8 +23,35 @@ ***** END LICENSE BLOCK ***** */ -Zotero.LibraryTreeView = function () {}; +Zotero.LibraryTreeView = function () { + this._initialized = false; + this._listeners = { + load: [] + }; +}; + Zotero.LibraryTreeView.prototype = { + addEventListener: function(event, listener) { + if (event == 'load') { + // If already initialized run now + if (this._initialized) { + listener(); + } + else { + this._listeners[event].push(listener); + } + } + }, + + + _runListeners: Zotero.Promise.coroutine(function* (event) { + var listener; + while (listener = this._listeners[event].shift()) { + yield Zotero.Promise.resolve(listener()); + } + }), + + /** * Called while a drag is over the tree */ diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js index fe0674a9a..ebeae812c 100644 --- a/chrome/content/zotero/xpcom/search.js +++ b/chrome/content/zotero/xpcom/search.js @@ -2189,6 +2189,18 @@ Zotero.SearchConditions = new function(){ noLoad: true }, + { + name: 'itemID', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'itemID', + special: true, + noLoad: true + }, + { name: 'annotation', operators: { diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index f75d11f59..c2789be76 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -32,6 +32,7 @@ var ZoteroPane = new function() var _unserialized = false; this.collectionsView = false; this.itemsView = false; + this._listeners = {}; this.__defineGetter__('loaded', function () _loaded); //Privileged methods @@ -1099,7 +1100,7 @@ var ZoteroPane = new function() this.onCollectionSelected = Zotero.Promise.coroutine(function* () { var collectionTreeRow = this.getCollectionTreeRow(); - if (this.itemsView.collectionTreeRow == collectionTreeRow) { + if (this.itemsView && this.itemsView.collectionTreeRow == collectionTreeRow) { Zotero.debug("Collection selection hasn't changed"); return; } @@ -1167,7 +1168,14 @@ var ZoteroPane = new function() this.itemsView.onError = function () { ZoteroPane_Local.displayErrorMessage(); }; - this.itemsView.addCallback(this.setTagScope); + // If any queued load listeners, set them to run when the tree is ready + if (this._listeners.itemsLoaded) { + let listener; + while (listener = this._listeners.itemsLoaded.shift()) { + this.itemsView.addEventListener('load', listener); + } + } + this.itemsView.addEventListener('load', this.setTagScope); document.getElementById('zotero-items-tree').view = this.itemsView; // Add events to treecolpicker to update menu before showing/hiding @@ -1953,34 +1961,77 @@ var ZoteroPane = new function() return false; } - if (!this.itemsView) { - Components.utils.reportError("Items view not set in ZoteroPane_Local.selectItem()"); - return false; + // Restore window if it's in the dock + if (window.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) { + window.restore(); } - var currentLibraryID = this.getSelectedLibraryID(); - // If in a different library - if (item.libraryID != currentLibraryID) { - Zotero.debug("Library ID differs; switching library"); - yield this.collectionsView.selectLibrary(item.libraryID); - } - // Force switch to library view - else if (!this.collectionsView.selectedTreeRow.isLibrary() && inLibrary) { - Zotero.debug("Told to select in library; switching to library"); - yield this.collectionsView.selectLibrary(item.libraryID); + if (!this.collectionsView) { + throw new Error("Collections view not loaded"); } - var selected = yield this.itemsView.selectItem(itemID, expand); - if (!selected) { - Zotero.debug("Item was not selected; switching to library"); - yield this.collectionsView.selectLibrary(item.libraryID); - yield this.itemsView.selectItem(itemID, expand); - } + var self = this; + this.collectionsView.addEventListener('load', function () { + Zotero.spawn(function* () { + var currentLibraryID = self.getSelectedLibraryID(); + // If in a different library + if (item.libraryID != currentLibraryID) { + Zotero.debug("Library ID differs; switching library"); + yield self.collectionsView.selectLibrary(item.libraryID); + } + // Force switch to library view + else if (!self.collectionsView.selectedTreeRow.isLibrary() && inLibrary) { + Zotero.debug("Told to select in library; switching to library"); + yield self.collectionsView.selectLibrary(item.libraryID); + } + + self.addEventListener('itemsLoaded', function () { + Zotero.spawn(function* () { + var selected = yield self.itemsView.selectItem(itemID, expand); + if (!selected) { + Zotero.debug("Item was not selected; switching to library"); + yield self.collectionsView.selectLibrary(item.libraryID); + yield self.itemsView.selectItem(itemID, expand); + } + }); + }); + }); + }); + + // open Zotero pane + this.show(); return true; }); + this.addEventListener = function (event, listener) { + if (event == 'itemsLoaded') { + if (this.itemsView) { + this.itemsView.addEventListener('load', listener); + } + else { + if (!this._listeners.itemsLoaded) { + this._listeners.itemsLoaded = []; + } + this._listeners.itemsLoaded.push(listener); + } + } + }; + + + this._runListeners = Zotero.Promise.coroutine(function* (event) { + if (!this._listeners[event]) { + return; + } + + var listener; + while (listener = this._listeners[event].shift()) { + yield Zotero.Promise.resolve(listener()); + } + }); + + this.getSelectedLibraryID = function () { return this.collectionsView.getSelectedLibraryID(); } diff --git a/components/zotero-protocol-handler.js b/components/zotero-protocol-handler.js index 319fdadb4..9a78202cd 100644 --- a/components/zotero-protocol-handler.js +++ b/components/zotero-protocol-handler.js @@ -110,6 +110,7 @@ function ZoteroProtocolHandler() { } var params = { + objectType: 'item', format: 'html', sort: 'title' }; @@ -793,47 +794,68 @@ function ZoteroProtocolHandler() { var SelectExtension = { newChannel: function (uri) { return new AsyncChannel(uri, function* () { - generateContent:try { - var mimeType, content = ''; - - var [path, queryString] = uri.path.substr(1).split('?'); - var [type, id] = path.split('/'); - - // currently only able to select one item - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var win = wm.getMostRecentWindow("navigator:browser"); - - // restore window if it's in the dock - if(win.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) { - win.restore(); - } - - // open Zotero pane - win.ZoteroPane.show(); - - if(!id) return; - - var lkh = Zotero.Items.parseLibraryKeyHash(id); + var path = uri.path; + if (!path) { + return 'Invalid URL'; + } + // Strip leading '/' + path = path.substr(1); + var mimeType, content = ''; + + var params = { + objectType: 'item' + }; + var router = new Zotero.Router(params); + + // Item within a collection or search + router.add('library/:scopeObject/:scopeObjectKey/items/:objectKey', function () { + params.libraryID = 0; + }); + router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/:objectKey'); + + // All items + router.add('library/items/:objectKey', function () { + params.libraryID = 0; + }); + router.add('groups/:groupID/items/:objectKey'); + + // Old-style URLs + router.add('item/:id', function () { + var lkh = Zotero.Items.parseLibraryKeyHash(params.id); if (lkh) { - var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key); + params.libraryID = lkh.libraryID; + params.objectKey = lkh.key; } else { - var item = Zotero.Items.get(id); + params.objectID = params.id; } - if (!item) { - var msg = "Item " + id + " not found in zotero://select"; - Zotero.debug(msg, 2); - Components.utils.reportError(msg); - return; - } - - win.ZoteroPane.selectItem(item.id); + delete params.id; + }); + router.run(path); + + try { + Zotero.API.parseParams(params); + var results = yield Zotero.API.getResultsFromParams(params); } - catch (e){ - Zotero.debug(e); - throw (e); + catch (e) { + Zotero.debug(e, 1); + return e.toString(); } + + + if (!results.length) { + var msg = "Selected items not found"; + Zotero.debug(msg, 2); + Components.utils.reportError(msg); + return; + } + + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + + // TODO: Currently only able to select one item + yield win.ZoteroPane.selectItem(results[0].id); }); } }; @@ -1226,13 +1248,18 @@ AsyncChannel.prototype = { }); return promise; } + else if (data === undefined) { + this.cancel(0x804b0002); // BINDING_ABORTED + } else { throw new Error("Invalid return type (" + typeof data + ") from generator passed to AsyncChannel"); } }.bind(this)) .then(function () { - Zotero.debug("AsyncChannel request succeeded in " + (new Date - t) + " ms"); - channel._isPending = false; + if (this._isPending) { + Zotero.debug("AsyncChannel request succeeded in " + (new Date - t) + " ms"); + channel._isPending = false; + } }) .catch(function (e) { Zotero.debug(e, 1);