Update zotero:// extensions (report, timeline, etc.) for async DB, and more

- Protocol handler extensions can now handle promises and can also make
  data available as it's ready instead of all at once (e.g., reports now
  output one entry at a time)
- zotero:// URL syntaxes are now more consistent and closer to the web
  API (old URLs should work, but some may currently be broken)

Also:

- Code to generate server API, currently available for testing via
  zotero://data URLs but eventually moving to HTTP -- zotero://data URLs match
  web API URLs, with a different prefix for the personal library (/library vs.
  /users/12345)
- Miscellaneous fixes to data objects

Under the hood:

- Extensions now return an AsyncChannel, which is an nsIChannel implementation
  that takes a promise-yielding generator that returns a string,
  nsIAsyncInputStream, or file that will be used for the channel's data
- New function Zotero.Utilities.Internal.getAsyncInputStream() takes a
  generator that yields either promises or strings and returns an async input
  stream filled with the yielded strings
- Zotero.Router parsers URLs and extract parameters
- Zotero.Item.toResponseJSON()
This commit is contained in:
Dan Stillman 2014-09-08 16:51:05 -04:00
parent c5ee3651fe
commit 755ead2119
26 changed files with 1731 additions and 900 deletions

View file

@ -44,7 +44,11 @@ var ZoteroAdvancedSearch = new function() {
_searchBox.onLibraryChange = this.onLibraryChange;
var io = window.arguments[0];
_searchBox.search = io.dataIn.search;
io.dataIn.search.loadPrimaryData()
.then(function () {
_searchBox.search = io.dataIn.search;
});
}
@ -62,6 +66,7 @@ var ZoteroAdvancedSearch = new function() {
// Hack to create a condition for the search's library --
// this logic should really go in the search itself instead of here
// and in collectionTreeView.js
yield search.loadPrimaryData();
var conditions = search.getSearchConditions();
if (!conditions.some(function (condition) condition.condition == 'libraryID')) {
yield search.addCondition('libraryID', 'is', _searchBox.search.libraryID);

View file

@ -176,8 +176,9 @@
if (this.onLibraryChange) {
this.onLibraryChange(libraryID);
}
this.searchRef.libraryID = libraryID;
if (!this.searchRef.id) {
this.searchRef.libraryID = libraryID;
}
]]></body>
</method>

View file

@ -25,47 +25,45 @@
var Zotero_Report_Interface = new function() {
this.loadCollectionReport = loadCollectionReport;
this.loadItemReport = loadItemReport;
this.loadItemReportByIds = loadItemReportByIds;
/*
* Load a report for the currently selected collection
*/
function loadCollectionReport(event) {
var queryString = '';
var col = ZoteroPane_Local.getSelectedCollection();
this.loadCollectionReport = function (event) {
var sortColumn = ZoteroPane_Local.getSortField();
var sortDirection = ZoteroPane_Local.getSortDirection();
if (sortColumn != 'title' || sortDirection != 'ascending') {
queryString = '?sort=' + sortColumn + (sortDirection == 'ascending' ? '' : '/d');
var queryString = '?sort=' + sortColumn
+ '&direction=' + (sortDirection == 'ascending' ? 'asc' : 'desc');
var url = 'zotero://report/';
var source = ZoteroPane_Local.getSelectedCollection();
if (!source) {
source = ZoteroPane_Local.getSelectedSavedSearch();
}
if (!source) {
throw new Error('No collection currently selected');
}
if (col) {
ZoteroPane_Local.loadURI('zotero://report/collection/'
+ Zotero.Collections.getLibraryKeyHash(col)
+ '/html/report.html' + queryString, event);
return;
url += Zotero.API.getLibraryPrefix(source.libraryID) + '/';
if (source instanceof Zotero.Collection) {
url += 'collections/' + source.key;
}
else {
url += 'searches/' + source.key;
}
var s = ZoteroPane_Local.getSelectedSavedSearch();
if (s) {
ZoteroPane_Local.loadURI('zotero://report/search/'
+ Zotero.Searches.getLibraryKeyHash(s)
+ '/html/report.html' + queryString, event);
return;
}
url += '/items/report.html' + queryString;
throw ('No collection currently selected');
ZoteroPane_Local.loadURI(url, event);
}
/*
* Load a report for the currently selected items
*/
function loadItemReport(event) {
this.loadItemReport = function (event) {
var libraryID = ZoteroPane_Local.getSelectedLibraryID();
var items = ZoteroPane_Local.getSelectedItems();
if (!items || !items.length) {
@ -77,18 +75,8 @@ var Zotero_Report_Interface = new function() {
keyHashes.push(Zotero.Items.getLibraryKeyHash(item));
}
ZoteroPane_Local.loadURI('zotero://report/items/' + keyHashes.join('-') + '/html/report.html', event);
}
/*
* Load a report for the specified items
*/
function loadItemReportByIds(ids) {
if (!ids || !ids.length) {
throw ('No itemIDs provided to loadItemReportByIds()');
}
ZoteroPane_Local.loadURI('zotero://report/items/' + ids.join('-') + '/html/report.html');
var url = 'zotero://report/' + Zotero.API.getLibraryPrefix(libraryID) + '/items/report.html'
+ '?itemKey=' + items.map(item => item.key).join(',');
ZoteroPane_Local.loadURI(url, event);
}
}

View file

@ -30,25 +30,23 @@ var Zotero_Timeline_Interface = new function() {
*/
this.loadTimeline = function () {
var uri = 'zotero://timeline/';
var col = ZoteroPane_Local.getSelectedCollection();
if (col) {
ZoteroPane_Local.loadURI(uri + 'collection/' + Zotero.Collections.getLibraryKeyHash(col));
return;
uri += Zotero.API.getLibraryPrefix(col.libraryID) + '/collections/' + col.key;
}
var s = ZoteroPane_Local.getSelectedSavedSearch();
if (s) {
ZoteroPane_Local.loadURI(uri + 'search/' + Zotero.Searches.getLibraryKeyHash(s));
return;
else {
var s = ZoteroPane_Local.getSelectedSavedSearch();
if (s) {
uri += Zotero.API.getLibraryPrefix(s.libraryID) + '/searches/' + s.key;
}
else {
let libraryID = ZoteroPane_Local.getSelectedLibraryID();
if (libraryID) {
uri += Zotero.API.getLibraryPrefix(libraryID);
}
}
}
var l = ZoteroPane_Local.getSelectedLibraryID();
if (l) {
ZoteroPane_Local.loadURI(uri + 'library/' + l);
return;
}
ZoteroPane_Local.loadURI(uri);
}
}

View file

@ -0,0 +1,191 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2014 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.API = {
parseParams: function (params) {
if (params.groupID) {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID);
}
if (typeof params.itemKey == 'string') {
params.itemKey = params.itemKey.split(',');
}
},
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 (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);
}
return results;
}),
getLibraryPrefix: function (libraryID) {
return libraryID
? 'groups/' + Zotero.Groups.getGroupIDFromLibraryID(libraryID)
: 'library';
}
};
Zotero.API.Data = {
/**
* Parse a relative URI path and return parameters for the request
*/
parsePath: function (path) {
var params = {};
var router = new Zotero.Router(params);
// Top-level objects
router.add('library/:controller/top', function () {
params.libraryID = 0;
params.subset = 'top';
});
router.add('groups/:groupID/:controller/top', function () {
params.subset = 'top';
});
router.add('library/:scopeObject/:scopeObjectKey/items/:objectKey/:subset', function () {
params.libraryID = 0;
params.controller = 'items';
});
router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/:objectKey/:subset', function () {
params.controller = 'items';
});
// All objects
router.add('library/:controller', function () {
params.libraryID = 0;
});
router.add('groups/:groupID/:controller', function () {});
var parsed = router.run(path);
if (!parsed || !params.controller) {
throw new Zotero.Router.InvalidPathException(path);
}
if (params.groupID) {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID);
}
Zotero.Router.Utilities.convertControllerToObjectType(params);
return params;
},
getGenerator: function (path) {
var params = this.parsePath(path);
//Zotero.debug(params);
return Zotero.DataObjectUtilities.getClassForObjectType(params.objectType)
.apiDataGenerator(params);
}
};

