libzotero
Version:
javascript libZotero
1,308 lines (1,153 loc) • 44 kB
JavaScript
'use strict';
var log = require('./Log.js').Logger('libZotero:Library');
/**
* A user or group Zotero library. This is generally the top level object
* through which interactions should happen. It houses containers for
* Zotero API objects (collections, items, etc) and handles making requests
* with particular API credentials, as well as storing data locally.
* @param {string} type type of library, 'user' or 'group'
* @param {int} libraryID ID of the library
* @param {string} libraryUrlIdentifier identifier used in urls, could be library id or user/group slug
* @param {string} apiKey key to use for API requests
*/
var Library = function Library(type, libraryID, libraryUrlIdentifier, apiKey) {
log.debug('Zotero.Library constructor', 3);
log.debug('Library Constructor: ' + type + ' ' + libraryID + ' ', 3);
var library = this;
log.debug(libraryUrlIdentifier, 4);
library.instance = 'Zotero.Library';
library.libraryVersion = 0;
library.syncState = {
earliestVersion: null,
latestVersion: null
};
library._apiKey = apiKey || '';
library.libraryUrlIdentifier = libraryUrlIdentifier;
if (Zotero.config.librarySettings) {
library.libraryBaseWebsiteUrl = Zotero.config.librarySettings.libraryPathString;
} else {
library.libraryBaseWebsiteUrl = Zotero.config.baseWebsiteUrl;
if (type == 'group') {
library.libraryBaseWebsiteUrl += 'groups/';
}
if (libraryUrlIdentifier) {
this.libraryBaseWebsiteUrl += libraryUrlIdentifier + '/items';
} else {
log.warn('no libraryUrlIdentifier specified');
}
}
//object holders within this library, whether tied to a specific library or not
library.items = new Zotero.Items();
library.items.owningLibrary = library;
library.itemKeys = [];
library.collections = new Zotero.Collections();
library.collections.libraryUrlIdentifier = library.libraryUrlIdentifier;
library.collections.owningLibrary = library;
library.tags = new Zotero.Tags();
library.searches = new Zotero.Searches();
library.searches.owningLibrary = library;
library.groups = new Zotero.Groups();
library.groups.owningLibrary = library;
library.deleted = new Zotero.Deleted();
library.deleted.owningLibrary = library;
if (!type) {
//return early if library not specified
log.warn('No type specified for library');
return;
}
//attributes tying instance to a specific Zotero library
library.type = type;
library.libraryType = type;
library.libraryID = libraryID;
library.libraryString = Zotero.utils.libraryString(library.libraryType, library.libraryID);
library.libraryUrlIdentifier = libraryUrlIdentifier;
//initialize preferences object
library.preferences = new Zotero.Preferences(Zotero.store, library.libraryString);
if (typeof window === 'undefined') {
Zotero.config.useIndexedDB = false;
log.warn('Node detected; disabling indexedDB');
} else {
//initialize indexedDB if we're supposed to use it
//detect safari until they fix their shit
var is_chrome = navigator.userAgent.indexOf('Chrome') > -1;
var is_explorer = navigator.userAgent.indexOf('MSIE') > -1;
var is_firefox = navigator.userAgent.indexOf('Firefox') > -1;
var is_safari = navigator.userAgent.indexOf('Safari') > -1;
var is_opera = navigator.userAgent.toLowerCase().indexOf('op') > -1;
if (is_chrome && is_safari) {
is_safari = false;
}
if (is_chrome && is_opera) {
is_chrome = false;
}
if (is_safari) {
Zotero.config.useIndexedDB = false;
log.warn('Safari detected; disabling indexedDB');
}
}
if (Zotero.config.useIndexedDB === true) {
log.debug('Library Constructor: indexedDB init', 3);
var idbLibrary = new Zotero.Idb.Library(library.libraryString);
idbLibrary.owningLibrary = this;
library.idbLibrary = idbLibrary;
library.cachedDataPromise = idbLibrary.init().then(function () {
log.debug('Library Constructor: idbInitD Done', 3);
if (Zotero.config.preloadCachedLibrary === true) {
log.debug('Library Constructor: preloading cached library', 3);
var cacheLoadD = library.loadIndexedDBCache();
cacheLoadD.then(function () {
//TODO: any stuff that needs to execute only after cache is loaded
//possibly fire new events to cause display to refresh after load
log.debug('Library Constructor: Library.items.itemsVersion: ' + library.items.itemsVersion, 3);
log.debug('Library Constructor: Library.collections.collectionsVersion: ' + library.collections.collectionsVersion, 3);
log.debug('Library Constructor: Library.tags.tagsVersion: ' + library.tags.tagsVersion, 3);
log.debug('Library Constructor: Triggering cachedDataLoaded', 3);
library.trigger('cachedDataLoaded');
}, function (err) {
log.error('Error loading cached library');
log.error(err);
throw new Error('Error loading cached library');
});
return cacheLoadD;
} else {
//trigger cachedDataLoaded since we are done with that step
library.trigger('cachedDataLoaded');
}
}, function () {
//can't use indexedDB. Set to false in config and trigger error to notify user
Zotero.config.useIndexedDB = false;
library.trigger('indexedDBError');
library.trigger('cachedDataLoaded');
log.error('Error initializing indexedDB. Promise rejected.');
//don't re-throw error, since we can still load data from the API
});
} else {
library.cachedDataPromise = Promise.resolve();
}
library.dirty = false;
//set noop data-change callbacks
library.tagsChanged = function () {};
library.collectionsChanged = function () {};
library.itemsChanged = function () {};
};
/**
* Items columns for which sorting is supported
* @type {Array}
*/
Library.prototype.sortableColumns = ['title', 'creator', 'itemType', 'date', 'year', 'publisher', 'publicationTitle', 'journalAbbreviation', 'language', 'accessDate', 'libraryCatalog', 'callNumber', 'rights', 'dateAdded', 'dateModified',
/*'numChildren',*/
'addedBy'
/*'modifiedBy'*/];
/**
* Columns that can be displayed in an items table UI
* @type {Array}
*/
Library.prototype.displayableColumns = ['title', 'creator', 'itemType', 'date', 'year', 'publisher', 'publicationTitle', 'journalAbbreviation', 'language', 'accessDate', 'libraryCatalog', 'callNumber', 'rights', 'dateAdded', 'dateModified', 'numChildren', 'addedBy'
/*'modifiedBy'*/];
/**
* Items columns that only apply to group libraries
* @type {Array}
*/
Library.prototype.groupOnlyColumns = ['addedBy'
/*'modifiedBy'*/];
/**
* Sort function that converts strings to locale lower case before comparing,
* however this is still not particularly effective at getting correct localized
* sorting in modern browsers due to browser implementations being poor. What we
* really want here is to strip diacritics first.
* @param {string} a [description]
* @param {string} b [description]
* @return {int} [description]
*/
Library.prototype.comparer = function () {
if (Intl) {
return new Intl.Collator().compare;
} else {
return function (a, b) {
if (a.toLocaleLowerCase() == b.toLocaleLowerCase()) {
return 0;
}
if (a.toLocaleLowerCase() < b.toLocaleLowerCase()) {
return -1;
}
return 1;
};
}
};
//Zotero library wrapper around jQuery ajax that returns a jQuery promise
//@url String url to request or object for input to apiRequestUrl and query string
//@type request method
//@options jquery options that are not the default for Zotero requests
Library.prototype.ajaxRequest = function (url, type, options) {
log.debug('Library.ajaxRequest', 3);
if (!type) {
type = 'GET';
}
if (!options) {
options = {};
}
var requestObject = {
url: url,
type: type
};
requestObject = Z.extend({}, requestObject, options);
if (!requestObject.key && this._apiKey != '') {
requestObject.key = this._apiKey;
}
log.debug(requestObject, 3);
return Zotero.net.queueRequest(requestObject);
};
//Take an array of objects that specify Zotero API requests and perform them
//in sequence.
//return deferred that gets resolved when all requests have gone through.
//Update versions after each request, otherwise subsequent writes won't go through.
//or do we depend on specified callbacks to update versions if necessary?
//fail on error?
//request object must specify: url, method, body, headers, success callback, fail callback(?)
/**
* Take an array of objects that specify Zotero API requests and perform them
* in sequence. Return a promise that gets resolved when all requests have
* gone through.
* @param {[] Objects} requests Array of objects specifying requests to be made
* @return {Promise} Promise that resolves/rejects along with requests
*/
Library.prototype.sequentialRequests = function (requests) {
var _this = this;
log.debug('Zotero.Library.sequentialRequests', 3);
var modRequests = requests.map(function (request) {
if (!request.key && _this._apiKey != '') {
request.key = _this._apiKey;
}
return request;
});
return Zotero.net.queueRequest(modRequests);
};
/**
* Generate a website url based on a dictionary of variables and the configured
* libraryBaseWebsiteUrl
* @param {Object} urlvars Dictionary of key/value variables
* @return {string} website url
*/
Library.prototype.websiteUrl = function (urlvars) {
log.debug('Zotero.library.websiteUrl', 3);
log.debug(urlvars, 4);
var library = this;
var urlVarsArray = [];
Object.keys(urlvars).forEach(function (key) {
var value = urlvars[key];
if (value === '') return;
urlVarsArray.push(key + '/' + value);
});
urlVarsArray.sort();
log.debug(urlVarsArray, 4);
var pathVarsString = urlVarsArray.join('/');
return library.libraryBaseWebsiteUrl + '/' + pathVarsString;
};
Library.prototype.synchronize = function () {
//get updated group metadata if applicable
// (this is an individual library method, so only necessary if this is
// a group library and we want to keep info about it)
//sync library data
// get updated collections versions newer than current library version
// get updated searches versions newer than current library version
// get updated item versions newer than current library version
//
};
/**
* Make and process API requests to update the local library items based on the
* versions we have locally. When the promise is resolved, we should have up to
* date items in this library's items container, as well as saved to indexedDB
* if configured to use it.
* @return {Promise} Promise
*/
Library.prototype.loadUpdatedItems = function () {
log.debug('Zotero.Library.loadUpdatedItems', 3);
var library = this;
//sync from the libraryVersion if it exists, otherwise use the itemsVersion, which is likely
//derived from the most recent version of any individual item we have.
var syncFromVersion = library.libraryVersion ? library.libraryVersion : library.items.itemsVersion;
return Promise.resolve(library.updatedVersions('items', syncFromVersion)).then(function (response) {
log.debug('itemVersions resolved', 3);
log.debug('items Last-Modified-Version: ' + response.lastModifiedVersion, 3);
library.items.updateSyncState(response.lastModifiedVersion);
var itemVersions = response.data;
library.itemVersions = itemVersions;
var itemKeys = [];
Object.keys(itemVersions).forEach(function (key) {
var val = itemVersions[key];
var item = library.items.getItem(key);
if (!item || item.apiObj.key != val) {
itemKeys.push(key);
}
});
return library.loadItemsFromKeys(itemKeys);
}).then(function (responses) {
log.debug('loadItemsFromKeys resolved', 3);
library.items.updateSyncedVersion();
//TODO: library needs its own state
if (Zotero.state) {
var displayParams = Zotero.state.getUrlVars();
library.buildItemDisplayView(displayParams);
}
//save updated items to IDB
if (Zotero.config.useIndexedDB) {
var saveItemsD = library.idbLibrary.updateItems(library.items.objectArray);
}
});
};
Library.prototype.loadUpdatedCollections = function () {
log.debug('Zotero.Library.loadUpdatedCollections', 3);
var library = this;
//sync from the libraryVersion if it exists, otherwise use the collectionsVersion, which is likely
//derived from the most recent version of any individual collection we have.
log.debug('library.collections.collectionsVersion:' + library.collections.collectionsVersion, 4);
//var syncFromVersion = library.libraryVersion ? library.libraryVersion : library.collections.collectionsVersion;
var syncFromVersion = library.collections.collectionsVersion;
log.debug('loadUpdatedCollections syncFromVersion: ' + syncFromVersion, 3);
//we need modified collectionKeys regardless, so load them
return library.updatedVersions('collections', syncFromVersion).then(function (response) {
log.debug('collectionVersions finished', 3);
log.debug('Collections Last-Modified-Version: ' + response.lastModifiedVersion, 3);
//start the syncState version tracking. This should be the earliest version throughout
library.collections.updateSyncState(response.lastModifiedVersion);
var collectionVersions = response.data;
library.collectionVersions = collectionVersions;
var collectionKeys = [];
Object.keys(collectionVersions).forEach(function (key) {
var val = collectionVersions[key];
var c = library.collections.getCollection(key);
if (!c || c.apiObj.version != val) {
collectionKeys.push(key);
}
});
if (collectionKeys.length === 0) {
log.debug('No collectionKeys need updating. resolving', 3);
return response;
} else {
log.debug('fetching collections by key', 3);
return library.loadCollectionsFromKeys(collectionKeys).then(function () {
var collections = library.collections;
collections.initSecondaryData();
log.debug('All updated collections loaded', 3);
library.collections.updateSyncedVersion();
//TODO: library needs its own state
//save updated collections to cache
log.debug('loadUpdatedCollections complete - saving collections to cache before resolving', 3);
log.debug('collectionsVersion: ' + library.collections.collectionsVersion, 3);
//library.saveCachedCollections();
//save updated collections to IDB
if (Zotero.config.useIndexedDB) {
return library.idbLibrary.updateCollections(collections.collectionsArray);
}
});
}
}).then(function () {
log.debug('done getting collection data. requesting deleted data', 3);
return library.getDeleted(library.libraryVersion);
}).then(function (response) {
log.debug('got deleted collections data: removing local copies', 3);
log.debug(library.deleted, 3);
if (library.deleted.deletedData.collections && library.deleted.deletedData.collections.length > 0) {
library.collections.removeLocalCollections(library.deleted.deletedData.collections);
}
});
};
Library.prototype.loadUpdatedTags = function () {
log.debug('Zotero.Library.loadUpdatedTags', 3);
var library = this;
log.debug('tagsVersion: ' + library.tags.tagsVersion, 3);
return Promise.resolve(library.loadAllTags({ since: library.tags.tagsVersion })).then(function () {
log.debug('done getting tags, request deleted tags data', 3);
return library.getDeleted(library.libraryVersion);
}).then(function (response) {
log.debug('got deleted tags data', 3);
if (library.deleted.deletedData.tags && library.deleted.deletedData.tags.length > 0) {
library.tags.removeTags(library.deleted.deletedData.tags);
}
//save updated tags to IDB
if (Zotero.config.useIndexedDB) {
log.debug('saving updated tags to IDB', 3);
var saveTagsD = library.idbLibrary.updateTags(library.tags.tagsArray);
}
});
};
Library.prototype.getDeleted = function (version) {
log.debug('Zotero.Library.getDeleted', 3);
var library = this;
var urlconf = {
target: 'deleted',
libraryType: library.libraryType,
libraryID: library.libraryID,
since: version
};
//if there is already a request working, create a new promise to resolve
//when the actual request finishes
if (library.deleted.pending) {
log.debug('getDeleted resolving with previously pending promise', 3);
return Promise.resolve(library.deleted.pendingPromise);
}
//don't fetch again if version we'd be requesting is between
//deleted.newer and delete.deleted versions, just use that one
log.debug('version:' + version, 3);
log.debug('sinceVersion:' + library.deleted.sinceVersion, 3);
log.debug('untilVersion:' + library.deleted.untilVersion, 3);
if (library.deleted.untilVersion && version >= library.deleted.sinceVersion /*&&
version < library.deleted.untilVersion*/) {
log.debug('deletedVersion matches requested: immediately resolving', 3);
return Promise.resolve(library.deleted.deletedData);
}
library.deleted.pending = true;
library.deleted.pendingPromise = library.ajaxRequest(urlconf).then(function (response) {
log.debug('got deleted response', 3);
library.deleted.deletedData = response.data;
log.debug('Deleted Last-Modified-Version:' + response.lastModifiedVersion, 3);
library.deleted.untilVersion = response.lastModifiedVersion;
library.deleted.sinceVersion = version;
}).then(function (response) {
log.debug('cleaning up deleted pending', 3);
library.deleted.pending = false;
library.deleted.pendingPromise = false;
});
return library.deleted.pendingPromise;
};
Library.prototype.processDeletions = function (deletions) {
var library = this;
//process deleted collections
library.collections.processDeletions(deletions.collections);
//process deleted items
library.items.processDeletions(deletions.items);
};
//Get a full bibliography from the API for web based citating
Library.prototype.loadFullBib = function (itemKeys, style) {
var library = this;
var itemKeyString = itemKeys.join(',');
var urlconfig = {
'target': 'items',
'libraryType': library.libraryType,
'libraryID': library.libraryID,
'itemKey': itemKeyString,
'format': 'bib',
'linkwrap': '1'
};
if (itemKeys.length == 1) {
urlconfig.target = 'item';
}
if (style) {
urlconfig['style'] = style;
}
var loadBibPromise = library.ajaxRequest(urlconfig).then(function (response) {
return response.data;
});
return loadBibPromise;
};
//load bib for a single item from the API
Library.prototype.loadItemBib = function (itemKey, style) {
log.debug('Zotero.Library.loadItemBib', 3);
var library = this;
var urlconfig = {
'target': 'item',
'libraryType': library.libraryType,
'libraryID': library.libraryID,
'itemKey': itemKey,
'content': 'bib'
};
if (style) {
urlconfig['style'] = style;
}
var itemBibPromise = library.ajaxRequest(urlconfig).then(function (response) {
var item = new Zotero.Item(response.data);
var bibContent = item.apiObj.bib;
return bibContent;
});
return itemBibPromise;
};
//load library settings from Zotero API and return a promise that gets resolved with
//the Zotero.Preferences object for this library
Library.prototype.loadSettings = function () {
log.debug('Zotero.Library.loadSettings', 3);
var library = this;
var urlconfig = {
'target': 'settings',
'libraryType': library.libraryType,
'libraryID': library.libraryID
};
return library.ajaxRequest(urlconfig).then(function (response) {
var resultObject;
if (typeof response.data == 'string') {
resultObject = JSON.parse(response.data);
} else {
resultObject = response.data;
}
//save the full settings object so we have it available if we need to write,
//even if it has settings we don't use or know about
library.preferences.setPref('settings', resultObject);
library.trigger('settingsLoaded');
return library.preferences;
});
};
//take an array of tags and return subset of tags that should be colored, along with
//the colors they should be
Library.prototype.matchColoredTags = function (tags) {
var library = this;
if (!library.tagColors) {
//pull out the settings we know we care about so we can query them directly
var tagColors = [];
var settings = library.preferences.getPref('settings');
if (settings && settings.hasOwnProperty('tagColors')) {
tagColors = settings.tagColors.value;
}
library.tagColors = new Zotero.TagColors(tagColors);
}
return library.tagColors.match(tags);
};
/**
* Duplicate existing Items from this library and save to foreignLibrary
* with relationships indicating the ties. At time of writing, Zotero client
* saves the relationship with either the destination group of two group
* libraries or the personal library.
* @param {Zotero.Item[]} items
* @param {Zotero.Library} foreignLibrary
* @return {Promise.Zotero.Item[]} - newly created items
*/
Library.prototype.sendToLibrary = function (items, foreignLibrary) {
var foreignItems = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
var transferData = item.emptyJsonItem();
transferData.data = Z.extend({}, items[i].apiObj.data);
//clear data that shouldn't be transferred:itemKey, collections
transferData.data.key = '';
transferData.data.version = 0;
transferData.data.collections = [];
delete transferData.data.dateModified;
delete transferData.data.dateAdded;
var newForeignItem = new Zotero.Item(transferData);
newForeignItem.pristine = Z.extend({}, newForeignItem.apiObj);
newForeignItem.initSecondaryData();
//set relationship to tie to old item
if (!newForeignItem.apiObj.data.relations) {
newForeignItem.apiObj.data.relations = {};
}
newForeignItem.apiObj.data.relations['owl:sameAs'] = Zotero.url.relationUrl(item.owningLibrary.libraryType, item.owningLibrary.libraryID, item.key);
foreignItems.push(newForeignItem);
}
return foreignLibrary.items.writeItems(foreignItems);
};
/*METHODS FOR WORKING WITH THE ENTIRE LIBRARY -- NOT FOR GENERAL USE */
//sync pull:
//upload changed data
// get updatedVersions for collections
// get updatedVersions for searches
// get upatedVersions for items
// (sanity check versions we have for individual objects?)
// loadCollectionsFromKeys
// loadSearchesFromKeys
// loadItemsFromKeys
// process updated objects:
// ...
// getDeletedData
// process deleted
// checkConcurrentUpdates (compare Last-Modified-Version from collections?newer request to one from /deleted request)
Library.prototype.updatedVersions = function () {
var target = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'items';
var version = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.libraryVersion;
log.debug('Library.updatedVersions', 3);
var library = this;
var urlconf = {
target: target,
format: 'versions',
libraryType: library.libraryType,
libraryID: library.libraryID,
since: version
};
return library.ajaxRequest(urlconf);
};
//Download and save information about every item in the library
//keys is an array of itemKeys from this library that we need to download
Library.prototype.loadItemsFromKeys = function (keys) {
log.debug('Zotero.Library.loadItemsFromKeys', 3);
var library = this;
return library.loadFromKeys(keys, 'items');
};
//keys is an array of collectionKeys from this library that we need to download
Library.prototype.loadCollectionsFromKeys = function (keys) {
log.debug('Zotero.Library.loadCollectionsFromKeys', 3);
var library = this;
return library.loadFromKeys(keys, 'collections');
};
//keys is an array of searchKeys from this library that we need to download
Library.prototype.loadSeachesFromKeys = function (keys) {
log.debug('Zotero.Library.loadSearchesFromKeys', 3);
var library = this;
return library.loadFromKeys(keys, 'searches');
};
Library.prototype.loadFromKeys = function (keys, objectType) {
log.debug('Zotero.Library.loadFromKeys', 3);
if (!objectType) objectType = 'items';
var library = this;
var keyslices = [];
while (keys.length > 0) {
keyslices.push(keys.splice(0, 50));
}
var requestObjects = [];
keyslices.forEach(function (keyslice) {
var keystring = keyslice.join(',');
switch (objectType) {
case 'items':
requestObjects.push({
url: {
'target': 'items',
'targetModifier': null,
'itemKey': keystring,
'limit': 50,
'libraryType': library.libraryType,
'libraryID': library.libraryID
},
type: 'GET',
success: library.processLoadedItems.bind(library)
});
break;
case 'collections':
requestObjects.push({
url: {
'target': 'collections',
'targetModifier': null,
'collectionKey': keystring,
'limit': 50,
'libraryType': library.libraryType,
'libraryID': library.libraryID
},
type: 'GET',
success: library.processLoadedCollections.bind(library)
});
break;
case 'searches':
requestObjects.push({
url: {
'target': 'searches',
'targetModifier': null,
'searchKey': keystring,
'limit': 50,
'libraryType': library.libraryType,
'libraryID': library.libraryID
},
type: 'GET'
//success: library.processLoadedSearches.bind(library)
});
break;
}
});
var promises = [];
for (var i = 0; i < requestObjects.length; i++) {
var url = requestObjects[i].url;
var type = requestObjects[i].type;
var options = {
success: requestObjects[i].success
};
promises.push(library.ajaxRequest(url, type, options));
}
return Promise.all(promises);
};
//publishes: displayedItemsUpdated
//assume we have up to date information about items in indexeddb.
//build a list of indexedDB filter requests to then intersect to get final result
Library.prototype.buildItemDisplayView = function (params) {
log.debug('Zotero.Library.buildItemDisplayView', 3);
log.debug(params, 4);
//start with list of all items if we don't have collectionKey
//otherwise get the list of items in that collection
var library = this;
//short-circuit if we don't have an initialized IDB yet
if (!library.idbLibrary.db) {
return Promise.resolve([]);
}
var filterPromises = [];
if (params.collectionKey) {
if (params.collectionKey == 'trash') {
filterPromises.push(library.idbLibrary.filterItems('deleted', 1));
} else {
filterPromises.push(library.idbLibrary.filterItems('collectionKeys', params.collectionKey));
}
} else {
filterPromises.push(library.idbLibrary.getOrderedItemKeys('title'));
}
//filter by selected tags
var selectedTags = params.tag || [];
if (typeof selectedTags == 'string') selectedTags = [selectedTags];
for (var i = 0; i < selectedTags.length; i++) {
log.debug('adding selected tag filter', 3);
filterPromises.push(library.idbLibrary.filterItems('itemTagStrings', selectedTags[i]));
}
//TODO: filter by search term.
//(need full text array or to decide what we're actually searching on to implement this locally)
//when all the filters have been applied, combine and sort
return Promise.all(filterPromises).then(function (results) {
var i;
for (i = 0; i < results.length; i++) {
log.debug('result from filterPromise: ' + results[i].length, 3);
log.debug(results[i], 3);
}
var finalItemKeys = library.idbLibrary.intersectAll(results);
var itemsArray = library.items.getItems(finalItemKeys);
log.debug('All filters applied - Down to ' + itemsArray.length + ' items displayed', 3);
log.debug('remove child items and, if not viewing trash, deleted items', 3);
var displayItemsArray = [];
for (i = 0; i < itemsArray.length; i++) {
if (itemsArray[i].apiObj.data.parentItem) {
continue;
}
if (params.collectionKey != 'trash' && itemsArray[i].apiObj.deleted) {
continue;
}
displayItemsArray.push(itemsArray[i]);
}
//sort displayedItemsArray by given or configured column
var orderCol = params['order'] || 'title';
var sort = params['sort'] || 'asc';
log.debug('Sorting by ' + orderCol + ' - ' + sort, 3);
var comparer = Zotero.Library.prototype.comparer();
displayItemsArray.sort(function (a, b) {
var aval = a.get(orderCol);
var bval = b.get(orderCol);
return comparer(aval, bval);
});
if (sort == 'desc') {
log.debug('sort is desc - reversing array', 4);
displayItemsArray.reverse();
}
//publish event signalling we're done
log.debug('triggering publishing displayedItemsUpdated', 3);
library.trigger('displayedItemsUpdated');
return displayItemsArray;
});
};
Library.prototype.trigger = function (eventType, data) {
var library = this;
Zotero.trigger(eventType, data, library.libraryString);
};
Library.prototype.listen = function (events, handler, data) {
var library = this;
var filter = library.libraryString;
Zotero.listen(events, handler, data, filter);
};
//CollectionFunctions
Library.prototype.processLoadedCollections = function (response) {
log.debug('processLoadedCollections', 3);
var library = this;
//clear out display items
log.debug('adding collections to library.collections', 3);
var collectionsAdded = library.collections.addCollectionsFromJson(response.data);
for (var i = 0; i < collectionsAdded.length; i++) {
collectionsAdded[i].associateWithLibrary(library);
}
//update sync state
library.collections.updateSyncState(response.lastModifiedVersion);
Zotero.trigger('loadedCollectionsProcessed', { library: library, collectionsAdded: collectionsAdded });
return response;
};
//create+write a collection given a name and optional parentCollectionKey
Library.prototype.addCollection = function (name, parentCollection) {
log.debug('Zotero.Library.addCollection', 3);
var library = this;
var collection = new Zotero.Collection();
collection.associateWithLibrary(library);
collection.set('name', name);
collection.set('parentCollection', parentCollection);
return library.collections.writeCollections([collection]);
};
//ItemFunctions
//make request for item keys and return jquery ajax promise
Library.prototype.fetchItemKeys = function () {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
log.debug('Zotero.Library.fetchItemKeys', 3);
var library = this;
var urlconfig = Z.extend(true, {
'target': 'items',
'libraryType': this.libraryType,
'libraryID': this.libraryID,
'format': 'keys'
}, config);
return library.ajaxRequest(urlconfig);
};
//get keys of all items marked for deletion
Library.prototype.getTrashKeys = function () {
log.debug('Zotero.Library.getTrashKeys', 3);
var library = this;
var urlconfig = {
'target': 'items',
'libraryType': library.libraryType,
'libraryID': library.libraryID,
'format': 'keys',
'collectionKey': 'trash'
};
return library.ajaxRequest(urlconfig);
};
Library.prototype.emptyTrash = function () {
log.debug('Zotero.Library.emptyTrash', 3);
var library = this;
return library.getTrashKeys().then(function (response) {
var trashedItemKeys = response.data.split('\n');
return library.items.deleteItems(trashedItemKeys, response.lastModifiedVersion);
});
};
//gets the full set of item keys that satisfy `config`
Library.prototype.loadItemKeys = function (config) {
log.debug('Zotero.Library.loadItemKeys', 3);
var library = this;
return this.fetchItemKeys(config).then(function (response) {
log.debug('loadItemKeys proxied callback', 3);
var keys = response.data.split(/[\s]+/);
library.itemKeys = keys;
});
};
//loads a set of items specified by `config`
//The items are added to this Library's items container, as well included as an array of Zotero.Item
//on the returned promise as `response.loadedItems`
Library.prototype.loadItems = function (config) {
log.debug('Zotero.Library.loadItems', 3);
var library = this;
if (!config) {
config = {};
}
var defaultConfig = {
target: 'items',
targetModifier: 'top',
start: 0,
limit: 25,
order: Zotero.config.defaultSortColumn,
sort: Zotero.config.defaultSortOrder
};
//Build config object that should be displayed next and compare to currently displayed
var newConfig = Z.extend({}, defaultConfig, config);
//newConfig.start = parseInt(newConfig.limit, 10) * (parseInt(newConfig.itemPage, 10) - 1);
var urlconfig = Z.extend({
'target': 'items',
'libraryType': library.libraryType,
'libraryID': library.libraryID
}, newConfig);
return library.ajaxRequest(urlconfig).then(function (response) {
log.debug('loadItems proxied callback', 3);
//var library = this;
var items = library.items;
//clear out display items
var loadedItemsArray = items.addItemsFromJson(response.data);
for (var i = 0; i < loadedItemsArray.length; i++) {
loadedItemsArray[i].associateWithLibrary(library);
}
response.loadedItems = loadedItemsArray;
Zotero.trigger('itemsChanged', { library: library });
return response;
});
};
Library.prototype.loadPublications = function (config) {
log.debug('Zotero.Library.loadPublications', 3);
var library = this;
if (!config) {
config = {};
}
var defaultConfig = {
target: 'publications',
start: 0,
limit: 50,
order: Zotero.config.defaultSortColumn,
sort: Zotero.config.defaultSortOrder,
include: 'bib'
};
//Build config object that should be displayed next and compare to currently displayed
var newConfig = Z.extend({}, defaultConfig, config);
var urlconfig = Z.extend({
'target': 'publications',
'libraryType': library.libraryType,
'libraryID': library.libraryID
}, newConfig);
return library.ajaxRequest(urlconfig).then(function (response) {
log.debug('loadPublications proxied callback', 3);
var publicationItems = [];
var parsedItemJson = response.data;
parsedItemJson.forEach(function (itemObj) {
var item = new Zotero.Item(itemObj);
publicationItems.push(item);
});
response.publicationItems = publicationItems;
return response;
});
};
Library.prototype.processLoadedItems = function (response) {
log.debug('processLoadedItems', 3);
var library = this;
var items = library.items;
//clear out display items
var loadedItemsArray = items.addItemsFromJson(response.data);
for (var i = 0; i < loadedItemsArray.length; i++) {
loadedItemsArray[i].associateWithLibrary(library);
}
//update sync state
library.items.updateSyncState(response.lastModifiedVersion);
Zotero.trigger('itemsChanged', { library: library, loadedItems: loadedItemsArray });
return response;
};
Library.prototype.loadItem = function (itemKey) {
log.debug('Zotero.Library.loadItem', 3);
var library = this;
if (!config) {
var config = {};
}
var urlconfig = {
'target': 'item',
'libraryType': library.libraryType,
'libraryID': library.libraryID,
'itemKey': itemKey
};
return library.ajaxRequest(urlconfig).then(function (response) {
log.debug('Got loadItem response', 3);
var item = new Zotero.Item(response.data);
item.owningLibrary = library;
library.items.itemObjects[item.key] = item;
Zotero.trigger('itemsChanged', { library: library });
return item;
}, function (response) {
log.warn('Error loading Item');
});
};
Library.prototype.trashItem = function (itemKey) {
var library = this;
return library.items.trashItems([library.items.getItem(itemKey)]);
};
Library.prototype.untrashItem = function (itemKey) {
log.debug('Zotero.Library.untrashItem', 3);
if (!itemKey) return false;
var item = this.items.getItem(itemKey);
item.apiObj.deleted = 0;
return item.writeItem();
};
Library.prototype.deleteItem = function (itemKey) {
log.debug('Zotero.Library.deleteItem', 3);
var library = this;
return library.items.deleteItem(itemKey);
};
Library.prototype.deleteItems = function (itemKeys) {
log.debug('Zotero.Library.deleteItems', 3);
var library = this;
return library.items.deleteItems(itemKeys);
};
Library.prototype.addNote = function (itemKey, note) {
log.debug('Zotero.Library.prototype.addNote', 3);
var library = this;
var config = {
'target': 'children',
'libraryType': library.libraryType,
'libraryID': library.libraryID,
'itemKey': itemKey
};
var item = this.items.getItem(itemKey);
return library.ajaxRequest(config, 'POST', { processData: false });
};
Library.prototype.fetchGlobalItems = function (config) {
log.debug('Zotero.Library.fetchGlobalItems', 3);
var library = this;
if (!config) {
config = {};
}
var defaultConfig = {
target: 'items',
start: 0,
limit: 25
};
//Build config object that should be displayed next and compare to currently displayed
var newConfig = Z.extend({}, defaultConfig, config);
var urlconfig = Z.extend({ 'target': 'items', 'libraryType': '' }, newConfig);
return library.ajaxRequest(urlconfig, 'GET', { dataType: 'json' }).then(function (response) {
log.debug('globalItems callback', 3);
return response.data;
});
};
Library.prototype.fetchGlobalItem = function (globalKey) {
log.debug('Zotero.Library.fetchGlobalItem', 3);
log.debug(globalKey, 3);
var library = this;
var defaultConfig = { target: 'item' };
//Build config object that should be displayed next and compare to currently displayed
var newConfig = Z.extend({}, defaultConfig);
var urlconfig = Z.extend({
'target': 'item',
'libraryType': '',
'itemKey': globalKey
}, newConfig);
return library.ajaxRequest(urlconfig, 'GET', { dataType: 'json' }).then(function (response) {
log.debug('globalItem callback', 3);
return response.data;
});
};
//TagFunctions
Library.prototype.fetchTags = function (config) {
log.debug('Zotero.Library.fetchTags', 3);
var library = this;
var defaultConfig = {
target: 'tags',
order: 'title',
sort: 'asc',
limit: 100
};
var newConfig = Z.extend({}, defaultConfig, config);
var urlconfig = Z.extend({
'target': 'tags',
'libraryType': this.libraryType,
'libraryID': this.libraryID
}, newConfig);
return Zotero.ajaxRequest(urlconfig);
};
Library.prototype.loadTags = function () {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
log.debug('Zotero.Library.loadTags', 3);
var library = this;
if (config.showAutomaticTags && config.collectionKey) {
delete config.collectionKey;
}
library.tags.displayTagsArray = [];
return library.fetchTags(config).then(function (response) {
log.debug('loadTags proxied callback', 3);
var updatedVersion = response.lastModifiedVersion;
library.tags.updateSyncState(updatedVersion);
var addedTags = library.tags.addTagsFromJson(response.data);
library.tags.updateTagsVersion(updatedVersion);
library.tags.rebuildTagsArray();
if (response.parsedLinks.hasOwnProperty('next')) {
library.tags.hasNextLink = true;
library.tags.nextLink = response.parsedLinks['next'];
} else {
library.tags.hasNextLink = false;
library.tags.nextLink = null;
}
library.trigger('tagsChanged', { library: library });
return library.tags;
});
};
Library.prototype.loadAllTags = function () {
var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
log.debug('Zotero.Library.loadAllTags', 3);
var library = this;
var defaultConfig = {
target: 'tags',
order: 'title',
sort: 'asc',
limit: 100,
libraryType: library.libraryType,
libraryID: library.libraryID
};
//Build config object that should be displayed next and compare to currently displayed
var newConfig = Z.extend({}, defaultConfig, config);
var urlconfig = Z.extend({}, newConfig);
//check if already loaded tags are okay to use
return new Promise(function (resolve, reject) {
var continueLoadingCallback = function continueLoadingCallback(tags) {
log.debug('loadAllTags continueLoadingCallback', 3);
var plainList = Zotero.Tags.prototype.plainTagsList(tags.tagsArray);
plainList.sort(Library.prototype.comparer());
tags.plainList = plainList;
if (tags.hasNextLink) {
log.debug('still has next link.', 3);
tags.tagsArray.sort(Zotero.Tag.prototype.tagComparer());
plainList = Zotero.Tags.prototype.plainTagsList(tags.tagsArray);
plainList.sort(Library.prototype.comparer());
tags.plainList = plainList;
var nextLink = tags.nextLink;
var nextLinkConfig = Zotero.utils.parseQuery(Zotero.utils.querystring(nextLink));
var newConfig = Z.extend({}, config);
newConfig.start = nextLinkConfig.start;
newConfig.limit = nextLinkConfig.limit;
return library.loadTags(newConfig).then(continueLoadingCallback);
} else {
log.debug('no next in tags link', 3);
tags.updateSyncedVersion();
tags.tagsArray.sort(Zotero.Tag.prototype.tagComparer());
plainList = Zotero.Tags.prototype.plainTagsList(tags.tagsArray);
plainList.sort(Library.prototype.comparer());
tags.plainList = plainList;
log.debug('resolving loadTags deferred', 3);
library.tagsLoaded = true;
library.tags.loaded = true;
tags.loadedConfig = config;
//update all tags with tagsVersion
for (var i = 0; i < library.tags.tagsArray.length; i++) {
tags.tagsArray[i].apiObj.version = tags.tagsVersion;
}
library.trigger('tagsChanged', { library: library });
return tags;
}
};
resolve(library.loadTags(urlconfig).then(continueLoadingCallback));
});
};
//LibraryCache
//load objects from indexedDB
Library.prototype.loadIndexedDBCache = function () {
log.debug('Zotero.Library.loadIndexedDBCache', 3);
var library = this;
var itemsPromise = library.idbLibrary.getAllItems();
var collectionsPromise = library.idbLibrary.getAllCollections();
var tagsPromise = library.idbLibrary.getAllTags();
itemsPromise.then(function (itemsArray) {
log.debug('loadIndexedDBCache itemsD done', 3);
//create itemsDump from array of item objects
var latestItemVersion = 0;
for (var i = 0; i < itemsArray.length; i++) {
var item = new Zotero.Item(itemsArray[i]);
library.items.addItem(item);
if (item.version > latestItemVersion) {
latestItemVersion = item.version;
}
}
library.items.itemsVersion = latestItemVersion;
//TODO: add itemsVersion as last version in any of these items?
//or store it somewhere else for indexedDB cache purposes
library.items.loaded = true;
log.debug('Done loading indexedDB items promise into library', 3);
});
collectionsPromise.then(function (collectionsArray) {
log.debug('loadIndexedDBCache collectionsD done', 3);
//create collectionsDump from array of collection objects
var latestCollectionVersion = 0;
for (var i = 0; i < collectionsArray.length; i++) {
var collection = new Zotero.Collection(collectionsArray[i]);
library.collections.addCollection(collection);
if (collection.version > latestCollectionVersion) {
latestCollectionVersion = collection.version;
}
}
library.collections.collectionsVersion = latestCollectionVersion;
//TODO: add collectionsVersion as last version in any of these items?
//or store it somewhere else for indexedDB cache purposes
library.collections.initSecondaryData();
library.collections.loaded = true;
});
tagsPromise.then(function (tagsArray) {
log.debug('loadIndexedDBCache tagsD done', 3);
log.debug(tagsArray, 4);
//create tagsDump from array of tag objects
var latestVersion = 0;
var tagsVersion = 0;
for (var i = 0; i < tagsArray.length; i++) {
var tag = new Zotero.Tag(tagsArray[i]);
library.tags.addTag(tag);
if (tagsArray[i].version > latestVersion) {
latestVersion = tagsArray[i].version;
}
}
tagsVersion = latestVersion;
library.tags.tagsVersion = tagsVersion;
//TODO: add tagsVersion as last version in any of these items?
//or store it somewhere else for indexedDB cache purposes
library.tags.loaded = true;
});
//resolve the overall deferred when all the child deferreds are finished
return Promise.all([itemsPromise, collectionsPromise, tagsPromise]);
};
Library.prototype.saveIndexedDB = function () {
var library = this;
var saveItemsPromise = library.idbLibrary.updateItems(library.items.itemsArray);
var saveCollectionsPromise = library.idbLibrary.updateCollections(library.collections.collectionsArray);
var saveTagsPromise = library.idbLibrary.updateTags(library.tags.tagsArray);
//resolve the overall deferred when all the child deferreds are finished
return Promise.all([saveItemsPromise, saveCollectionsPromise, saveTagsPromise]);
};
module.exports = Library;