diff --git a/chrome/content/zotero-platform/mac/overlay.css b/chrome/content/zotero-platform/mac/overlay.css
index 6ed923252b..a3fa35a425 100644
--- a/chrome/content/zotero-platform/mac/overlay.css
+++ b/chrome/content/zotero-platform/mac/overlay.css
@@ -111,7 +111,7 @@
background-color: #ffffff;
}
-#zotero-view-selected-label {
+#zotero-item-pane-message {
color: #7f7f7f;
}
diff --git a/chrome/content/zotero/bindings/itembox.xml b/chrome/content/zotero/bindings/itembox.xml
index 927d76448f..cdf4c80601 100644
--- a/chrome/content/zotero/bindings/itembox.xml
+++ b/chrome/content/zotero/bindings/itembox.xml
@@ -79,7 +79,6 @@
break;
case 'merge':
- //this.hideEmptyFields = true;
this.clickByItem = true;
break;
@@ -92,6 +91,11 @@
this.blurHandler = this.hideEditor;
break;
+ case 'fieldmerge':
+ this.hideEmptyFields = true;
+ this._fieldAlternatives = {};
+ break;
+
default:
throw ("Invalid mode '" + val + "' in itembox.xml");
}
@@ -103,15 +107,22 @@
-
+
+
+ .item must be a Zotero.Item");
+ }
+ this._item = val;
+ this.refresh();
+ ]]>
+
+ onset="this.item = val; this.refresh();">
@@ -132,6 +143,22 @@
+
+ []
+
+
+ .visibleFields');
+ }
+
+ this._hiddenFields = val;
+ ]]>
+
+
+
+ {}
+
+
+ .fieldAlternatives');
+ }
+
+ if (this.mode != 'fieldmerge') {
+ throw ('fieldAlternatives is valid only in fieldmerge mode in .fieldAlternatives');
+ }
+
+ this._fieldAlternatives = val;
+ ]]>
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Choose the version of the item to use as the master item:
+
+
+
+
+
+
+
+ Select fields to keep from other versions of the item:
+
+
+
+
+
+
\ No newline at end of file
diff --git a/chrome/content/zotero/selectItemsDialog.js b/chrome/content/zotero/selectItemsDialog.js
index ec9750bb47..02ba2370ce 100644
--- a/chrome/content/zotero/selectItemsDialog.js
+++ b/chrome/content/zotero/selectItemsDialog.js
@@ -45,7 +45,7 @@ function doLoad()
collectionsView = new Zotero.CollectionTreeView();
// Don't show Commons when citing
- collectionsView.showCommons = false;
+ collectionsView.hideSources = ['duplicates', 'trash', 'commons'];
document.getElementById('zotero-collections-tree').view = collectionsView;
if(io.select) itemsView.selectItem(io.select);
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
index b340d857db..690d85847e 100644
--- a/chrome/content/zotero/xpcom/collectionTreeView.js
+++ b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -40,8 +40,7 @@ Zotero.CollectionTreeView = function()
this.itemToSelect = null;
this._highlightedRows = {};
this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share', 'group', 'bucket']);
- this.showDuplicates = false;
- this.showCommons = true;
+ this.hideSources = [];
}
/*
@@ -78,6 +77,12 @@ Zotero.CollectionTreeView.prototype.setTree = function(treebox)
var row = this.getLastViewedRow();
this.selection.select(row);
+
+ // TODO: make better
+ var tb = this._treebox;
+ setTimeout(function () {
+ tb.ensureRowIsVisible(row);
+ }, 1);
}
@@ -102,6 +107,17 @@ Zotero.CollectionTreeView.prototype.refresh = function()
this._dataItems = [];
this.rowCount = 0;
+ if (this.hideSources.indexOf('duplicates') == -1) {
+ try {
+ var duplicateLibraries = Zotero.Prefs.get('duplicateLibraries').split(',');
+ }
+ catch (e) {
+ // Add to personal library by default
+ Zotero.Prefs.set('duplicateLibraries', '0');
+ duplicateLibraries = ['0'];
+ }
+ }
+
try {
var unfiledLibraries = Zotero.Prefs.get('unfiledLibraries').split(',');
}
@@ -136,24 +152,31 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
- // Unfiled items
- if (unfiledLibraries.indexOf('0') != -1) {
- var s = new Zotero.Search;
- // Give virtual search an id so it can be reselected automatically
- s.id = 86345330000; // 'UNFILED' + '000' + libraryID
- s.name = Zotero.getString('pane.collections.unfiled');
- s.addCondition('libraryID', 'is', null);
- s.addCondition('unfiled', 'true');
- self._showItem(new Zotero.ItemGroup('search', s), 1, newRows+1);
+ // Duplicate items
+ if (self.hideSources.indexOf('duplicates') == -1 && duplicateLibraries.indexOf('0') != -1) {
+ var d = new Zotero.Duplicates(0);
+ self._showItem(new Zotero.ItemGroup('duplicates', d), 1, newRows+1);
newRows++;
}
- var deletedItems = Zotero.Items.getDeleted();
- if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) {
- self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1);
+ // Unfiled items
+ if (unfiledLibraries.indexOf('0') != -1) {
+ var s = new Zotero.Search;
+ s.name = Zotero.getString('pane.collections.unfiled');
+ s.addCondition('libraryID', 'is', null);
+ s.addCondition('unfiled', 'true');
+ self._showItem(new Zotero.ItemGroup('unfiled', s), 1, newRows+1);
newRows++;
}
- self.trashNotEmpty = !!deletedItems;
+
+ if (self.hideSources.indexOf('trash') == -1) {
+ var deletedItems = Zotero.Items.getDeleted();
+ if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) {
+ self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1);
+ newRows++;
+ }
+ self.trashNotEmpty = !!deletedItems;
+ }
return newRows;
}
@@ -195,15 +218,22 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
+ // Duplicate items
+ if (self.hideSources.indexOf('duplicates') == -1
+ && duplicateLibraries.indexOf(groups[i].libraryID + '') != -1) {
+ var d = new Zotero.Duplicates(groups[i].libraryID);
+ self._showItem(new Zotero.ItemGroup('duplicates', d), 2);
+ newRows++;
+ }
+
// Unfiled items
if (unfiledLibraries.indexOf(groups[i].libraryID + '') != -1) {
var s = new Zotero.Search;
- s.id = parseInt('8634533000' + groups[i].libraryID); // 'UNFILED' + '000' + libraryID
s.libraryID = groups[i].libraryID;
s.name = Zotero.getString('pane.collections.unfiled');
s.addCondition('libraryID', 'is', groups[i].libraryID);
s.addCondition('unfiled', 'true');
- self._showItem(new Zotero.ItemGroup('search', s), 2);
+ self._showItem(new Zotero.ItemGroup('unfiled', s), 2);
newRows++;
}
}
@@ -221,7 +251,7 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
- if (this.showCommons && Zotero.Commons.enabled) {
+ if (this.hideSources.indexOf('commons') == -1 && Zotero.Commons.enabled) {
this._showItem(new Zotero.ItemGroup('separator', false));
var header = {
id: "commons-header",
@@ -246,7 +276,14 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
- this._refreshHashMap();
+ try {
+ this._refreshHashMap();
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ Zotero.debug(e);
+ throw (e);
+ }
// Update the treebox's row count
var diff = this.rowCount - oldCount;
@@ -274,7 +311,7 @@ Zotero.CollectionTreeView.prototype.reload = function()
for(var i = 0; i < openCollections.length; i++)
{
var row = this._collectionRowMap[openCollections[i]];
- if (row != null) {
+ if (typeof row != 'undefined') {
this.toggleOpenState(row);
}
}
@@ -313,22 +350,20 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
switch (type)
{
case 'collection':
- if(this._collectionRowMap[ids[i]] != null)
- {
- rows.push(this._collectionRowMap[ids[i]]);
+ if (typeof this._rowMap['C' + ids[i]] != 'undefined') {
+ rows.push(this._rowMap['C' + ids[i]]);
}
break;
case 'search':
- if(this._searchRowMap[ids[i]] != null)
- {
- rows.push(this._searchRowMap[ids[i]]);
+ if (typeof this._rowMap['S' + ids[i]] != 'undefined') {
+ rows.push(this._rowMap['S' + ids[i]]);
}
break;
case 'group':
- //if (this._groupRowMap[ids[i]] != null) {
- // rows.push(this._groupRowMap[ids[i]]);
+ //if (this._rowMap['G' + ids[i]] != null) {
+ // rows.push(this._rowMap['G' + ids[i]]);
//}
// For now, just reload if a group is removed, since otherwise
@@ -410,7 +445,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
this.rememberSelection(savedSelection);
break;
}
- this.selection.select(this._searchRowMap[ids]);
+ this.selection.select(this._rowMap['S' + ids]);
break;
case 'group':
@@ -454,21 +489,6 @@ Zotero.CollectionTreeView.prototype.unregister = function()
Zotero.Notifier.unregisterObserver(this._unregisterID);
}
-Zotero.CollectionTreeView.prototype.isLibrary = function(row)
-{
- return this._getItemAtRow(row).isLibrary();
-}
-
-Zotero.CollectionTreeView.prototype.isCollection = function(row)
-{
- return this._getItemAtRow(row).isCollection();
-}
-
-Zotero.CollectionTreeView.prototype.isSearch = function(row)
-{
- return this._getItemAtRow(row).isSearch();
-}
-
////////////////////////////////////////////////////////////////////////////////
///
@@ -498,17 +518,6 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
}
break;
- case 'collection':
- // TODO: group collection
- return "chrome://zotero-platform/content/treesource-collection.png";
-
- case 'search':
- if ((source.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
- collectionType = "search-virtual";
- break;
- }
- return "chrome://zotero-platform/content/treesource-search.png";
-
case 'header':
if (source.ref.id == 'group-libraries-header') {
collectionType = 'groups';
@@ -521,7 +530,12 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
case 'group':
collectionType = 'library';
break;
+
+ case 'collection':
+ case 'search':
+ return "chrome://zotero-platform/content/treesource-" + collectionType + ".png";
}
+
return "chrome://zotero/skin/treesource-" + collectionType + ".png";
}
@@ -752,12 +766,13 @@ Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) {
return false;
}
+
/**
* Select the last-viewed source
*/
Zotero.CollectionTreeView.prototype.getLastViewedRow = function () {
var lastViewedFolder = Zotero.Prefs.get('lastViewedFolder');
- var matches = lastViewedFolder.match(/^(?:(C|S|G)([0-9]+)|L)$/);
+ var matches = lastViewedFolder.match(/^([A-Z])([0-9]+)?$/);
var select = 0;
if (matches) {
if (matches[1] == 'C') {
@@ -813,11 +828,11 @@ Zotero.CollectionTreeView.prototype.getLastViewedRow = function () {
}
}
}
- else if (matches[1] == 'S' && this._searchRowMap[matches[2]]) {
- select = this._searchRowMap[matches[2]];
- }
- else if (matches[1] == 'G' && this._groupRowMap[matches[2]]) {
- select = this._groupRowMap[matches[2]];
+ else {
+ var id = matches[1] + (matches[2] ? matches[2] : "");
+ if (this._rowMap[id]) {
+ select = this._rowMap[id];
+ }
}
}
@@ -929,20 +944,12 @@ Zotero.CollectionTreeView.prototype.saveSelection = function()
for (var i=0, len=this.rowCount; i;
xml.subject = this.subject;
@@ -183,3 +204,22 @@ Zotero.Relation.prototype.toXML = function () {
xml.object = this.object;
return xml;
}
+
+
+Zotero.Relation.prototype.serialize = function () {
+ // Use a hash of the parts as the object key
+ var key = Zotero.Utilities.Internal.md5(this.subject + "_" + this.predicate + "_" + this.object);
+
+ var obj = {
+ primary: {
+ libraryID: this.libraryID,
+ key: key,
+ },
+ fields: {
+ subject: this.subject,
+ predicate: this.predicate,
+ object: this.object
+ }
+ };
+ return obj;
+}
diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js
index b4ddc066dc..3254cbfabb 100644
--- a/chrome/content/zotero/xpcom/data/relations.js
+++ b/chrome/content/zotero/xpcom/data/relations.js
@@ -27,7 +27,10 @@ Zotero.Relations = new function () {
Zotero.DataObjects.apply(this, ['relation']);
this.constructor.prototype = new Zotero.DataObjects();
+ this.__defineGetter__('deletedItemPredicate', function () 'dc:isReplacedBy');
+
var _namespaces = {
+ dc: 'http://purl.org/dc/elements/1.1/',
owl: 'http://www.w3.org/2002/07/owl#'
};
@@ -46,7 +49,10 @@ Zotero.Relations = new function () {
* @return {Object[]}
*/
this.getByURIs = function (subject, predicate, object) {
- predicate = _getPrefixAndValue(predicate).join(':');
+ if (predicate) {
+ predicate = _getPrefixAndValue(predicate).join(':');
+ }
+
if (!subject && !predicate && !object) {
throw ("No values provided in Zotero.Relations.get()");
}
@@ -151,34 +157,66 @@ Zotero.Relations = new function () {
}
- this.erase = function (id) {
+ /**
+ * Copy relations from one object to another within the same library
+ */
+ this.copyURIs = function (libraryID, fromURI, toURI) {
+ var rels = this.getByURIs(fromURI);
+ for each(var rel in rels) {
+ this.add(libraryID, toURI, rel.predicate, rel.object);
+ }
+
+ var rels = this.getByURIs(false, false, fromURI);
+ for each(var rel in rels) {
+ this.add(libraryID, rel.subject, rel.predicate, toURI);
+ }
+ }
+
+
+ /**
+ * @param {String} prefix
+ * @param {String[]} ignorePredicates
+ */
+ this.eraseByURIPrefix = function (prefix, ignorePredicates) {
Zotero.DB.beginTransaction();
- var sql = "DELETE FROM relations WHERE ROWID=?";
- Zotero.DB.query(sql, [id]);
+ prefix = prefix + '%';
+ var sql = "SELECT ROWID FROM relations WHERE (subject LIKE ? OR object LIKE ?)";
+ var params = [prefix, prefix];
+ if (ignorePredicates) {
+ sql += " AND predicate != ?";
+ params = params.concat(ignorePredicates);
+ }
+ var ids = Zotero.DB.columnQuery(sql, params);
- // TODO: log to syncDeleteLog
+ for each(var id in ids) {
+ var relation = this.get(id);
+ relation.erase();
+ }
Zotero.DB.commitTransaction();
}
- this.eraseByURIPrefix = function (prefix) {
- prefix = prefix + '%';
- var sql = "DELETE FROM relations WHERE subject LIKE ? OR object LIKE ?";
- Zotero.DB.query(sql, [prefix, prefix]);
- }
-
-
this.eraseByURI = function (uri) {
- var sql = "DELETE FROM relations WHERE subject=? OR object=?";
- Zotero.DB.query(sql, [uri, uri]);
+ Zotero.DB.beginTransaction();
+
+ var sql = "SELECT ROWID FROM relations WHERE subject=? OR object=?";
+ var ids = Zotero.DB.columnQuery(sql, [uri, uri]);
+
+ for each(var id in ids) {
+ var relation = this.get(id);
+ relation.erase();
+ }
+
+ Zotero.DB.commitTransaction();
}
this.purge = function () {
- var sql = "SELECT subject FROM relations UNION SELECT object FROM relations";
- var uris = Zotero.DB.columnQuery(sql);
+ var sql = "SELECT subject FROM relations WHERE predicate != ? "
+ + "UNION SELECT object FROM relations WHERE predicate != ?";
+ var uris = Zotero.DB.columnQuery(sql, [this.deletedItemPredicate, this.deletedItemPredicate]);
if (uris) {
var prefix = Zotero.URI.defaultPrefix;
Zotero.DB.beginTransaction();
diff --git a/chrome/content/zotero/xpcom/duplicate.js b/chrome/content/zotero/xpcom/duplicate.js
deleted file mode 100644
index 6f8b2aba1d..0000000000
--- a/chrome/content/zotero/xpcom/duplicate.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2009 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 .
-
- ***** END LICENSE BLOCK *****
-*/
-
-
-Zotero.Duplicate = function(duplicateID) {
- this._id = duplicateID ? duplicateID : null;
- this._itemIDs = [];
-}
-
-Zotero.Duplicate.prototype.__defineGetter__('id', function () { return this._id; });
-
-Zotero.Duplicate.prototype.getIDs = function(idsTable) {
- if (!idsTable) {
- return;
- }
-
- var minLen = 5, percentLen = 1./3, checkLen, i, j;
-
- var sql = "SELECT itemID, value AS val "
- + "FROM " + idsTable + " NATURAL JOIN itemData "
- + "NATURAL JOIN itemDataValues "
- + "WHERE fieldID BETWEEN 110 AND 113 AND "
- + "itemID NOT IN (SELECT itemID FROM itemAttachments) "
- + "ORDER BY val";
-
- var results = Zotero.DB.query(sql);
-
- var resultsLen = results.length;
- this._itemIDs = [];
-
- for (i = 0; i < resultsLen; i++) {
- results[i].len = results[i].val.length;
- }
-
- for (i = 0; i < resultsLen; i++) {
- // title must be at least minLen long to be a duplicate
- if (results[i].len < minLen) {
- continue;
- }
-
- for (j = i + 1; j < resultsLen; j++) {
- // duplicates must match the first checkLen characters
- // checkLen = percentLen * the length of the longer title
- checkLen = (results[i].len >= results[j].len) ?
- parseInt(percentLen * results[i].len) : parseInt(percentLen * results[j].len);
- checkLen = (checkLen > results[i].len) ? results[i].len : checkLen;
- checkLen = (checkLen > results[j].len) ? results[j].len : checkLen;
- checkLen = (checkLen < minLen) ? minLen : checkLen;
-
- if (results[i].val.substr(0, checkLen) == results[j].val.substr(0, checkLen)) {
- // include results[i] when a duplicate is first found
- if (j == i + 1) {
- this._itemIDs.push(results[i].itemID);
- }
- this._itemIDs.push(results[j].itemID);
- }
- else {
- break;
- }
- }
- i = j - 1;
- }
-
- return this._itemIDs;
-}
diff --git a/chrome/content/zotero/xpcom/duplicates.js b/chrome/content/zotero/xpcom/duplicates.js
new file mode 100644
index 0000000000..fccc30c6d6
--- /dev/null
+++ b/chrome/content/zotero/xpcom/duplicates.js
@@ -0,0 +1,286 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2009 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 .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+Zotero.Duplicates = function (libraryID) {
+ if (typeof libraryID == 'undefined') {
+ throw ("libraryID not provided in Zotero.Duplicates constructor");
+ }
+
+ if (!libraryID) {
+ libraryID = null;
+ }
+
+ this._libraryID = libraryID;
+}
+
+
+Zotero.Duplicates.prototype.__defineGetter__('name', function () "Duplicate Items"); // TODO: localize
+Zotero.Duplicates.prototype.__defineGetter__('libraryID', function () this._libraryID);
+
+
+/**
+ * Get duplicates, populate a temporary table, and return a search based
+ * on that table
+ *
+ * @return {Zotero.Search}
+ */
+Zotero.Duplicates.prototype.getSearchObject = function () {
+ Zotero.DB.beginTransaction();
+
+ var sql = "DROP TABLE IF EXISTS tmpDuplicates";
+ Zotero.DB.query(sql);
+
+ var sql = "CREATE TEMPORARY TABLE tmpDuplicates "
+ + "(id INTEGER PRIMARY KEY)";
+ Zotero.DB.query(sql);
+
+ this._findDuplicates();
+ var ids = this._sets.findAll(true);
+
+ sql = "INSERT INTO tmpDuplicates VALUES (?)";
+ var insertStatement = Zotero.DB.getStatement(sql);
+
+ for each(var id in ids) {
+ insertStatement.bindInt32Parameter(0, id);
+
+ try {
+ insertStatement.execute();
+ }
+ catch(e) {
+ throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
+ }
+ }
+
+ Zotero.DB.commitTransaction();
+
+ var s = new Zotero.Search;
+ s.libraryID = this._libraryID;
+ s.addCondition('tempTable', 'is', 'tmpDuplicates');
+ return s;
+}
+
+
+/**
+ * Finds all items in the same set as a given item
+ *
+ * @param {Integer} itemID
+ * @return {Integer[]} Array of itemIDs
+ */
+Zotero.Duplicates.prototype.getSetItemsByItemID = function (itemID) {
+ return this._sets.findAllInSet(this._getObjectFromID(itemID), true);
+}
+
+
+Zotero.Duplicates.prototype._getObjectFromID = function (id) {
+ return {
+ get id() { return id; }
+ }
+}
+
+
+Zotero.Duplicates.prototype._findDuplicates = function () {
+ var self = this;
+
+ this._sets = new Zotero.DisjointSetForest;
+ var sets = this._sets;
+
+ function normalizeString(str) {
+ // Make sure we have a string and not an integer
+ str = str + "";
+
+ str = Zotero.Utilities.removeDiacritics(str)
+ .replace(/[^!-~]/g, ' ') // Convert punctuation to spaces
+ .replace(/ +/, ' ') // Normalize spaces
+ .toLowerCase();
+
+ return str;
+ }
+
+ /**
+ * @param {Function} compareRows Comparison function, if not exact match
+ */
+ function processRows(compareRows) {
+ if (!rows) {
+ return;
+ }
+
+ for (var i = 0, len = rows.length; i < len; i++) {
+ var j = i + 1, lastMatch = false, added = false;
+ while (j < len) {
+ if (compareRows) {
+ var match = compareRows(rows[i], rows[j]);
+ // Not a match, and don't try any more with this i value
+ if (match == -1) {
+ break;
+ }
+ // Not a match, but keep looking
+ if (match == 0) {
+ j++;
+ continue;
+ }
+ }
+ // If no comparison function, check for exact match
+ else {
+ if (rows[i].value != rows[j].value) {
+ break;
+ }
+ }
+
+ sets.union(
+ self._getObjectFromID(rows[i].itemID),
+ self._getObjectFromID(rows[j].itemID)
+ );
+
+ lastMatch = j;
+ j++;
+ }
+ if (lastMatch) {
+ i = lastMatch;
+ }
+ }
+ }
+
+ // Match on normalized title
+ var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+ + "JOIN itemDataValues USING (valueID) "
+ + "WHERE libraryID=? AND fieldID BETWEEN 110 AND 113 "
+ + "AND itemTypeID NOT IN (1, 14) "
+ + "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+ + "ORDER BY value COLLATE locale";
+ var rows = Zotero.DB.query(sql, [this._libraryID]);
+ processRows(function (a, b) {
+ a = normalizeString(a.value);
+ b = normalizeString(b.value);
+ // If we stripped one of the strings completely, we can't compare them
+ if (a.length == 0 || b.length == 0) {
+ return -1;
+ }
+ return a == b ? 1 : -1;
+ });
+
+ // Match on exact fields
+ var fields = ['DOI', 'ISBN'];
+ for each(var field in fields) {
+ var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+ + "JOIN itemDataValues USING (valueID) "
+ + "WHERE libraryID=? AND fieldID=? "
+ + "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+ + "ORDER BY value";
+ var rows = Zotero.DB.query(sql, [this._libraryID, Zotero.ItemFields.getID(field)]);
+ processRows();
+ }
+}
+
+
+
+/**
+ * Implements the Disjoint Set data structure
+ *
+ * Based on pseudo-code from http://en.wikipedia.org/wiki/Disjoint-set_data_structure
+ *
+ * Objects passed should have .id properties that uniquely identify them
+ */
+
+Zotero.DisjointSetForest = function () {
+ this._objects = {};
+}
+
+Zotero.DisjointSetForest.prototype.find = function (x) {
+ var id = x.id;
+
+ // If we've seen this object before, use the existing copy,
+ // which will have .parent and .rank properties
+ if (this._objects[id]) {
+ var obj = this._objects[id];
+ }
+ // Otherwise initialize it as a new set
+ else {
+ this._makeSet(x);
+ this._objects[id] = x;
+ var obj = x;
+ }
+
+ if (obj.parent.id == obj.id) {
+ return obj;
+ }
+ else {
+ obj.parent = this.find(obj.parent);
+ return obj.parent;
+ }
+}
+
+
+Zotero.DisjointSetForest.prototype.union = function (x, y) {
+ var xRoot = this.find(x);
+ var yRoot = this.find(y);
+
+ // Already in same set
+ if (xRoot.id == yRoot.id) {
+ return;
+ }
+
+ if (xRoot.rank < yRoot.rank) {
+ xRoot.parent = yRoot;
+ }
+ else if (xRoot.rank > yRoot.rank) {
+ yRoot.parent = xRoot;
+ }
+ else {
+ yRoot.parent = xRoot;
+ xRoot.rank = xRoot.rank + 1;
+ }
+}
+
+
+Zotero.DisjointSetForest.prototype.sameSet = function (x, y) {
+ return this.find(x) == this.find(y);
+}
+
+
+Zotero.DisjointSetForest.prototype.findAll = function (asIDs) {
+ var objects = [];
+ for each(var obj in this._objects) {
+ objects.push(asIDs ? obj.id : obj);
+ }
+ return objects;
+}
+
+
+Zotero.DisjointSetForest.prototype.findAllInSet = function (x, asIDs) {
+ var xRoot = this.find(x);
+ var objects = [];
+ for each(var obj in this._objects) {
+ if (this.find(obj) == xRoot) {
+ objects.push(asIDs ? obj.id : obj);
+ }
+ }
+ return objects;
+}
+
+
+Zotero.DisjointSetForest.prototype._makeSet = function (x) {
+ x.parent = x;
+ x.rank = 0;
+}
diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js
index f436549907..c75ea060d0 100644
--- a/chrome/content/zotero/xpcom/integration.js
+++ b/chrome/content/zotero/xpcom/integration.js
@@ -1203,7 +1203,40 @@ Zotero.Integration.Session.prototype.addCitation = function(index, noteIndex, ar
var zoteroItem = false;
if(citationItem.uri) {
[zoteroItem, needUpdate] = this.uriMap.getZoteroItemForURIs(citationItem.uri);
- if(needUpdate) this.updateIndices[index] = true;
+ if(zoteroItem) {
+ if(needUpdate) this.updateIndices[index] = true;
+ } else {
+ // Try merged item mappings
+ for each(var uri in citationItem.uri) {
+ var seen = [];
+
+ // Follow merged item relations until we find an item
+ // or hit a dead end
+ while (!zoteroItem) {
+ var relations = Zotero.Relations.getByURIs(uri, Zotero.Relations.deletedItemPredicate);
+ // No merged items found
+ if(!relations.length) {
+ break;
+ }
+
+ uri = relations[0].object;
+
+ // Keep track of mapped URIs in case there's a circular relation
+ if(seen.indexOf(uri) != -1) {
+ var msg = "Circular relation for '" + uri + "' in merged item mapping resolution";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+ break;
+ }
+ seen.push(uri);
+
+ [zoteroItem, needUpdate] = this.uriMap.getZoteroItemForURIs([uri]);
+ }
+ }
+
+ if(zoteroItem && needUpdate) this.updateIndices[index] = true;
+ }
+
} else {
if(citationItem.key) {
zoteroItem = Zotero.Items.getByKey(citationItem.key);
@@ -2044,8 +2077,13 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = function(uris) {
for(var i in uris) {
try {
- zoteroItem = Zotero.URI.getURIItem(uris[i]);
- if(zoteroItem) break;
+ zoteroItem = Zotero.URI.getURIItem(uris[i]);
+ if(zoteroItem) {
+ // Ignore items in the trash
+ if(zoteroItem.deleted) {
+ zoteroItem = false;
+ }
+ }
} catch(e) {}
}
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
index ed9feebc4f..095c19b75c 100644
--- a/chrome/content/zotero/xpcom/itemTreeView.js
+++ b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -122,19 +122,37 @@ Zotero.ItemTreeView.prototype.setTree = function(treebox)
obj.refresh();
// Add a keypress listener for expand/collapse
- var expandAllRows = obj.expandAllRows;
- var collapseAllRows = obj.collapseAllRows;
var tree = obj._treebox.treeBody.parentNode;
var listener = function(event) {
- var key = String.fromCharCode(event.which);
-
- if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
- obj.expandAllRows(treebox);
+ // Handle arrow keys specially on multiple selection, since
+ // otherwise the tree just applies it to the last-selected row
+ if (event.keyCode == 39 || event.keyCode == 37) {
+ if (obj._treebox.view.selection.count > 1) {
+ switch (event.keyCode) {
+ case 39:
+ obj.expandSelectedRows();
+ break;
+
+ case 37:
+ obj.collapseSelectedRows();
+ break;
+ }
+
+ event.preventDefault();
+ }
+
return;
}
- else if (key == '-' && !(event.shiftKey || event.ctrlKey ||
- event.altKey || event.metaKey)) {
- obj.collapseAllRows(treebox);
+
+ var key = String.fromCharCode(event.which);
+ if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
+ obj.expandAllRows();
+ event.preventDefault();
+ return;
+ }
+ else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {
+ obj.collapseAllRows();
+ event.preventDefault();
return;
}
};
@@ -206,7 +224,8 @@ Zotero.ItemTreeView.prototype.refresh = function()
Zotero.DB.beginTransaction();
Zotero.Items.cacheFields(cacheFields);
- var newRows = this._itemGroup.getChildItems();
+ var newRows = this._itemGroup.getItems();
+
var added = 0;
for (var i=0, len=newRows.length; i < len; i++) {
@@ -276,6 +295,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
var sort = false;
var savedSelection = this.saveSelection();
+ var previousRow = false;
// Redraw the tree (for tag color changes)
if (action == 'redraw') {
@@ -303,26 +323,27 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
return;
}
- if (this._itemGroup.isShare()) {
+ if (itemGroup.isShare()) {
return;
}
- this.selection.selectEventsSuppressed = true;
-
// See if we're in the active window
var zp = Zotero.getActiveZoteroPane();
var activeWindow = zp && zp.itemsView == this;
var quicksearch = this._ownerDocument.getElementById('zotero-tb-search');
-
// 'collection-item' ids are in the form collectionID-itemID
if (type == 'collection-item') {
+ if (!itemGroup.isCollection()) {
+ return;
+ }
+
var splitIDs = [];
for each(var id in ids) {
var split = id.split('-');
- // Skip if not collection or not an item in this collection
- if (!itemGroup.isCollection() || split[0] != this._itemGroup.ref.id) {
+ // Skip if not an item in this collection
+ if (split[0] != itemGroup.ref.id) {
continue;
}
splitIDs.push(split[1]);
@@ -336,41 +357,51 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
}
}
+ this.selection.selectEventsSuppressed = true;
+
if ((action == 'remove' && !itemGroup.isLibrary(true))
|| action == 'delete' || action == 'trash') {
- // Since a remove involves shifting of rows, we have to do it in order,
- // so sort the ids by row
- var rows = [];
- for(var i=0, len=ids.length; i 0)
- {
- rows.sort(function(a,b) { return a-b });
-
- for(var i=0, len=rows.length; i 0) {
+ rows.sort(function(a,b) { return a-b });
+
+ for(var i=0, len=rows.length; i 31
+ var sql = "UPDATE syncObjectTypes SET name='relation' WHERE syncObjectTypeID=6 AND name='relations'";
+ Zotero.DB.query(sql);
+
var sql = "SELECT * FROM syncObjectTypes";
var types = Zotero.DB.query(sql);
for each(var type in types) {
diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js
index 4e2920b5ee..ceff752109 100644
--- a/chrome/content/zotero/xpcom/utilities.js
+++ b/chrome/content/zotero/xpcom/utilities.js
@@ -558,7 +558,125 @@ Zotero.Utilities = {
return newString;
},
-
+
+ /**
+ * Replaces accented characters in a string with ASCII equivalents
+ *
+ * @param {String} str
+ * @param {Boolean} [lowercaseOnly] Limit conversions to lowercase characters
+ * (for improved performance on lowercase input)
+ * @return {String}
+ *
+ * From http://lehelk.com/2011/05/06/script-to-remove-diacritics/
+ */
+ "removeDiacritics": function (str, lowercaseOnly) {
+ var map = this._diacriticsRemovalMap.lowercase;
+ for (var i=0, len=map.length; i 5 && count == this.itemsView.rowCount;
+
+ // Initialize the merge pane with the selected items
+ Zotero_Duplicates_Pane.setItems(this.getSelectedItems(), displayNumItemsOnTypeError);
+ }
+ else {
+ // TODO: localize
+ var msg = "Select items to merge";
+ this.setItemPaneMessage(msg);
+ }
}
+ // Display label in the middle of the item pane
else {
- label.value = Zotero.getString('pane.item.selected.zero');
+ if (count) {
+ var msg = Zotero.getString('pane.item.selected.multiple', count);
+ }
+ else {
+ var msg = Zotero.getString('pane.item.selected.zero');
+ }
+
+ this.setItemPaneMessage(msg);
}
}
}
@@ -1400,23 +1427,12 @@ var ZoteroPane = new function()
// In collection, only prompt if trashing
var prompt = force ? (itemGroup.isWithinGroup() ? toDelete : toTrash) : false;
}
- // This should be changed if/when groups get trash
- else if (itemGroup.isGroup()) {
- var prompt = toDelete;
- }
- else if (itemGroup.isSearch()) {
+ else if (itemGroup.isSearch() || itemGroup.isUnfiled() || itemGroup.isDuplicates()) {
if (!force) {
return;
}
var prompt = toTrash;
}
- // Do nothing in share views
- else if (itemGroup.isShare()) {
- return;
- }
- else if (itemGroup.isBucket()) {
- var prompt = toDelete;
- }
// Do nothing in trash view if any non-deleted items are selected
else if (itemGroup.isTrash()) {
var start = {};
@@ -1431,6 +1447,17 @@ var ZoteroPane = new function()
}
var prompt = toDelete;
}
+ // This should be changed if/when groups get trash
+ else if (itemGroup.isGroup()) {
+ var prompt = toDelete;
+ }
+ else if (itemGroup.isBucket()) {
+ var prompt = toDelete;
+ }
+ // Do nothing in share views
+ else if (itemGroup.isShare()) {
+ return;
+ }
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
@@ -1439,11 +1466,37 @@ var ZoteroPane = new function()
}
}
+
+ this.mergeSelectedItems = function () {
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
+ document.getElementById('zotero-item-pane-content').selectedIndex = 4;
+
+ if (typeof Zotero_Duplicates_Pane == 'undefined') {
+ Zotero.debug("Loading duplicatesMerge.js");
+ Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader)
+ .loadSubScript("chrome://zotero/content/duplicatesMerge.js");
+ }
+
+ // Initialize the merge pane with the selected items
+ Zotero_Duplicates_Pane.setItems(this.getSelectedItems());
+ }
+
+
this.deleteSelectedCollection = function () {
- // Remove virtual Unfiled search
- var row = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
- if (row.isSearch() && (row.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
- this.setUnfiled(row.ref.libraryID, false);
+ var itemGroup = this.getItemGroup();
+
+ // Remove virtual duplicates collection
+ if (itemGroup.isDuplicates()) {
+ this.setVirtual(itemGroup.ref.libraryID, 'duplicates', false);
+ }
+ // Remove virtual unfiled collection
+ else if (itemGroup.isUnfiled()) {
+ this.setVirtual(itemGroup.ref.libraryID, 'unfiled', false);
return;
}
@@ -1453,14 +1506,14 @@ var ZoteroPane = new function()
}
if (this.collectionsView.selection.count == 1) {
- if (row.isCollection())
+ if (itemGroup.isCollection())
{
if (confirm(Zotero.getString('pane.collections.delete')))
{
this.collectionsView.deleteSelection();
}
}
- else if (row.isSearch())
+ else if (itemGroup.isSearch())
{
if (confirm(Zotero.getString('pane.collections.deleteSearch')))
{
@@ -1939,30 +1992,39 @@ var ZoteroPane = new function()
this.buildCollectionContextMenu = function buildCollectionContextMenu()
{
+ var options = [
+ "newCollection",
+ "newSavedSearch",
+ "newSubcollection",
+ "sep1",
+ "showDuplicates",
+ "showUnfiled",
+ "editSelectedCollection",
+ "removeCollection",
+ "sep2",
+ "exportCollection",
+ "createBibCollection",
+ "exportFile",
+ "loadReport",
+ "emptyTrash",
+ "createCommonsBucket",
+ "refreshCommonsBucket"
+ ];
+
+ var m = {};
+ var i = 0;
+ for each(var option in options) {
+ m[option] = i++;
+ }
+
var menu = document.getElementById('zotero-collectionmenu');
- var m = {
- newCollection: 0,
- newSavedSearch: 1,
- newSubcollection: 2,
- sep1: 3,
- showUnfiled: 4,
- editSelectedCollection: 5,
- removeCollection: 6,
- sep2: 7,
- exportCollection: 8,
- createBibCollection: 9,
- exportFile: 10,
- loadReport: 11,
- emptyTrash: 12,
- createCommonsBucket: 13,
- refreshCommonsBucket: 14
- };
var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
- var enable = [], disable = [], show = [];
+ // By default things are hidden and visible, so we only need to record
+ // when things are visible and when they're visible but disabled
+ var show = [], disable = [];
- // Collection
if (itemGroup.isCollection()) {
show = [
m.newSubcollection,
@@ -1975,15 +2037,13 @@ var ZoteroPane = new function()
m.loadReport
];
var s = [m.exportCollection, m.createBibCollection, m.loadReport];
- if (this.itemsView.rowCount>0) {
- enable = s;
- }
- else if (!this.collectionsView.isContainerEmpty(this.collectionsView.selection.currentIndex)) {
- enable = [m.exportCollection];
- disable = [m.createBibCollection, m.loadReport];
- }
- else {
- disable = s;
+ if (!this.itemsView.rowCount) {
+ if (!this.collectionsView.isContainerEmpty(this.collectionsView.selection.currentIndex)) {
+ disable = [m.createBibCollection, m.loadReport];
+ }
+ else {
+ disable = s;
+ }
}
// Adjust labels
@@ -1993,35 +2053,20 @@ var ZoteroPane = new function()
menu.childNodes[m.createBibCollection].setAttribute('label', Zotero.getString('pane.collections.menu.createBib.collection'));
menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.collection'));
}
- // Saved Search
else if (itemGroup.isSearch()) {
- // Unfiled items view
- if ((itemGroup.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
- show = [
- m.removeCollection
- ];
-
- menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('general.remove'));
- }
- // Normal search view
- else {
- show = [
- m.editSelectedCollection,
- m.removeCollection,
- m.sep2,
- m.exportCollection,
- m.createBibCollection,
- m.loadReport
- ];
-
- menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('pane.collections.menu.remove.savedSearch'));
- }
+ show = [
+ m.editSelectedCollection,
+ m.removeCollection,
+ m.sep2,
+ m.exportCollection,
+ m.createBibCollection,
+ m.loadReport
+ ];
+
+ menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('pane.collections.menu.remove.savedSearch'));
var s = [m.exportCollection, m.createBibCollection, m.loadReport];
- if (this.itemsView.rowCount>0) {
- enable = s;
- }
- else {
+ if (!this.itemsView.rowCount) {
disable = s;
}
@@ -2031,10 +2076,19 @@ var ZoteroPane = new function()
menu.childNodes[m.createBibCollection].setAttribute('label', Zotero.getString('pane.collections.menu.createBib.savedSearch'));
menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.savedSearch'));
}
- // Trash
else if (itemGroup.isTrash()) {
show = [m.emptyTrash];
}
+ else if (itemGroup.isGroup()) {
+ show = [m.newCollection, m.newSavedSearch, m.sep1, m.showDuplicates, m.showUnfiled];
+ }
+ else if (itemGroup.isDuplicates() || itemGroup.isUnfiled()) {
+ show = [
+ m.removeCollection
+ ];
+
+ menu.childNodes[m.removeCollection].setAttribute('label', Zotero.getString('general.remove'));
+ }
else if (itemGroup.isHeader()) {
if (itemGroup.ref.id == 'commons-header') {
show = [m.createCommonsBucket];
@@ -2043,67 +2097,63 @@ var ZoteroPane = new function()
else if (itemGroup.isBucket()) {
show = [m.refreshCommonsBucket];
}
- // Group
- else if (itemGroup.isGroup()) {
- show = [m.newCollection, m.newSavedSearch, m.sep1, m.showUnfiled];
- }
// Library
else
{
- show = [m.newCollection, m.newSavedSearch, m.sep1, m.showUnfiled, m.sep2, m.exportFile];
+ show = [m.newCollection, m.newSavedSearch, m.showDuplicates, m.showUnfiled, m.sep2, m.exportFile];
}
// Disable some actions if user doesn't have write access
var s = [m.editSelectedCollection, m.removeCollection, m.newCollection, m.newSavedSearch, m.newSubcollection];
- if (itemGroup.isWithinGroup() && !itemGroup.editable) {
+ if (itemGroup.isWithinGroup() && !itemGroup.editable && !itemGroup.isDuplicates() && !itemGroup.isUnfiled()) {
disable = disable.concat(s);
}
- else {
- enable = enable.concat(s);
- }
- for (var i in disable)
- {
- menu.childNodes[disable[i]].setAttribute('disabled', true);
- }
-
- for (var i in enable)
- {
- menu.childNodes[enable[i]].setAttribute('disabled', false);
- }
-
- // Hide all items by default
+ // Hide and enable all actions by default (so if they're shown they're enabled)
for each(var pos in m) {
menu.childNodes[pos].setAttribute('hidden', true);
+ menu.childNodes[pos].setAttribute('disabled', false);
}
for (var i in show)
{
menu.childNodes[show[i]].setAttribute('hidden', false);
}
+
+ for (var i in disable)
+ {
+ menu.childNodes[disable[i]].setAttribute('disabled', true);
+ }
}
function buildItemContextMenu()
{
- var m = {
- showInLibrary: 0,
- sep1: 1,
- addNote: 2,
- addAttachments: 3,
- sep2: 4,
- duplicateItem: 5,
- deleteItem: 6,
- deleteFromLibrary: 7,
- sep3: 8,
- exportItems: 9,
- createBib: 10,
- loadReport: 11,
- sep4: 12,
- createParent: 13,
- recognizePDF: 14,
- renameAttachments: 15,
- reindexItem: 16
- };
+ var options = [
+ 'showInLibrary',
+ 'sep1',
+ 'addNote',
+ 'addAttachments',
+ 'sep2',
+ 'duplicateItem',
+ 'deleteItem',
+ 'deleteFromLibrary',
+ 'mergeItems',
+ 'sep3',
+ 'exportItems',
+ 'createBib',
+ 'loadReport',
+ 'sep4',
+ 'createParent',
+ 'recognizePDF',
+ 'renameAttachments',
+ 'reindexItem'
+ ];
+
+ var m = {};
+ var i = 0;
+ for each(var option in options) {
+ m[option] = i++;
+ }
var menu = document.getElementById('zotero-itemmenu');
@@ -2112,59 +2162,62 @@ var ZoteroPane = new function()
menu.removeChild(menu.firstChild);
}
- var enable = [], disable = [], show = [], hide = [], multiple = '';
+ var disable = [], show = [], multiple = '';
if (!this.itemsView) {
return;
}
+ var itemGroup = this.getItemGroup();
+
+ show.push(m.deleteItem, m.deleteFromLibrary, m.sep3, m.exportItems, m.createBib, m.loadReport);
+
if (this.itemsView.selection.count > 0) {
- var itemGroup = this.itemsView._itemGroup;
-
- enable.push(m.showInLibrary, m.addNote, m.addAttachments,
- m.sep2, m.duplicateItem, m.deleteItem, m.deleteFromLibrary,
- m.exportItems, m.createBib, m.loadReport);
-
// Multiple items selected
if (this.itemsView.selection.count > 1) {
var multiple = '.multiple';
- hide.push(m.showInLibrary, m.sep1, m.addNote, m.addAttachments,
- m.sep2, m.duplicateItem);
- // If all items can be reindexed, or all items can be recognized, show option
var items = this.getSelectedItems();
- var canIndex = true;
- var canRecognize = true;
+ var canMerge = true, canIndex = true, canRecognize = true, canRename = true;
+
if (!Zotero.Fulltext.pdfConverterIsRegistered()) {
canIndex = false;
}
- for (var i=0; i
-
-
@@ -153,8 +151,9 @@
-
-
+
+
+
@@ -231,7 +230,8 @@
-
+
+
@@ -261,6 +261,8 @@
+
+
@@ -316,7 +318,7 @@
enableColumnDrag="true"
onfocus="if (ZoteroPane_Local.itemsView.rowCount && !ZoteroPane_Local.itemsView.selection.count) { ZoteroPane_Local.itemsView.selection.select(0); }"
onkeypress="ZoteroPane_Local.handleKeyPress(event, this.id)"
- onselect="ZoteroPane_Local.itemSelected();"
+ onselect="ZoteroPane_Local.itemSelected(event)"
ondragstart="if (event.target.localName == 'treechildren') { ZoteroPane_Local.itemsView.onDragStart(event); }"
ondragenter="return ZoteroPane_Local.itemsView.onDragEnter(event)"
ondragover="return ZoteroPane_Local.itemsView.onDragOver(event)"
@@ -417,37 +419,8 @@
onmousemove="ZoteroPane_Local.updateToolbarPosition()"
oncommand="ZoteroPane_Local.updateToolbarPosition()"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd
index fc366b0f50..8528c1fd51 100644
--- a/chrome/locale/en-US/zotero/zotero.dtd
+++ b/chrome/locale/en-US/zotero/zotero.dtd
@@ -37,6 +37,7 @@
+
@@ -85,7 +86,6 @@
-
diff --git a/chrome/skin/default/zotero/bindings/itembox.css b/chrome/skin/default/zotero/bindings/itembox.css
index e25d355cf4..688b008245 100644
--- a/chrome/skin/default/zotero/bindings/itembox.css
+++ b/chrome/skin/default/zotero/bindings/itembox.css
@@ -148,4 +148,10 @@ hbox.zotero-date-field-status > label
background-position: center !important;
border-width: 0 !important;
-moz-border-radius: 4px !important;
+}
+
+/* Merge pane in duplicates view */
+.zotero-field-version-button {
+ margin: 0;
+ padding: 0;
}
\ No newline at end of file
diff --git a/chrome/skin/default/zotero/itemPane.css b/chrome/skin/default/zotero/itemPane.css
index 87f4f391a4..cd22c282d0 100644
--- a/chrome/skin/default/zotero/itemPane.css
+++ b/chrome/skin/default/zotero/itemPane.css
@@ -1,4 +1,54 @@
+#zotero-item-pane-message {
+ padding: 0 2em;
+}
+
+#zotero-view-tabbox, #zotero-item-pane-content > groupbox, #zotero-item-pane-content > groupbox > .groupbox-body
+{
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+#zotero-view-tabbox tabs tab
+{
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+
+#zotero-view-tabbox tabs tab .tab-text
+{
+ margin-top: .2em !important;
+ margin-bottom: .25em !important;
+}
+
+#zotero-view-item
+{
+ padding: 1.5em .25em .25em;
+}
+
#zotero-view-item > tabpanel > *
{
overflow: auto;
}
+
+#zotero-view-item > vbox
+{
+ overflow: auto;
+ margin-left: 5px;
+}
+
+
+/* Merge pane in duplicates view */
+#zotero-duplicates-merge-button
+{
+ font-size: 13px;
+}
+
+#zotero-duplicates-merge-pane > groupbox
+{
+ margin: 0;
+}
+
+#zotero-duplicates-merge-item-box row
+{
+ min-height: 20px;
+}
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
index 9e748467cc..6b0a3d7d9f 100644
--- a/chrome/skin/default/zotero/overlay.css
+++ b/chrome/skin/default/zotero/overlay.css
@@ -429,35 +429,6 @@
cursor: default;
}
-#zotero-view-tabbox, #zotero-item-pane-content > groupbox, #zotero-item-pane-content > groupbox > .groupbox-body
-{
- margin: 0 !important;
- padding: 0 !important;
-}
-
-#zotero-view-tabbox tabs tab
-{
- margin-top: 0 !important;
- margin-bottom: 0 !important;
-}
-
-#zotero-view-tabbox tabs tab .tab-text
-{
- margin-top: .2em !important;
- margin-bottom: .25em !important;
-}
-
-#zotero-view-item
-{
- padding: 1.5em .25em .25em;
-}
-
-#zotero-view-item > vbox
-{
- overflow: auto;
- margin-left: 5px;
-}
-
#zotero-splitter
{
border-top: none;
diff --git a/chrome/skin/default/zotero/treesource-duplicates.png b/chrome/skin/default/zotero/treesource-duplicates.png
new file mode 100644
index 0000000000..ca779f3237
Binary files /dev/null and b/chrome/skin/default/zotero/treesource-duplicates.png differ
diff --git a/chrome/skin/default/zotero/treesource-search-virtual.png b/chrome/skin/default/zotero/treesource-search-virtual.png
deleted file mode 100644
index 44084add79..0000000000
Binary files a/chrome/skin/default/zotero/treesource-search-virtual.png and /dev/null differ
diff --git a/components/zotero-service.js b/components/zotero-service.js
index d47b086295..1f8631ca6e 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -76,7 +76,7 @@ const xpcomFilesLocal = [
'data/tags',
'date',
'db',
- 'duplicate',
+ 'duplicates',
'enstyle',
'fulltext',
'id',
diff --git a/system.sql b/system.sql
index 623ffeff9f..d7560b1763 100644
--- a/system.sql
+++ b/system.sql
@@ -1359,4 +1359,4 @@ INSERT INTO "syncObjectTypes" VALUES(2, 'creator');
INSERT INTO "syncObjectTypes" VALUES(3, 'item');
INSERT INTO "syncObjectTypes" VALUES(4, 'search');
INSERT INTO "syncObjectTypes" VALUES(5, 'tag');
-INSERT INTO "syncObjectTypes" VALUES(6, 'relations');
+INSERT INTO "syncObjectTypes" VALUES(6, 'relation');