View file

@ -121,8 +121,7 @@ Zotero.Collection.prototype.loadPrimaryData = Zotero.Promise.coroutine(function*
var key = this._key;
var libraryID = this._libraryID;
// Should be same as query in Zotero.Collections, just with collectionID
var sql = Zotero.Collections._getPrimaryDataSQL();
var sql = Zotero.Collections.getPrimaryDataSQL();
if (id) {
sql += " AND O.collectionID=?";
var params = id;

View file

@ -204,7 +204,7 @@ Zotero.Collections = new function() {
}
this._getPrimaryDataSQL = function () {
this.getPrimaryDataSQL = function () {
// This should be the same as the query in Zotero.Collection.load(),
// just without a specific collectionID
return "SELECT "

View file

@ -53,6 +53,12 @@ Zotero.DataObjectUtilities = {
return key;
},
getObjectTypeSingular: function (objectTypePlural) {
return objectTypePlural.replace(/(s|es)$/, '');
},
"getObjectTypePlural": function getObjectTypePlural(objectType) {
return objectType == 'search' ? 'searches' : objectType + 's';
},

View file

@ -186,17 +186,38 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
});
/**
* @deprecated - use .libraryKey
*/
this.makeLibraryKeyHash = function (libraryID, key) {
Zotero.debug("WARNING: Zotero.DataObjects.makeLibraryKeyHash() is deprecated -- use obj.libraryKey instead");
return libraryID + '_' + key;
}
/**
* @deprecated - use .libraryKey
*/
this.getLibraryKeyHash = function (obj) {
Zotero.debug("WARNING: Zotero.DataObjects.getLibraryKeyHash() is deprecated -- use obj.libraryKey instead");
return this.makeLibraryKeyHash(obj.libraryID, obj.key);
}
this.parseLibraryKey = function (libraryKey) {
var [libraryID, key] = libraryKey.split('/');
return {
libraryID: parseInt(libraryID),
key: key
};
}
/**
* @deprecated - Use Zotero.DataObjects.parseLibraryKey()
*/
this.parseLibraryKeyHash = function (libraryKey) {
Zotero.debug("WARNING: Zotero.DataObjects.parseLibraryKeyHash() is deprecated -- use .parseLibraryKey() instead");
var [libraryID, key] = libraryKey.split('_');
if (!key) {
return false;
@ -254,7 +275,8 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this.getIDFromLibraryAndKey = function (libraryID, key) {
return this._objectIDs[libraryID][key] ? this._objectIDs[libraryID][key] : false;
return (this._objectIDs[libraryID] && this._objectIDs[libraryID][key])
? this._objectIDs[libraryID][key] : false;
}
@ -531,8 +553,8 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
return loaded;
}
// _getPrimaryDataSQL() should use "O" for the primary table alias
var sql = this._getPrimaryDataSQL();
// getPrimaryDataSQL() should use "O" for the primary table alias
var sql = this.getPrimaryDataSQL();
var params = [];
if (libraryID !== false) {
sql += ' AND O.libraryID=?';

View file

@ -293,7 +293,7 @@ Zotero.Item.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (relo
if (!columns.length) {
return;
}
// This should match Zotero.Items._getPrimaryDataSQL(), but without
// This should match Zotero.Items.getPrimaryDataSQL(), but without
// necessarily including all columns
var sql = "SELECT " + columns.join(", ") + Zotero.Items.primaryDataSQLFrom;
if (id) {
@ -923,9 +923,9 @@ Zotero.Item.prototype.getCreator = function (pos) {
* @param {Integer} pos
* @return {Object|Boolean} The API JSON creator data at the given position, or FALSE if none
*/
Zotero.Item.prototype.getCreatorsJSON = function (pos) {
Zotero.Item.prototype.getCreatorJSON = function (pos) {
this._requireData('creators');
return this._creators[pos] ? Zotero.Creators.internalToAPIJSON(this._creators[pos]) : false;
return this._creators[pos] ? Zotero.Creators.internalToJSON(this._creators[pos]) : false;
}
@ -954,7 +954,7 @@ Zotero.Item.prototype.getCreators = function () {
*/
Zotero.Item.prototype.getCreatorsAPIData = function () {
this._requireData('creators');
return this._creators.map(function (data) Zotero.Creators.internalToAPIJSON(data));
return this._creators.map(function (data) Zotero.Creators.internalToJSON(data));
}
@ -2075,7 +2075,7 @@ Zotero.Item.prototype.numAttachments = function(includeTrashed) {
/**
* Get an nsILocalFile for the attachment, or false for invalid paths
*
* This no longer checks whether a file exists
* Note: This no longer checks whether a file exists
*
* @return {nsILocalFile|false} An nsIFile, or false for invalid paths
*/
@ -4150,12 +4150,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
}
var obj = {};
if (options && options.includeKey) {
obj.itemKey = this.key;
}
if (options && options.includeVersion) {
obj.itemVersion = this.version;
}
obj.itemKey = this.key;
obj.itemVersion = this.version;
obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
// Fields
@ -4170,7 +4166,7 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
// Creators
if (this.isRegularItem()) {
yield this.loadCreators()
obj.creators = this.getCreators();
obj.creators = this.getCreatorsAPIData();
}
else {
var parent = this.parentKey;
@ -4200,14 +4196,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
obj.tags = [];
yield this.loadTags()
var tags = yield this.getTags();
for each (let tag in tags) {
let tagObj = {};
tagObj.tag = tag.name;
let type = tag.type;
if (type != 0 || mode == 'full') {
tagObj.type = tag.type;
}
obj.tags.push(tagObj);
for (let i=0; i<tags.length; i++) {
obj.tags.push(tags[i]);
}
// Collections
@ -4248,10 +4238,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
obj.deleted = deleted;
}
if (options && options.includeDate) {
obj.dateAdded = this.dateAdded;
obj.dateModified = this.dateModified;
}
obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded);
obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified);
if (mode == 'patch') {
for (let i in patchBase) {
@ -4277,6 +4265,34 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
});
Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options, patchBase) {
var json = {
key: this.key,
version: this.version,
meta: {},
data: yield this.toJSON(options, patchBase)
};
// TODO: library block?
// creatorSummary
var firstCreator = this.getField('firstCreator');
if (firstCreator) {
json.meta.creatorSummary = firstCreator;
}
// parsedDate
var parsedDate = Zotero.Date.multipartToSQL(this.getField('date', true, true));
if (parsedDate) {
// 0000?
json.meta.parsedDate = parsedDate;
}
// numChildren
if (this.isRegularItem()) {
json.meta.numChildren = this.numChildren();
}
return json;
})
//////////////////////////////////////////////////////////////////////////////
//

View file

@ -154,6 +154,67 @@ Zotero.Items = new function() {
});
/**
* Return item data in web API format
*
* var data = Zotero.Items.getAPIData(0, 'collections/NF3GJ38A/items');
*
* @param {Number} libraryID
* @param {String} [apiPath='items'] - Web API style
* @return {Promise<String>}.
*/
this.getAPIData = Zotero.Promise.coroutine(function* (libraryID, apiPath) {
var gen = this.getAPIDataGenerator(...arguments);
var data = "";
while (true) {
var result = gen.next();
if (result.done) {
break;
}
var val = yield result.value;
if (typeof val == 'string') {
data += val;
}
else if (val === undefined) {
continue;
}
else {
throw new Error("Invalid return value from generator");
}
}
return data;
});
/**
* Zotero.Utilities.Internal.getAsyncInputStream-compatible generator that yields item data
* in web API format as strings
*
* @param {Object} params - Request parameters from Zotero.API.parsePath()
*/
this.apiDataGenerator = function* (params) {
Zotero.debug(params);
var s = new Zotero.Search;
yield s.addCondition('libraryID', 'is', params.libraryID);
if (params.scopeObject == 'collections') {
yield s.addCondition('collection', 'is', params.libraryID + '/' + params.scopeObjectKey);
}
yield s.addCondition('title', 'contains', 'test');
var ids = yield s.search();
yield '[\n';
for (let i=0; i<ids.length; i++) {
let prefix = i > 0 ? ',\n' : '';
let item = yield this.getAsync(ids[i], { noCache: true });
var json = yield item.toResponseJSON();
yield prefix + JSON.stringify(json, null, 4);
}
yield '\n]';
};
/*
* Create a new item with optional metadata and pass back the primary reference
*
@ -621,8 +682,7 @@ Zotero.Items = new function() {
});
this._getPrimaryDataSQL = function () {
// This should match Zotero.Item.loadPrimaryData, but with all possible columns
this.getPrimaryDataSQL = function () {
return "SELECT "
+ Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ')
+ this.primaryDataSQLFrom;

View file

@ -845,7 +845,7 @@ Zotero.Tags = new function() {
}
this._getPrimaryDataSQL = function () {
this.getPrimaryDataSQL = function () {
// This should be the same as the query in Zotero.Tag.load(),
// just without a specific tagID
return "SELECT * FROM tags O WHERE 1";

View file

@ -544,6 +544,29 @@ Zotero.Date = new function(){
return false;
}
this.sqlToISO8601 = function (sqlDate) {
var date = sqlDate.substr(0, 10);
var matches = date.match(/^([0-9]{4})\-([0-9]{2})\-([0-9]{2})/);
if (!matches) {
return false;
}
date = matches[1];
// Drop parts for reduced precision
if (matches[2] !== "00") {
date += "-" + matches[2];
if (matches[3] !== "00") {
date += "-" + matches[3];
}
}
var time = sqlDate.substr(11);
// TODO: validate times
if (time) {
date += "T" + time + "Z";
}
return date;
}
function strToMultipart(str){
if (!str){
return '';

View file

@ -189,7 +189,7 @@ Zotero.File = new function(){
* @param {nsIURI|nsIFile|string spec|string path|nsIChannel|nsIInputStream} source The source to read
* @param {String} [charset] The character set; defaults to UTF-8
* @param {Integer} [maxLength] Maximum length to fetch, in bytes
* @return {Promise} A Q promise that is resolved with the contents of the file
* @return {Promise} A promise that is resolved with the contents of the file
*/
this.getContentsAsync = function (source, charset, maxLength) {
Zotero.debug("Getting contents of " + source);
@ -243,7 +243,7 @@ Zotero.File = new function(){
*
* @param {nsIURI|nsIFile|string spec|nsIChannel|nsIInputStream} source The source to read
* @param {Integer} [maxLength] Maximum length to fetch, in bytes (unimplemented)
* @return {Promise} A Q promise that is resolved with the contents of the source
* @return {Promise} A promise that is resolved with the contents of the source
*/
this.getBinaryContentsAsync = function (source, maxLength) {
var deferred = Zotero.Promise.defer();

View file

@ -24,57 +24,54 @@
*/
Zotero.Report = new function() {
this.generateHTMLDetails = generateHTMLDetails;
this.generateHTMLList = generateHTMLList;
var escapeXML = function (str) {
str = str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A');
return Zotero.Utilities.htmlSpecialChars(str);
}
function generateHTMLDetails(items, combineChildItems) {
var content = '<!DOCTYPE html>\n';
content += '<html>\n';
content += '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n';
content += '<title>' + Zotero.getString('report.title.default') + '</title>\n';
content += '<link rel="stylesheet" type="text/css" href="zotero://report/detail.css"/>\n';
content += '<link rel="stylesheet" type="text/css" media="screen,projection" href="zotero://report/detail_screen.css"/>\n';
content += '<link rel="stylesheet" type="text/css" media="print" href="zotero://report/detail_print.css"/>\n';
content += '</head>\n\n<body>\n';
Zotero.Report = {};
Zotero.Report.HTML = new function () {
this.listGenerator = function* (items, combineChildItems) {
yield '<!DOCTYPE html>\n'
+ '<html>\n'
+ ' <head>\n'
+ ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n'
+ ' <title>' + Zotero.getString('report.title.default') + '</title>\n'
+ ' <link rel="stylesheet" type="text/css" href="zotero://report/detail.css"/>\n'
+ ' <link rel="stylesheet" type="text/css" media="screen,projection" href="zotero://report/detail_screen.css"/>\n'
+ ' <link rel="stylesheet" type="text/css" media="print" href="zotero://report/detail_print.css"/>\n'
+ ' </head>\n'
+ ' <body>\n'
+ ' <ul class="report' + (combineChildItems ? ' combineChildItems' : '') + '">';
content += '<ul class="report' + (combineChildItems ? ' combineChildItems' : '') + '">\n';
for each(var arr in items) {
content += '\n<li id="i' + arr.itemID + '" class="item ' + arr.itemType + '">\n';
for (let i=0; i<items.length; i++) {
let obj = items[i];
if (arr.title) {
let content = '\n\t\t\t<li id="item_' + obj.itemKey + '" class="item ' + obj.itemType + '">\n';
if (obj.title) {
// Top-level item matched search, so display title
if (arr.reportSearchMatch) {
content += '<h2>' + escapeXML(arr.title) + '</h2>\n';
if (obj.reportSearchMatch) {
content += '\t\t\t<h2>' + escapeXML(obj.title) + '</h2>\n';
}
// Non-matching parent, so display "Parent Item: [Title]"
else {
content += '<h2 class="parentItem">' + escapeXML(Zotero.getString('report.parentItem'))
+ ' <span class="title">' + escapeXML(arr.title) + '</span></h2>';
content += '\t\t\t<h2 class="parentItem">' + escapeXML(Zotero.getString('report.parentItem'))
+ ' <span class="title">' + escapeXML(obj.title) + '</span></h2>\n';
}
}
// If parent matches search, display parent item metadata table and tags
if (arr.reportSearchMatch) {
content += _generateMetadataTable(arr);
if (obj.reportSearchMatch) {
content += _generateMetadataTable(obj);
content += _generateTagsList(arr);
content += _generateTagsList(obj);
// Independent note
if (arr['note']) {
content += '\n';
if (obj['note']) {
content += '\n\t\t\t';
// If not valid XML, display notes with entities encoded
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser);
var doc = parser.parseFromString('<div>'
+ arr.note
+ obj.note
// &nbsp; isn't valid in HTML
.replace(/&nbsp;/g, "&#160;")
// Strip control characters (for notes that were
@ -83,26 +80,26 @@ Zotero.Report = new function() {
+ '</div>', "application/xml");
if (doc.documentElement.tagName == 'parsererror') {
Zotero.debug(doc.documentElement.textContent, 2);
content += '<p class="plaintext">' + escapeXML(arr.note) + '</p>\n';
content += '<p class="plaintext">' + escapeXML(obj.note) + '</p>\n';
}
// Otherwise render markup normally
else {
content += arr.note + '\n';
content += obj.note + '\n';
}
}
}
// Children
if (arr.reportChildren) {
if (obj.reportChildren) {
// Child notes
if (arr.reportChildren.notes.length) {
if (obj.reportChildren.notes.length) {
// Only display "Notes:" header if parent matches search
if (arr.reportSearchMatch) {
content += '<h3 class="notes">' + escapeXML(Zotero.getString('report.notes')) + '</h3>\n';
if (obj.reportSearchMatch) {
content += '\t\t\t\t<h3 class="notes">' + escapeXML(Zotero.getString('report.notes')) + '</h3>\n';
}
content += '<ul class="notes">\n';
for each(var note in arr.reportChildren.notes) {
content += '<li id="i' + note.itemID + '">\n';
content += '\t\t\t\t<ul class="notes">\n';
for each(var note in obj.reportChildren.notes) {
content += '\t\t\t\t\t<li id="item_' + note.itemKey + '">\n';
// If not valid XML, display notes with entities encoded
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
@ -126,114 +123,117 @@ Zotero.Report = new function() {
// Child note tags
content += _generateTagsList(note);
content += '</li>\n';
content += '\t\t\t\t\t</li>\n';
}
content += '</ul>\n';
content += '\t\t\t\t</ul>\n';
}
// Chid attachments
content += _generateAttachmentsList(arr.reportChildren);
content += _generateAttachmentsList(obj.reportChildren);
}
// Related
if (arr.reportSearchMatch && arr.related && arr.related.length) {
content += '<h3 class="related">' + escapeXML(Zotero.getString('itemFields.related')) + '</h3>\n';
content += '<ul class="related">\n';
var relateds = Zotero.Items.get(arr.related);
for each(var related in relateds) {
content += '<li id="i' + related.getID() + '">';
content += escapeXML(related.getDisplayTitle());
content += '</li>\n';
// Related items
if (obj.reportSearchMatch && Zotero.Relations.relatedItemPredicate in obj.relations) {
content += '\t\t\t\t<h3 class="related">' + escapeXML(Zotero.getString('itemFields.related')) + '</h3>\n';
content += '\t\t\t\t<ul class="related">\n';
var rels = obj.relations[Zotero.Relations.relatedItemPredicate];
// TEMP
if (!Array.isArray(rels)) {
rels = [rels];
}
content += '</ul>\n';
for (let i=0; i<rels.length; i++) {
let rel = rels[i];
let relItem = Zotero.URI.getURIItem(rel);
if (relItem) {
content += '\t\t\t\t\t<li id="item_' + relItem.key + '">';
content += escapeXML(relItem.getDisplayTitle());
content += '</li>\n';
}
}
content += '\t\t\t\t</ul>\n';
}
content += '</li>\n\n';
content += '\t\t\t</li>\n\n';
yield content;
}
content += '</ul>\n';
content += '</body>\n</html>';
return content;
}
yield '\t\t</ul>\n\t</body>\n</html>';
};
function generateHTMLList(items) {
}
function _generateMetadataTable(arr) {
function _generateMetadataTable(obj) {
var table = false;
var content = '<table>\n';
var content = '\t\t\t\t<table>\n';
// Item type
content += '<tr>\n';
content += '<th>'
content += '\t\t\t\t\t<tr>\n';
content += '\t\t\t\t\t\t<th>'
+ escapeXML(Zotero.getString('itemFields.itemType'))
+ '</th>\n';
content += '<td>' + escapeXML(Zotero.ItemTypes.getLocalizedString(arr.itemType)) + '</td>\n';
content += '</tr>\n';
content += '\t\t\t\t\t\t<td>' + escapeXML(Zotero.ItemTypes.getLocalizedString(obj.itemType)) + '</td>\n';
content += '\t\t\t\t\t</tr>\n';
// Creators
if (arr['creators']) {
if (obj['creators']) {
table = true;
var displayText;
for each(var creator in arr['creators']) {
// Two fields
if (creator['fieldMode']==0) {
displayText = creator['firstName'] + ' ' + creator['lastName'];
}
// Single field
else if (creator['fieldMode']==1) {
displayText = creator['lastName'];
for each(var creator in obj['creators']) {
// One field
if (creator.name !== undefined) {
displayText = creator.name;
}
// Two field
else {
// TODO
displayText = (creator.firstName + ' ' + creator.lastName).trim();
}
content += '<tr>\n';
content += '<th class="' + creator.creatorType + '">'
content += '\t\t\t\t\t<tr>\n';
content += '\t\t\t\t\t\t<th class="' + creator.creatorType + '">'
+ escapeXML(Zotero.getString('creatorTypes.' + creator.creatorType))
+ '</th>\n';
content += '<td>' + escapeXML(displayText) + '</td>\n';
content += '</tr>\n';
content += '\t\t\t\t\t\t<td>' + escapeXML(displayText) + '</td>\n';
content += '\t\t\t\t\t</tr>\n';
}
}
// Move dateAdded and dateModified to the end of the array
var da = arr['dateAdded'];
var dm = arr['dateModified'];
delete arr['dateAdded'];
delete arr['dateModified'];
arr['dateAdded'] = da;
arr['dateModified'] = dm;
// Move dateAdded and dateModified to the end of the objay
var da = obj['dateAdded'];
var dm = obj['dateModified'];
delete obj['dateAdded'];
delete obj['dateModified'];
obj['dateAdded'] = da;
obj['dateModified'] = dm;
for (var i in arr) {
for (var i in obj) {
// Skip certain fields
switch (i) {
case 'reportSearchMatch':
case 'reportChildren':
case 'libraryID':
case 'key':
case 'itemKey':
case 'itemVersion':
case 'itemType':
case 'itemID':
case 'parentItemID':
case 'title':
case 'firstCreator':
case 'creators':
case 'tags':
case 'related':
case 'notes':
case 'note':
case 'attachments':
case 'collections':
case 'relations':
case 'tags':
case 'deleted':
case 'parentItem':
case 'charset':
case 'contentType':
case 'linkMode':
case 'path':
continue;
}
try {
var localizedFieldName = Zotero.ItemFields.getLocalizedString(arr.itemType, i);
var localizedFieldName = Zotero.ItemFields.getLocalizedString(obj.itemType, i);
}
// Skip fields we don't have a localized string for
catch (e) {
@ -241,79 +241,82 @@ Zotero.Report = new function() {
continue;
}
arr[i] = Zotero.Utilities.trim(arr[i] + '');
obj[i] = (obj[i] + '').trim();
// Skip empty fields
if (!arr[i]) {
if (!obj[i]) {
continue;
}
table = true;
var fieldText;
if (i == 'url' && arr[i].match(/^https?:\/\//)) {
fieldText = '<a href="' + escapeXML(arr[i]) + '">'
+ escapeXML(arr[i]) + '</a>';
if (i == 'url' && obj[i].match(/^https?:\/\//)) {
fieldText = '<a href="' + escapeXML(obj[i]) + '">' + escapeXML(obj[i]) + '</a>';
}
// Hyperlink DOI
else if (i == 'DOI') {
fieldText = '<a href="' + escapeXML('http://doi.org/' + arr[i]) + '">'
+ escapeXML(arr[i]) + '</a>';
fieldText = '<a href="' + escapeXML('http://doi.org/' + obj[i]) + '">'
+ escapeXML(obj[i]) + '</a>';
}
// Remove SQL date from multipart dates
// (e.g. '2006-00-00 Summer 2006' becomes 'Summer 2006')
else if (i=='date') {
fieldText = escapeXML(Zotero.Date.multipartToStr(arr[i]));
fieldText = escapeXML(Zotero.Date.multipartToStr(obj[i]));
}
// Convert dates to local format
else if (i=='accessDate' || i=='dateAdded' || i=='dateModified') {
var date = Zotero.Date.sqlToDate(arr[i], true)
var date = Zotero.Date.isoToDate(obj[i], true)
fieldText = escapeXML(date.toLocaleString());
}
else {
fieldText = escapeXML(arr[i]);
fieldText = escapeXML(obj[i]);
}
content += '<tr>\n<th>' + escapeXML(localizedFieldName)
+ '</th>\n<td>' + fieldText + '</td>\n</tr>\n';
content += '\t\t\t\t\t<tr>\n\t\t\t\t\t<th>' + escapeXML(localizedFieldName)
+ '</th>\n\t\t\t\t\t\t<td>' + fieldText + '</td>\n\t\t\t\t\t</tr>\n';
}
content += '</table>';
content += '\t\t\t\t</table>\n';
return table ? content : '';
}
function _generateTagsList(arr) {
function _generateTagsList(obj) {
var content = '';
if (arr['tags'] && arr['tags'].length) {
if (obj.tags && obj.tags.length) {
var str = Zotero.getString('report.tags');
content += '<h3 class="tags">' + escapeXML(str) + '</h3>\n';
content += '<ul class="tags">\n';
for each(var tag in arr.tags) {
content += '<li>' + escapeXML(tag.fields.name) + '</li>\n';
content += '\t\t\t\t<h3 class="tags">' + escapeXML(str) + '</h3>\n';
content += '\t\t\t\t<ul class="tags">\n';
for (let i=0; i<obj.tags.length; i++) {
content += '\t\t\t\t\t<li>' + escapeXML(obj.tags[i].tag) + '</li>\n';
}
content += '</ul>\n';
content += '\t\t\t\t</ul>\n';
}
return content;
}
function _generateAttachmentsList(arr) {
function _generateAttachmentsList(obj) {
var content = '';
if (arr.attachments && arr.attachments.length) {
content += '<h3 class="attachments">' + escapeXML(Zotero.getString('itemFields.attachments')) + '</h3>\n';
content += '<ul class="attachments">\n';
for each(var attachment in arr.attachments) {
content += '<li id="i' + attachment.itemID + '">';
content += escapeXML(attachment.title);
if (obj.attachments && obj.attachments.length) {
content += '\t\t\t\t<h3 class="attachments">' + escapeXML(Zotero.getString('itemFields.attachments')) + '</h3>\n';
content += '\t\t\t\t<ul class="attachments">\n';
for (let i=0; i<obj.attachments.length; i++) {
let attachment = obj.attachments[i];
content += '\t\t\t\t\t<li id="item_' + attachment.itemKey + '">';
if (attachment.title !== undefined) {
content += escapeXML(attachment.title);
}
// Attachment tags
content += _generateTagsList(attachment);
// Attachment note
if (attachment.note) {
content += '<div class="note">';
content += '\t\t\t\t\t\t<div class="note">';
if (attachment.note.substr(0, 1024).match(/<p[^>]*>/)) {
content += attachment.note + '\n';
}
@ -321,13 +324,19 @@ Zotero.Report = new function() {
else {
content += '<p class="plaintext">' + escapeXML(attachment.note) + '</p>\n';
}
content += '</div>';
content += '\t\t\t\t\t</div>';
}
content += '</li>\n';
content += '\t\t\t\t\t</li>\n';
}
content += '</ul>\n';
content += '\t\t\t\t</ul>\n';
}
return content;
}
var escapeXML = function (str) {
str = str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A');
return Zotero.Utilities.htmlSpecialChars(str);
}
}

View file

@ -0,0 +1,25 @@
Components.utils.import("resource://zotero/pathparser.js", Zotero);
Zotero.Router = Zotero.PathParser;
delete Zotero.PathParser;
Zotero.Router.Utilities = {
convertControllerToObjectType: function (params) {
if (params.controller !== undefined) {
params.objectType = Zotero.DataObjectUtilities.getObjectTypeSingular(params.controller);
delete params.controller;
}
}
};
Zotero.Router.InvalidPathException = function (path) {
this.path = path;
}
Zotero.Router.InvalidPathException.prototype = {
name: "InvalidPathException",
toString: function () {
return "Path '" + this.path + "' could not be parsed";
}
};

View file

@ -119,13 +119,17 @@ Zotero.Search.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (re
var libraryID = this._libraryID;
var desc = id ? id : libraryID + "/" + key;
var sql = "SELECT * FROM savedSearches WHERE ";
if (!id && !key) {
throw new Error('ID or key not set');
}
var sql = Zotero.Searches.getPrimaryDataSQL()
if (id) {
sql += "savedSearchID=?";
sql += " AND savedSearchID=?";
var params = id;
}
else {
sql += "key=? AND libraryID=?";
sql += " AND key=? AND libraryID=?";
var params = [key, libraryID];
}
var data = yield Zotero.DB.rowQueryAsync(sql, params);
@ -323,7 +327,7 @@ Zotero.Search.prototype.clone = Zotero.Promise.coroutine(function* (libraryID) {
Zotero.Search.prototype.addCondition = Zotero.Promise.coroutine(function* (condition, operator, value, required) {
yield this.loadPrimaryData();
this._requireData('conditions');
if (!Zotero.SearchConditions.hasOperator(condition, operator)){
throw ("Invalid operator '" + operator + "' for condition " + condition);
@ -483,7 +487,7 @@ Zotero.Search.prototype.removeCondition = Zotero.Promise.coroutine(function* (se
* for the given searchConditionID
*/
Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
this._requireData('primaryData');
this._requireData('conditions');
return this._conditions[searchConditionID];
}
@ -493,8 +497,7 @@ Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
* used in the search, indexed by searchConditionID
*/
Zotero.Search.prototype.getSearchConditions = function(){
this._requireData('primaryData');
this._requireData('conditions');
var conditions = [];
for (var id in this._conditions) {
var condition = this._conditions[id];
@ -512,7 +515,7 @@ Zotero.Search.prototype.getSearchConditions = function(){
Zotero.Search.prototype.hasPostSearchFilter = function() {
this._requireData('primaryData');
this._requireData('conditions');
for each(var i in this._conditions){
if (i.condition == 'fulltextContent'){
return true;
@ -531,9 +534,15 @@ Zotero.Search.prototype.hasPostSearchFilter = function() {
Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable) {
var tmpTable;
if (this._identified) {
yield this.loadConditions();
}
// Mark conditions as loaded
else {
this._requireData('conditions');
}
try {
yield this.loadPrimaryData();
if (!this._sql){
yield this._buildQuery();
}
@ -580,6 +589,11 @@ Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable
// Run a subsearch to define the superset of possible results
if (this._scope) {
if (this._scope._identified) {
yield this._scope.loadPrimaryData();
yield this._scope.loadConditions();
}
// If subsearch has post-search filter, run and insert ids into temp table
if (this._scope.hasPostSearchFilter()) {
var ids = yield this._scope.search();
@ -863,6 +877,7 @@ Zotero.Search.prototype.serialize = function() {
*/
Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () {
if (!this._sql) {
yield this.loadConditions();
yield this._buildQuery();
}
return this._sql;
@ -871,6 +886,7 @@ Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () {
Zotero.Search.prototype.getSQLParams = Zotero.Promise.coroutine(function* () {
if (!this._sql) {
yield this.loadConditions();
yield this._buildQuery();
}
return this._sqlParams;
@ -966,6 +982,8 @@ Zotero.Search.idsToTempTable = function (ids) {
* Build the SQL query for the search
*/
Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
this._requireData('conditions');
var sql = 'SELECT itemID FROM items';
var sqlParams = [];
// Separate ANY conditions for 'required' condition support
@ -1171,14 +1189,16 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
if (condition.value) {
var lkh = Zotero.Collections.parseLibraryKeyHash(condition.value);
if (lkh) {
col = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key);
col = yield Zotero.Collections.getByLibraryAndKeyAsync(lkh.libraryID, lkh.key);
}
}
if (!col) {
var msg = "Collection " + condition.value + " specified in saved search doesn't exist";
Zotero.debug(msg, 2);
Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js');
continue;
col = {
id: 0
};
}
var q = ['?'];
@ -1633,6 +1653,22 @@ Zotero.Searches = new function(){
Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']);
this.constructor.prototype = new Zotero.DataObjects();
Object.defineProperty(this, "_primaryDataSQLParts", {
get: function () {
return _primaryDataSQLParts ? _primaryDataSQLParts : (_primaryDataSQLParts = {
savedSearchID: "O.savedSearchID",
name: "O.savedSearchName",
libraryID: "O.libraryID",
key: "O.key",
version: "O.version",
synced: "O.synced"
});
}
});
var _primaryDataSQLParts;
this.init = Zotero.Promise.coroutine(function* () {
yield this.constructor.prototype.init.apply(this);
@ -1661,7 +1697,7 @@ Zotero.Searches = new function(){
});
var searches = [];
for (i=0; i<rows.length; i++) {
for (var i=0; i<rows.length; i++) {
let search = new Zotero.Search;
search.id = rows[i].id;
yield search.loadPrimaryData();
@ -1697,10 +1733,12 @@ Zotero.Searches = new function(){
});
this._getPrimaryDataSQL = function () {
this.getPrimaryDataSQL = function () {
// This should be the same as the query in Zotero.Search.loadPrimaryData(),
// just without a specific savedSearchID
return "SELECT O.* FROM savedSearches O WHERE 1";
return "SELECT "
+ Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " "
+ "FROM savedSearches O WHERE 1";
}
}
@ -1715,8 +1753,8 @@ Zotero.SearchConditions = new function(){
this.parseCondition = parseCondition;
var _initialized = false;
var _conditions = {};
var _standardConditions = [];
var _conditions;
var _standardConditions;
var self = this;
@ -2180,6 +2218,7 @@ Zotero.SearchConditions = new function(){
];
// Index conditions by name and aliases
_conditions = {};
for (var i in conditions) {
_conditions[conditions[i]['name']] = conditions[i];
if (conditions[i]['aliases']) {
@ -2271,6 +2310,10 @@ Zotero.SearchConditions = new function(){
function hasOperator(condition, operator){
var [condition, mode] = this.parseCondition(condition);
if (!_conditions) {
throw new Zotero.Exception.UnloadedDataException("Search conditions not yet loaded");
}
if (!_conditions[condition]){
throw ("Invalid condition '" + condition + "' in hasOperator()");
}

View file

@ -24,32 +24,28 @@
*/
Zotero.Timeline = new function () {
this.generateXMLDetails = generateXMLDetails;
this.generateXMLList = generateXMLList;
function generateXMLDetails(items, dateType) {
Zotero.Timeline = {
generateXMLDetails: function* (items, dateType) {
var escapeXML = Zotero.Utilities.htmlSpecialChars;
var content = '<data>\n';
for each(var item in items) {
yield '<data>\n';
for (let i=0; i<items.length; i++) {
let item = items[i];
yield item.loadItemData();
var date = item.getField(dateType, true, true);
if (date) {
var sqlDate = (dateType == 'date') ? Zotero.Date.multipartToSQL(date) : date;
let sqlDate = (dateType == 'date') ? Zotero.Date.multipartToSQL(date) : date;
sqlDate = sqlDate.replace("00-00", "01-01");
content += '<event start="' + Zotero.Date.sqlToDate(sqlDate) + '" ';
var title = item.getField('title');
let content = '<event start="' + Zotero.Date.sqlToDate(sqlDate) + '" ';
let title = item.getField('title');
content += 'title=" ' + (title ? escapeXML(title) : '') + '" ';
content += 'icon="' + item.getImageSrc() + '" ';
content += 'color="black">';
content += item.id;
content += '</event>\n';
yield content;
}
}
content += '</data>';
return content;
yield '</data>';
}
function generateXMLList(items) {
}
}
};

View file

@ -0,0 +1,101 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2014 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.Users = new function () {
var _userID;
var _libraryID;
var _username;
var _localUserKey;
this.init = Zotero.Promise.coroutine(function* () {
var sql = "SELECT value FROM settings WHERE setting='account' AND key='userID'";
_userID = yield Zotero.DB.valueQueryAsync(sql);
sql = "SELECT value FROM settings WHERE setting='account' AND key='libraryID'";
_libraryID = yield Zotero.DB.valueQueryAsync(sql);
sql = "SELECT value FROM settings WHERE setting='account' AND key='username'";
_username = yield Zotero.DB.valueQueryAsync(sql);
// If we don't have a global user id, generate a local user key
if (!_userID) {
sql = "SELECT value FROM settings WHERE setting='account' AND key='localUserKey'";
let key = yield Zotero.DB.valueQueryAsync(sql);
// Generate a local user key if we don't have one
if (!key) {
key = Zotero.randomString(8);
sql = "INSERT INTO settings VALUES ('account', 'localUserKey', ?)";
yield Zotero.DB.queryAsync(sql, key);
}
_localUserKey = key;
}
});
this.getCurrentUserID = function () {
return _userID;
};
this.setCurrentUserID = Zotero.Promise.coroutine(function* (val) {
val = parseInt(val);
var sql = "REPLACE INTO settings VALUES ('account', 'userID', ?)";
Zotero.DB.queryAsync(sql, val);
_userID = val;
});
this.getCurrentLibraryID = function () {
return _libraryID;
};
this.setCurrentLibraryID = Zotero.Promise.coroutine(function* (val) {
val = parseInt(val);
var sql = "REPLACE INTO settings VALUES ('account', 'libraryID', ?)";
Zotero.DB.queryAsync(sql, val);
_userID = val;
});
this.getCurrentUsername = function () {
return _username;
};
this.setCurrentUsername = Zotero.Promise.coroutine(function* (val) {
var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
Zotero.DB.queryAsync(sql, val);
_userID = val;
});
this.getLocalUserKey = function () {
if (!_localUserKey) {
throw new Error("Local user key not available");
}
return _localUserKey;
};
};

View file

@ -368,6 +368,123 @@ Zotero.Utilities.Internal = {
}
},
/**
* Return an input stream that will be filled asynchronously with strings yielded from a
* generator. If the generator yields a promise, the promise is waited for, but its value
* is not added to the input stream.
*
* @param {GeneratorFunction|Generator} gen - Promise-returning generator function or
* generator
* @return {nsIAsyncInputStream}
*/
getAsyncInputStream: function (gen, onError) {
const funcName = 'getAsyncInputStream';
const maxOutOfSequenceSeconds = 10;
const outOfSequenceDelay = 50;
// Initialize generator if necessary
var g = gen.next ? gen : gen();
var seq = 0;
var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
pipe.init(true, true, 0, 0, null);
var os = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Components.interfaces.nsIConverterOutputStream);
os.init(pipe.outputStream, 'utf-8', 0, 0x0000);
pipe.outputStream.asyncWait({
onOutputStreamReady: function (aos) {
Zotero.debug("Output stream is ready");
let currentSeq = seq++;
Zotero.spawn(function* () {
var lastVal;
var error = false;
while (true) {
var data;
try {
let result = g.next(lastVal);
if (result.done) {
Zotero.debug("No more data to write");
aos.close();
return;
}
// If a promise is yielded, wait for it and pass on its value
if (result.value.then) {
lastVal = yield result.value;
continue;
}
// Otherwise use the return value
data = result.value;
break;
}
catch (e) {
Zotero.debug(e, 1);
if (onError) {
error = e;
data = onError();
break;
}
Zotero.debug("Closing input stream");
aos.close();
throw e;
}
}
if (typeof data != 'string') {
throw new Error("Yielded value is not a string or promise in " + funcName
+ " ('" + data + "')");
}
// Make sure that we're writing to the stream in order, in case
// onOutputStreamReady is called again before the last promise completes.
// If not in order, wait a bit and try again.
var maxTries = Math.floor(maxOutOfSequenceSeconds * 1000 / outOfSequenceDelay);
while (currentSeq != seq - 1) {
if (maxTries <= 0) {
throw new Error("Next promise took too long to finish in " + funcName);
}
Zotero.debug("Promise finished out of sequence in " + funcName
+ "-- waiting " + outOfSequenceDelay + " ms");
yield Zotero.Promise.delay(outOfSequenceDelay);
maxTries--;
}
// Write to stream
Zotero.debug("Writing " + data.length + " characters");
os.writeString(data);
if (error) {
Zotero.debug("Closing input stream");
aos.close();
throw error;
}
Zotero.debug("Waiting to write more");
// Wait until stream is ready for more
aos.asyncWait(this, 0, 0, null);
}, this)
.catch(function (e) {
Zotero.debug("Error getting data for async stream", 1);
Components.utils.reportError(e);
Zotero.debug(e, 1);
os.close();
});
}
}, 0, 0, null);
return pipe.inputStream;
},
/**
* Defines property on the object's prototype.
* More compact way to do Object.defineProperty

View file

@ -43,7 +43,6 @@ var ZoteroPane = new function()
this.handleKeyUp = handleKeyUp;
this.setHighlightedRowsCallback = setHighlightedRowsCallback;
this.handleKeyPress = handleKeyPress;
this.editSelectedCollection = editSelectedCollection;
this.handleSearchKeypress = handleSearchKeypress;
this.handleSearchInput = handleSearchInput;
this.getSelectedCollection = getSelectedCollection;
@ -1781,8 +1780,7 @@ var ZoteroPane = new function()
});
function editSelectedCollection()
{
this.editSelectedCollection = function () {
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
return;
@ -1807,11 +1805,17 @@ var ZoteroPane = new function()
else {
var s = new Zotero.Search();
s.id = row.ref.id;
var io = {dataIn: {search: s, name: row.getName()}, dataOut: null};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
if (io.dataOut) {
this.onCollectionSelected(); //reload itemsView
}
s.loadPrimaryData()
.then(function () {
return s.loadConditions();
})
.then(function () {
var io = {dataIn: {search: s, name: row.getName()}, dataOut: null};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
if (io.dataOut) {
this.onCollectionSelected(); //reload itemsView
}
}.bind(this));
}
}
}

View file

@ -69,7 +69,7 @@
Timeline.loadXML("zotero://timeline/data/", function(xml, url) { eventSource.loadXML(xml, url); });
setupFilterHighlightControls(document.getElementById("my-timeline-controls"), tl, [0,1,2], theme);
setupOtherControls(document.getElementById("my-other-controls"), tl, document.URL);
setupOtherControls(document.getElementById("my-other-controls"), tl, document.location.search);
}
function onResize() {

View file

@ -145,7 +145,7 @@ function checkDate(date) {
}
}
}
function changeBand(path, queryString, band, intervals, selectedIndex) {
function changeBand(queryString, band, intervals, selectedIndex) {
var values = new Array('d', 'm', 'y', 'e', 'c', 'i');
var newIntervals = '';
@ -158,7 +158,7 @@ function changeBand(path, queryString, band, intervals, selectedIndex) {
}
}
window.location = path + queryString + 'i=' + newIntervals;
window.location.search = queryString + 'i=' + newIntervals;
}
function createOption(t, selected) {
@ -192,7 +192,7 @@ function getFull(a) {
}
function createQueryString(theQueryValue, except, timeline) {
var temp = '?';
var temp = '';
for(var i in theQueryValue) {
if(except != i) {
temp += i + '=' + theQueryValue[i] + '&';
@ -208,16 +208,9 @@ function createQueryString(theQueryValue, except, timeline) {
return temp;
}
function setupOtherControls(div, timeline, url) {
function setupOtherControls(div, timeline, queryString) {
var table = document.createElement("table");
var [path, queryString] = url.split('?');
if(path == 'zotero://timeline') {
path += '/';
}
if(path =='zotero://timeline/') {
path += 'library';
}
var defaultQueryValue = new Object();
defaultQueryValue['i'] = 'mye';
defaultQueryValue['t'] = 'd';
@ -289,7 +282,7 @@ function setupOtherControls(div, timeline, url) {
select1.appendChild(createOption(options[i],(options[i] == selected)));
}
select1.onchange = function () {
changeBand(path, createQueryString(theQueryValue, 'i', timeline), 0, intervals, table.rows[1].cells[1].firstChild.selectedIndex);
changeBand(createQueryString(theQueryValue, 'i', timeline), 0, intervals, table.rows[1].cells[1].firstChild.selectedIndex);
};
td.appendChild(select1);
@ -301,7 +294,7 @@ function setupOtherControls(div, timeline, url) {
select2.appendChild(createOption(options[i],(options[i] == selected)));
}
select2.onchange = function () {
changeBand(path, createQueryString(theQueryValue, 'i', timeline), 1, intervals, table.rows[1].cells[2].firstChild.selectedIndex);
changeBand(createQueryString(theQueryValue, 'i', timeline), 1, intervals, table.rows[1].cells[2].firstChild.selectedIndex);
};
td.appendChild(select2);
@ -313,7 +306,7 @@ function setupOtherControls(div, timeline, url) {
select3.appendChild(createOption(options[i],(options[i] == selected)));
}
select3.onchange = function () {
changeBand(path, createQueryString(theQueryValue, 'i', timeline), 2, intervals, table.rows[1].cells[3].firstChild.selectedIndex);
changeBand(createQueryString(theQueryValue, 'i', timeline), 2, intervals, table.rows[1].cells[3].firstChild.selectedIndex);
};
td.appendChild(select3);
@ -327,7 +320,7 @@ function setupOtherControls(div, timeline, url) {
select4.appendChild(createOption(options[i],(values[i] == dateType)));
}
select4.onchange = function () {
window.location = path + createQueryString(theQueryValue, 't', timeline) + 't=' + values[table.rows[1].cells[4].firstChild.selectedIndex];
window.location.search = createQueryString(theQueryValue, 't', timeline) + 't=' + values[table.rows[1].cells[4].firstChild.selectedIndex];
};
td.appendChild(select4);
@ -335,7 +328,7 @@ function setupOtherControls(div, timeline, url) {
var fitToScreen = document.createElement("button");
fitToScreen.innerHTML = getString("general.fitToScreen");
Timeline.DOM.registerEvent(fitToScreen, "click", function () {
window.location = path + createQueryString(theQueryValue, false, timeline);
window.location.search = createQueryString(theQueryValue, false, timeline);
});
td.appendChild(fitToScreen);

File diff suppressed because it is too large Load diff

View file

@ -56,6 +56,7 @@ const xpcomFilesLocal = [
'libraryTreeView',
'collectionTreeView',
'annotate',
'api',
'attachments',
'cite',
'cookieSandbox',
@ -89,6 +90,7 @@ const xpcomFilesLocal = [
'proxy',
'quickCopy',
'report',
'router',
'schema',
'search',
'server',

119
resource/pathparser.js Normal file
View file

@ -0,0 +1,119 @@
/**
* pathparser.js - tiny URL parser/router
*
* Copyright (c) 2014 Dan Stillman
* License: MIT
* https://github.com/dstillman/pathparser.js
*/
(function (factory) {
// AMD/RequireJS
if (typeof define === 'function' && define.amd) {
define(factory);
// CommonJS/Node
} else if (typeof exports === 'object') {
module.exports = factory();
// Mozilla JSM
} else if (~String(this).indexOf('BackstagePass')) {
EXPORTED_SYMBOLS = ["PathParser"];
PathParser = factory();
// Browser global
} else {
PathParser = factory();
}
}(function () {
"use strict";
var PathParser = function (params) {
this.rules = [];
this.params = params;
}
PathParser.prototype = (function () {
function getParamsFromRule(rule, pathParts, queryParts) {
var params = {};
var missingParams = {};
// Parse path components
for (var i = 0; i < rule.parts.length; i++) {
var rulePart = rule.parts[i];
var part = pathParts[i];
if (part !== undefined) {
if (rulePart.charAt(0) == ':') {
params[rulePart.substr(1)] = part;
continue;
}
else if (rulePart !== part) {
return false;
}
}
else if (rulePart.charAt(0) != ':') {
return false;
}
else {
missingParams[rulePart.substr(1)] = true;
}
}
// Parse query strings
for (var i = 0; i < queryParts.length; ++i) {
var nameValue = queryParts[i].split('=', 2);
var key = nameValue[0];
// But ignore empty parameters and don't override named parameters
if (nameValue.length == 2 && !params[key] && !missingParams[key]) {
params[key] = nameValue[1];
}
}
return params;
}
return {
add: function (route, handler, autoPopulateOnMatch) {
this.rules.push({
parts: route.replace(/^\//, '').split('/'),
handler: handler,
autoPopulateOnMatch: autoPopulateOnMatch === undefined || autoPopulateOnMatch
});
},
run: function (url) {
if (url && url.length) {
url = url
// Remove redundant slashes
.replace(/\/+/g, '/')
// Strip leading and trailing '/' (at end or before query string)
.replace(/^\/|\/($|\?)/, '')
// Strip fragment identifiers
.replace(/#.*$/, '');
}
var urlSplit = url.split('?', 2);
var pathParts = urlSplit[0].split('/', 50);
var queryParts = urlSplit[1] ? urlSplit[1].split('&', 50) : [];
for (var i=0; i < this.rules.length; i++) {
var rule = this.rules[i];
var params = getParamsFromRule(rule, pathParts, queryParts);
if (params) {
params.url = url;
// Automatic parameter assignment
if (rule.autoPopulateOnMatch && this.params) {
for (var param in params) {
this.params[param] = params[param];
}
}
// Call handler with 'this' bound to parameter object
if (rule.handler) {
rule.handler.call(params);
}
return true;
}
}
return false;
}
};
})();
return PathParser;
}));