commit 56c7afc47ef6123fbbed25ac89fd18bacaed0e15
parent e945b84b5f265ff7312462b998ca8cd5b6cdf0d3
Author: Dan Stillman <dstillman@zotero.org>
Date: Fri, 22 Jul 2011 21:24:38 +0000
Duplicate detection:
- Adds a per-library "Duplicate Items" virtual search to the source list -- shows up by default for "My Library" but can be added to and removed from all libraries
- Current matching algorithm is very basic: finds exact title matches (after normalizing case/diacritics/punctuation/spacing) and DOI/ISBN matches (untested)
- In duplicates view, sets are selected automatically; in other views, duplicate items can be selected manually and the merge interface can be brought up with "Merge Items" in the context menu
- Can select a master item and individual fields to merge from other versions
- Word processor integration code will automatically find mapped replacements and update documents with new item keys
Possible future improvements:
- Improved detection algorithms
- UI tweaks
- Currently if any items differ, all available versions will be shown as master item options, even if only one item is different; probably the earliest equivalent item should be shown for each distinct version
- Caching of results for performance
- Confidence scale
- Creator version selection (currently the creators from the chosen master item are kept)
- Merging of matching child items
- Better sorting of duplicates if not clustered together by the selected sort column
- Relation path compression when merging items that are already mapped to previously removed duplicates
Other changes in this commit:
- Don't show Trash in word processor integration windows
- Consider items in trash to be missing in word processor documents
- Selection of special views (Trash, Unfiled, Duplicates) is now restored properly in new windows
- Disabled field transform context menu when item isn't editable
- Left/right arrow now expands/collapses all selected items instead of just the last-selected row
- Relation deletions are now synced
- The same items row is now reselected after item deletion
- (dev) Zotero.Item.getNotes(), Zotero.Item.getAttachments(), and Zotero.Item.getTags() now return empty arrays rather than FALSE if no matches -- tests on those return values in third-party code will need to be changed
- (dev) New function Zotero.Utilities.removeDiacritics(str, lowercaseOnly) -- could be used to generate ASCII BibTeX keys
- (dev) New 'tempTable' search condition can take a table to join against -- useful for implementing virtual source lists
- (dev) Significant UI code cleanup
- (dev) Moved all item pane content into itemPane.xul
- Probably various other things
Needless to say, this needs testing.
Diffstat:
29 files changed, 1843 insertions(+), 836 deletions(-)
diff --git 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
@@ -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 @@
</property>
<field name="_item"/>
- <property name="item"
- onget="return this._item;"
- onset="this._item = val; this.refresh();">
+ <property name="item" onget="return this._item;">
+ <setter>
+ <![CDATA[
+ if (!(val instanceof Zotero.Item)) {
+ throw ("<zoteroitembox>.item must be a Zotero.Item");
+ }
+ this._item = val;
+ this.refresh();
+ ]]>
+ </setter>
</property>
<!-- .ref is an alias for .item -->
<property name="ref"
onget="return this._item;"
- onset="this._item = val; this.refresh();">
+ onset="this.item = val; this.refresh();">
</property>
@@ -133,6 +144,22 @@
</property>
<!--
+ An array of field names that should be hidden
+ -->
+ <field name="_hiddenFields">[]</field>
+ <property name="hiddenFields">
+ <setter>
+ <![CDATA[
+ if (val.constructor.name != 'Array') {
+ throw ('hiddenFields must be an array in <itembox>.visibleFields');
+ }
+
+ this._hiddenFields = val;
+ ]]>
+ </setter>
+ </property>
+
+ <!--
An array of field names that should be clickable
even if this.clickable is false
-->
@@ -166,6 +193,26 @@
</setter>
</property>
+ <!--
+ An object of alternative values for keyed fields
+
+ -->
+ <field name="_fieldAlternatives">{}</field>
+ <property name="fieldAlternatives">
+ <setter>
+ <![CDATA[
+ if (val.constructor.name != 'Object') {
+ throw ('fieldAlternatives must be an Object in <itembox>.fieldAlternatives');
+ }
+
+ if (this.mode != 'fieldmerge') {
+ throw ('fieldAlternatives is valid only in fieldmerge mode in <itembox>.fieldAlternatives');
+ }
+
+ this._fieldAlternatives = val;
+ ]]>
+ </setter>
+ </property>
<!--
An array of field names in the order they should appear
@@ -209,7 +256,6 @@
onget="return '(' + Zotero.getString('pane.item.defaultLastName') + ')'"/>
<property name="_defaultFullName"
onget="return '(' + Zotero.getString('pane.item.defaultFullName') + ')'"/>
-
<method name="refresh">
<body>
<![CDATA[
@@ -285,6 +331,10 @@
}
if (fieldName) {
+ if (this._hiddenFields.indexOf(fieldName) != -1) {
+ continue;
+ }
+
// createValueElement() adds the itemTypeID as an attribute
// and converts it to a localized string for display
if (fieldName == 'itemType') {
@@ -294,13 +344,14 @@
val = this.item.getField(fieldName);
}
- var fieldIsClickable = this._fieldIsClickable(fieldName);
-
if (!val && this.hideEmptyFields
- && this._visibleFields.indexOf(fieldName) == -1) {
+ && this._visibleFields.indexOf(fieldName) == -1
+ && (this.mode != 'fieldmerge' || typeof this._fieldAlternatives[fieldName] == 'undefined')) {
continue;
}
+ var fieldIsClickable = this._fieldIsClickable(fieldName);
+
// Start tabindex at 1001 after creators
var tabindex = fieldIsClickable
? (i>0 ? this._tabIndexMinFields + i : 1) : 0;
@@ -365,11 +416,39 @@
"if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); }");
}
- this.addDynamicRow(label, valueElement);
+ var row = this.addDynamicRow(label, valueElement);
if (fieldName && this._selectField == fieldName) {
this.showEditor(valueElement);
}
+
+ // In field merge mode, add a button to switch field versions
+ else if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') {
+ var button = document.createElement("toolbarbutton");
+ button.className = 'zotero-field-version-button';
+ button.setAttribute('image', 'chrome://zotero/skin/treesource-duplicates.png');
+ button.setAttribute('type', 'menu');
+
+ var popup = button.appendChild(document.createElement("menupopup"));
+
+ for each(var v in this._fieldAlternatives[fieldName]) {
+ var menuitem = document.createElement("menuitem");
+ menuitem.setAttribute('label', Zotero.Utilities.ellipsize(v, 40));
+ menuitem.setAttribute('fieldName', fieldName);
+ menuitem.setAttribute('originalValue', v);
+ menuitem.setAttribute(
+ 'oncommand',
+ "var binding = document.getBindingParent(this); "
+ + "var item = binding.item; "
+ + "item.setField(this.getAttribute('fieldName'), this.getAttribute('originalValue')); "
+ + "var row = Zotero.getAncestorByTagName(this, 'row'); "
+ + "binding.refresh();"
+ );
+ popup.appendChild(menuitem);
+ }
+
+ row.appendChild(button);
+ }
}
this._selectField = false;
@@ -446,13 +525,11 @@
this.addCreatorRow(false, false, true, true);
}
-
// Move to next or previous field if (shift-)tab was pressed
if (this._lastTabIndex && this._tabDirection)
{
this._focusNextField('info', this._dynamicFields, this._lastTabIndex, this._tabDirection == -1);
}
-
]]>
</body>
</method>
@@ -534,6 +611,8 @@
else {
this._dynamicFields.appendChild(row);
}
+
+ return row;
]]>
</body>
</method>
@@ -1140,10 +1219,10 @@
}
// Display a context menu for certain fields
- if (fieldName == 'seriesTitle' || fieldName == 'shortTitle' ||
+ if (this.editable && (fieldName == 'seriesTitle' || fieldName == 'shortTitle' ||
Zotero.ItemFields.isFieldOfBase(fieldID, 'title') ||
- Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle')) {
- valueElement.setAttribute('contextmenu', 'field-menu');
+ Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) {
+ valueElement.setAttribute('contextmenu', 'zotero-field-transform-menu');
}
}
@@ -2261,7 +2340,7 @@
);
typeBox.setAttribute('typeid', typeID);
document.getBindingParent(this).modifyCreator(index, fields);"/>
- <menupopup id="field-menu">
+ <menupopup id="zotero-field-transform-menu">
<menu label="&zotero.item.textTransform;">
<menupopup>
<menuitem label="&zotero.item.textTransform.titlecase;" class="menuitem-non-iconic"
diff --git a/chrome/content/zotero/bindings/tagselector.xml b/chrome/content/zotero/bindings/tagselector.xml
@@ -467,6 +467,20 @@
<parameter name="ids"/>
<body>
<![CDATA[
+ var itemGroup = ZoteroPane_Local.getItemGroup();
+
+ // Ignore anything other than deletes in duplicates view
+ if (itemGroup.isDuplicates()) {
+ switch (event) {
+ case 'delete':
+ case 'trash':
+ break;
+
+ default:
+ return;
+ }
+ }
+
// If a selected tag no longer exists, deselect it
if (event == 'delete') {
this._tags = Zotero.Tags.getAll(this._types, this.libraryID);
diff --git a/chrome/content/zotero/duplicatesMerge.js b/chrome/content/zotero/duplicatesMerge.js
@@ -0,0 +1,156 @@
+/*
+ ***** 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 <http://www.gnu.org/licenses/>.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+var Zotero_Duplicates_Pane = new function () {
+ _items = [];
+ _otherItems = [];
+ _ignoreFields = ['dateAdded', 'dateModified', 'accessDate'];
+
+ this.setItems = function (items, displayNumItemsOnTypeError) {
+ var itemTypeID, oldestItem, otherItems = [];
+ for each(var item in items) {
+ // Find the oldest item
+ if (!oldestItem) {
+ oldestItem = item;
+ }
+ else if (item.dateAdded < oldestItem.dateAdded) {
+ otherItems.push(oldestItem);
+ oldestItem = item;
+ }
+ else {
+ otherItems.push(item);
+ }
+
+ if (!item.isRegularItem() || [1,14].indexOf(item.itemTypeID) != -1) {
+ // TODO: localize
+ var msg = "Only top-level full items can be merged.";
+ ZoteroPane_Local.setItemPaneMessage(msg);
+ return false;
+ }
+
+ // Make sure all items are of the same type
+ if (itemTypeID) {
+ if (itemTypeID != item.itemTypeID) {
+ if (displayNumItemsOnTypeError) {
+ var msg = Zotero.getString('pane.item.selected.multiple', items.length);
+ }
+ else {
+ // TODO: localize
+ var msg = "Merged items must all be of the same item type.";
+ }
+ ZoteroPane_Local.setItemPaneMessage(msg);
+ return false;
+ }
+ }
+ else {
+ itemTypeID = item.itemTypeID;
+ }
+ }
+
+ _items = items;
+
+ _items.sort(function (a, b) {
+ return a.dateAdded > b.dateAdded ? 1 : a.dateAdded == b.dateAdded ? 0 : -1;
+ });
+
+ //
+ // Update the UI
+ //
+
+ var diff = oldestItem.multiDiff(otherItems, _ignoreFields);
+
+ var button = document.getElementById('zotero-duplicates-merge-button');
+ var versionSelect = document.getElementById('zotero-duplicates-merge-version-select');
+ var itembox = document.getElementById('zotero-duplicates-merge-item-box');
+ var fieldSelect = document.getElementById('zotero-duplicates-merge-field-select');
+
+ versionSelect.hidden = !diff;
+ if (diff) {
+ // Populate menulist with Date Added values from all items
+ var dateList = document.getElementById('zotero-duplicates-merge-original-date');
+
+ while (dateList.itemCount) {
+ dateList.removeItemAt(0);
+ }
+
+ var numRows = 0;
+ for each(var item in _items) {
+ var date = Zotero.Date.sqlToDate(item.dateAdded, true);
+ dateList.appendItem(date.toLocaleString());
+ numRows++;
+ }
+
+ dateList.setAttribute('rows', numRows);
+
+ // If we set this inline, the selection doesn't take on the first
+ // selection after unhiding versionSelect (when clicking
+ // from a set with no differences) -- tested in Fx5.0.1
+ setTimeout(function () {
+ dateList.selectedIndex = 0;
+ }, 0);
+ }
+
+ button.label = "Merge " + (otherItems.length + 1) + " items";
+ itembox.hiddenFields = diff ? [] : ['dateAdded', 'dateModified'];
+ fieldSelect.hidden = !diff;
+
+ this.setMaster(0);
+
+ return true;
+ }
+
+
+ this.setMaster = function (pos) {
+ var itembox = document.getElementById('zotero-duplicates-merge-item-box');
+ itembox.mode = 'fieldmerge';
+
+ _otherItems = _items.concat();
+ var item = _otherItems.splice(pos, 1)[0];
+
+ // Add master item's values to the beginning of each set of
+ // alternative values so that they're still available if the item box
+ // modifies the item
+ var diff = item.multiDiff(_otherItems, _ignoreFields);
+ if (diff) {
+ var itemValues = item.serialize()
+ for (var i in diff) {
+ diff[i].unshift(itemValues.fields[i]);
+ }
+ itembox.fieldAlternatives = diff;
+ }
+
+ itembox.item = item.clone(true);
+ }
+
+
+ this.merge = function () {
+ var itembox = document.getElementById('zotero-duplicates-merge-item-box');
+ // Work around item.clone() weirdness -- the cloned item can't safely be
+ // used after it's saved, because it's not the version in memory and
+ // doesn't get reloaded properly in item.save()
+ var item = Zotero.Items.get(itembox.item.id);
+ Zotero.Items.merge(item, _otherItems);
+ }
+}
diff --git a/chrome/content/zotero/itemPane.xul b/chrome/content/zotero/itemPane.xul
@@ -28,39 +28,96 @@
<!DOCTYPE window SYSTEM "chrome://zotero/locale/zotero.dtd">
-<overlay
- xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="include.js"/>
<script src="itemPane.js"/>
- <tabpanels id="zotero-view-item" flex="1">
- <tabpanel>
- <zoteroitembox id="zotero-editpane-item-box" flex="1"/>
- </tabpanel>
-
- <tabpanel flex="1" orient="vertical">
- <vbox flex="1">
- <hbox align="center">
- <label id="zotero-editpane-notes-label"/>
- <button id="zotero-editpane-notes-add" label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote(event.shiftKey);"/>
- </hbox>
- <grid flex="1">
- <columns>
- <column flex="1"/>
- <column/>
- </columns>
- <rows id="zotero-editpane-dynamic-notes" flex="1"/>
- </grid>
- </vbox>
- </tabpanel>
+ <vbox id="zotero-item-pane" zotero-persist="width">
+ <!-- Trash -->
+ <!-- TODO: localize -->
+ <!-- TODO: Make look less awful -->
+ <button id="zotero-item-restore-button" label="Restore to Library"
+ oncommand="ZoteroPane_Local.restoreSelectedItems()" hidden="true"/>
- <tabpanel>
- <tagsbox id="zotero-editpane-tags" flex="1"/>
- </tabpanel>
+ <!-- Commons -->
+ <button id="zotero-item-show-original" label="Show Original"
+ oncommand="ZoteroPane_Local.showOriginalItem()" hidden="true"/>
- <tabpanel>
- <seealsobox id="zotero-editpane-related" flex="1"/>
- </tabpanel>
- </tabpanels>
+ <deck id="zotero-item-pane-content" selectedIndex="0" flex="1">
+ <!-- Center label (for zero or multiple item selection) -->
+ <vbox pack="center" align="center">
+ <label id="zotero-item-pane-message"/>
+ </vbox>
+
+ <!-- Regular item -->
+ <tabbox id="zotero-view-tabbox" flex="1" onselect="if (!ZoteroPane_Local.collectionsView.selection || event.originalTarget.localName != 'tabpanels') { return; }; ZoteroItemPane.viewItem(ZoteroPane_Local.getSelectedItems()[0], ZoteroPane_Local.collectionsView.editable ? 'edit' : 'view', this.selectedIndex)">
+ <tabs>
+ <tab label="&zotero.tabs.info.label;"/>
+ <tab label="&zotero.tabs.notes.label;"/>
+ <tab label="&zotero.tabs.tags.label;"/>
+ <tab label="&zotero.tabs.related.label;"/>
+ </tabs>
+ <tabpanels id="zotero-view-item" flex="1">
+ <tabpanel>
+ <zoteroitembox id="zotero-editpane-item-box" flex="1"/>
+ </tabpanel>
+
+ <tabpanel flex="1" orient="vertical">
+ <vbox flex="1">
+ <hbox align="center">
+ <label id="zotero-editpane-notes-label"/>
+ <button id="zotero-editpane-notes-add" label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote(event.shiftKey);"/>
+ </hbox>
+ <grid flex="1">
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows id="zotero-editpane-dynamic-notes" flex="1"/>
+ </grid>
+ </vbox>
+ </tabpanel>
+
+ <tabpanel>
+ <tagsbox id="zotero-editpane-tags" flex="1"/>
+ </tabpanel>
+
+ <tabpanel>
+ <seealsobox id="zotero-editpane-related" flex="1"/>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+
+ <!-- Note item -->
+ <groupbox id="zotero-view-note" flex="1">
+ <zoteronoteeditor id="zotero-note-editor" flex="1" notitle="1"/>
+ <button id="zotero-view-note-button" label="&zotero.notes.separate;" oncommand="ZoteroPane_Local.openNoteWindow(this.getAttribute('noteID')); if(this.hasAttribute('sourceID')) ZoteroPane_Local.selectItem(this.getAttribute('sourceID'));"/>
+ </groupbox>
+
+ <!-- Attachment item -->
+ <zoteroattachmentbox id="zotero-attachment-box" flex="1"/>
+
+ <!-- Duplicate merging -->
+ <!-- TODO: localize -->
+ <vbox id="zotero-duplicates-merge-pane" flex="1">
+ <groupbox>
+ <button id="zotero-duplicates-merge-button" oncommand="Zotero_Duplicates_Pane.merge()"/>
+ </groupbox>
+
+ <groupbox id="zotero-duplicates-merge-version-select">
+ <description>Choose the version of the item to use as the master item:</description>
+ <hbox>
+ <listbox id="zotero-duplicates-merge-original-date" onselect="Zotero_Duplicates_Pane.setMaster(this.selectedIndex)"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox flex="1">
+ <description id="zotero-duplicates-merge-field-select">
+ Select fields to keep from other versions of the item:
+ </description>
+ <zoteroitembox id="zotero-duplicates-merge-item-box" flex="1"/>
+ </groupbox>
+ </vbox>
+ </deck>
+ </vbox>
</overlay>
\ No newline at end of file
diff --git 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
@@ -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()
}
}
+ // 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++;
+ }
+
// 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);
+ self._showItem(new Zotero.ItemGroup('unfiled', s), 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);
- newRows++;
+ 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;
}
- 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<len; i++) {
if (this.selection.isSelected(i)) {
var itemGroup = this._getItemAtRow(i);
- if (itemGroup.isLibrary()) {
- return 'L';
+ var id = itemGroup.id;
+ if (id) {
+ return id;
}
- else if (itemGroup.isCollection()) {
- return 'C' + itemGroup.ref.id;
- }
- else if (itemGroup.isSearch()) {
- return 'S' + itemGroup.ref.id;
- }
- else if (itemGroup.isTrash()) {
- return 'T';
- }
- else if (itemGroup.isGroup()) {
- return 'G' + itemGroup.ref.id;
+ else {
+ break;
}
}
}
@@ -954,49 +961,8 @@ Zotero.CollectionTreeView.prototype.saveSelection = function()
*/
Zotero.CollectionTreeView.prototype.rememberSelection = function(selection)
{
- if (!selection) {
- return;
- }
-
- var id = selection.substr(1);
- switch (selection.substr(0, 1)) {
- // Library
- case 'L':
- this.selection.select(0);
- break;
-
- // Collection
- case 'C':
- // This only selects the collection if it's still visible,
- // so we open the parent in notify()
- if (this._collectionRowMap[id] != undefined) {
- this.selection.select(this._collectionRowMap[id]);
- }
- break;
-
- // Saved search
- case 'S':
- if (this._searchRowMap[id] != undefined) {
- this.selection.select(this._searchRowMap[id]);
- }
- break;
-
- // Trash
- case 'T':
- if (this._getItemAtRow(this.rowCount-1).isTrash()){
- this.selection.select(this.rowCount-1);
- }
- else {
- this.selection.select(0);
- }
- break;
-
- // Group
- case 'G':
- if (this._groupRowMap[id] != undefined) {
- this.selection.select(this._groupRowMap[i]);
- }
- break;
+ if (selection && this._rowMap[selection] != 'undefined') {
+ this.selection.select(this._rowMap[selection]);
}
}
@@ -1015,26 +981,22 @@ Zotero.CollectionTreeView.prototype.getSelectedCollection = function(asID) {
-/*
- * Creates hash map of collection and search ids to row indexes
- * e.g., var rowForID = this._collectionRowMap[]
+/**
+ * Creates mapping of item group ids to tree rows
*/
Zotero.CollectionTreeView.prototype._refreshHashMap = function()
{
this._collectionRowMap = [];
- this._searchRowMap = [];
- this._groupRowMap = [];
- for(var i=0; i < this.rowCount; i++){
+ this._rowMap = [];
+ for(var i = 0, len = this.rowCount; i < len; i++) {
var itemGroup = this._getItemAtRow(i);
- if (itemGroup.isCollection(i)) {
+
+ // Collections get special treatment for now
+ if (itemGroup.isCollection()) {
this._collectionRowMap[itemGroup.ref.id] = i;
}
- else if (itemGroup.isSearch(i)) {
- this._searchRowMap[itemGroup.ref.id] = i;
- }
- else if (itemGroup.isGroup(i)) {
- this._groupRowMap[itemGroup.ref.id] = i;
- }
+
+ this._rowMap[itemGroup.id] = i;
}
}
@@ -1663,7 +1625,36 @@ Zotero.ItemGroup = function(type, ref)
this.ref = ref;
}
-Zotero.ItemGroup.prototype.isLibrary = function(includeGlobal)
+
+Zotero.ItemGroup.prototype.__defineGetter__('id', function () {
+ switch (this.type) {
+ case 'library':
+ return 'L';
+
+ case 'collection':
+ return 'C' + this.ref.id;
+
+ case 'search':
+ return 'S' + this.ref.id;
+
+ case 'duplicates':
+ return 'D' + (this.ref.libraryID ? this.ref.libraryID : 0);
+
+ case 'unfiled':
+ return 'U' + (this.ref.libraryID ? this.ref.libraryID : 0);
+
+ case 'trash':
+ return 'T';
+
+ case 'group':
+ return 'G' + this.ref.id;
+
+ default:
+ return '';
+ }
+});
+
+Zotero.ItemGroup.prototype.isLibrary = function (includeGlobal)
{
if (includeGlobal) {
return this.type == 'library' || this.type == 'group';
@@ -1681,14 +1672,12 @@ Zotero.ItemGroup.prototype.isSearch = function()
return this.type == 'search';
}
-Zotero.ItemGroup.prototype.isShare = function()
-{
- return this.type == 'share';
+Zotero.ItemGroup.prototype.isDuplicates = function () {
+ return this.type == 'duplicates';
}
-Zotero.ItemGroup.prototype.isBucket = function()
-{
- return this.type == 'bucket';
+Zotero.ItemGroup.prototype.isUnfiled = function () {
+ return this.type == 'unfiled';
}
Zotero.ItemGroup.prototype.isTrash = function()
@@ -1696,18 +1685,29 @@ Zotero.ItemGroup.prototype.isTrash = function()
return this.type == 'trash';
}
-Zotero.ItemGroup.prototype.isGroup = function() {
- return this.type == 'group';
-}
-
Zotero.ItemGroup.prototype.isHeader = function () {
return this.type == 'header';
}
+Zotero.ItemGroup.prototype.isGroup = function() {
+ return this.type == 'group';
+}
+
Zotero.ItemGroup.prototype.isSeparator = function () {
return this.type == 'separator';
}
+Zotero.ItemGroup.prototype.isBucket = function()
+{
+ return this.type == 'bucket';
+}
+
+Zotero.ItemGroup.prototype.isShare = function()
+{
+ return this.type == 'share';
+}
+
+
// Special
Zotero.ItemGroup.prototype.isWithinGroup = function () {
@@ -1725,7 +1725,7 @@ Zotero.ItemGroup.prototype.__defineGetter__('editable', function () {
if (this.isGroup()) {
return this.ref.editable;
}
- if (this.isCollection() || this.isSearch()) {
+ if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled()) {
var type = Zotero.Libraries.getType(libraryID);
if (type == 'group') {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
@@ -1763,42 +1763,30 @@ Zotero.ItemGroup.prototype.__defineGetter__('filesEditable', function () {
Zotero.ItemGroup.prototype.getName = function()
{
switch (this.type) {
- case 'collection':
- return this.ref.name;
-
case 'library':
return Zotero.getString('pane.collections.library');
- case 'search':
- return this.ref.name;
-
- case 'share':
- return this.ref.name;
-
- case 'bucket':
- return this.ref.name;
-
case 'trash':
return Zotero.getString('pane.collections.trash');
- case 'group':
- return this.ref.name;
-
case 'header':
return this.ref.label;
- default:
+ case 'separator':
return "";
+
+ default:
+ return this.ref.name;
}
}
-Zotero.ItemGroup.prototype.getChildItems = function()
+Zotero.ItemGroup.prototype.getItems = function()
{
switch (this.type) {
// Fake results if this is a shared library
case 'share':
return this.ref.getAll();
-
+
case 'bucket':
return this.ref.getItems();
@@ -1823,16 +1811,7 @@ Zotero.ItemGroup.prototype.getChildItems = function()
}
try {
- var ids;
- if (this.showDuplicates) {
- var duplicates = new Zotero.Duplicate;
- var tmpTable = s.search(true);
- ids = duplicates.getIDs(tmpTable);
- Zotero.DB.query("DROP TABLE " + tmpTable);
- }
- else {
- ids = s.search();
- }
+ var ids = s.search();
}
catch (e) {
Zotero.DB.rollbackAllTransactions();
@@ -1853,33 +1832,38 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
var includeScopeChildren = false;
// Create/load the inner search
- var s = new Zotero.Search();
- if (this.isLibrary()) {
- s.addCondition('libraryID', 'is', null);
- s.addCondition('noChildren', 'true');
- includeScopeChildren = true;
- }
- else if (this.isGroup()) {
- s.addCondition('libraryID', 'is', this.ref.libraryID);
- s.addCondition('noChildren', 'true');
- includeScopeChildren = true;
- }
- else if (this.isCollection()) {
- s.addCondition('noChildren', 'true');
- s.addCondition('collectionID', 'is', this.ref.id);
- if (Zotero.Prefs.get('recursiveCollections')) {
- s.addCondition('recursive', 'true');
- }
- includeScopeChildren = true;
- }
- else if (this.isTrash()) {
- s.addCondition('deleted', 'true');
- }
- else if (this.isSearch()) {
+ if (this.ref instanceof Zotero.Search) {
var s = this.ref;
}
+ else if (this.isDuplicates()) {
+ var s = this.ref.getSearchObject();
+ }
else {
- throw ('Invalid search mode in Zotero.ItemGroup.getSearchObject()');
+ var s = new Zotero.Search();
+ if (this.isLibrary()) {
+ s.addCondition('libraryID', 'is', null);
+ s.addCondition('noChildren', 'true');
+ includeScopeChildren = true;
+ }
+ else if (this.isGroup()) {
+ s.addCondition('libraryID', 'is', this.ref.libraryID);
+ s.addCondition('noChildren', 'true');
+ includeScopeChildren = true;
+ }
+ else if (this.isCollection()) {
+ s.addCondition('noChildren', 'true');
+ s.addCondition('collectionID', 'is', this.ref.id);
+ if (Zotero.Prefs.get('recursiveCollections')) {
+ s.addCondition('recursive', 'true');
+ }
+ includeScopeChildren = true;
+ }
+ else if (this.isTrash()) {
+ s.addCondition('deleted', 'true');
+ }
+ else {
+ throw ('Invalid search mode in Zotero.ItemGroup.getSearchObject()');
+ }
}
// Create the outer (filter) search
@@ -1914,14 +1898,13 @@ Zotero.ItemGroup.prototype.getChildTags = function() {
// TODO: implement?
case 'share':
return false;
-
+
case 'bucket':
return false;
case 'header':
return false;
}
-
var s = this.getSearchObject();
return Zotero.Tags.getAllWithinSearch(s);
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -106,10 +106,10 @@ Zotero.Item.prototype.__defineGetter__('dateAdded', function () { return this.ge
Zotero.Item.prototype.__defineGetter__('dateModified', function () { return this.getField('dateModified'); });
Zotero.Item.prototype.__defineGetter__('firstCreator', function () { return this.getField('firstCreator'); });
-Zotero.Item.prototype.__defineGetter__('relatedItems', function () { var ids = this._getRelatedItems(true); return ids ? ids : []; });
+Zotero.Item.prototype.__defineGetter__('relatedItems', function () { var ids = this._getRelatedItems(true); return ids; });
Zotero.Item.prototype.__defineSetter__('relatedItems', function (arr) { this._setRelatedItems(arr); });
-Zotero.Item.prototype.__defineGetter__('relatedItemsReverse', function () { var ids = this._getRelatedItemsReverse(); return ids ? ids : []; });
-Zotero.Item.prototype.__defineGetter__('relatedItemsBidirectional', function () { var ids = this._getRelatedItemsBidirectional(); return ids ? ids : []; });
+Zotero.Item.prototype.__defineGetter__('relatedItemsReverse', function () { var ids = this._getRelatedItemsReverse(); return ids; });
+Zotero.Item.prototype.__defineGetter__('relatedItemsBidirectional', function () { var ids = this._getRelatedItemsBidirectional(); return ids; });
Zotero.Item.prototype.getID = function() {
@@ -608,8 +608,10 @@ Zotero.Item.prototype.getFieldsNotInType = function (itemTypeID, allowBaseConver
* Return an array of collectionIDs for all collections the item belongs to
**/
Zotero.Item.prototype.getCollections = function() {
- return Zotero.DB.columnQuery("SELECT collectionID FROM collectionItems "
- + "WHERE itemID=" + this.id);
+ var ids = Zotero.DB.columnQuery(
+ "SELECT collectionID FROM collectionItems WHERE itemID=?", this.id
+ );
+ return ids ? ids : [];
}
@@ -1105,7 +1107,7 @@ Zotero.Item.prototype.addRelatedItem = function (itemID) {
}
var current = this._getRelatedItems(true);
- if (current && current.indexOf(itemID) != -1) {
+ if (current.indexOf(itemID) != -1) {
Zotero.debug("Item " + this.id + " already related to item "
+ itemID + " in Zotero.Item.addItem()");
return false;
@@ -1139,11 +1141,9 @@ Zotero.Item.prototype.removeRelatedItem = function (itemID) {
itemID = parsedInt;
var current = this._getRelatedItems(true);
- if (current) {
- var index = current.indexOf(itemID);
- }
+ var index = current.indexOf(itemID);
- if (!current || index == -1) {
+ if (index == -1) {
Zotero.debug("Item " + this.id + " isn't related to item "
+ itemID + " in Zotero.Item.removeRelatedItem()");
return false;
@@ -1487,9 +1487,6 @@ Zotero.Item.prototype.save = function() {
var removed = [];
var newids = [];
var currentIDs = this._getRelatedItems(true);
- if (!currentIDs) {
- currentIDs = [];
- }
if (this._previousData && this._previousData.related) {
for each(var id in this._previousData.related) {
@@ -1763,6 +1760,16 @@ Zotero.Item.prototype.save = function() {
sql = "REPLACE INTO deletedItems (itemID) VALUES (?)";
}
else {
+ // If undeleting, remove any merge-tracking relations
+ var relations = Zotero.Relations.getByURIs(
+ Zotero.URI.getItemURI(this),
+ Zotero.Relations.deletedItemPredicate,
+ false
+ );
+ for each(var relation in relations) {
+ relation.erase();
+ }
+
sql = "DELETE FROM deletedItems WHERE itemID=?";
}
Zotero.DB.query(sql, this.id);
@@ -1952,9 +1959,6 @@ Zotero.Item.prototype.save = function() {
var removed = [];
var newids = [];
var currentIDs = this._getRelatedItems(true);
- if (!currentIDs) {
- currentIDs = [];
- }
if (this._previousData && this._previousData.related) {
for each(var id in this._previousData.related) {
@@ -2418,7 +2422,7 @@ Zotero.Item.prototype.setNote = function(text) {
* Returns child notes of this item
*
* @param {Boolean} includeTrashed Include trashed child items
- * @return {Integer[]} Array of itemIDs, or FALSE if none
+ * @return {Integer[]} Array of itemIDs
*/
Zotero.Item.prototype.getNotes = function(includeTrashed) {
if (this.isNote()) {
@@ -2442,7 +2446,7 @@ Zotero.Item.prototype.getNotes = function(includeTrashed) {
var notes = Zotero.DB.query(sql, this.id);
if (!notes) {
- return false;
+ return [];
}
// Sort by title
@@ -3204,7 +3208,7 @@ Zotero.Item.prototype.__defineGetter__('attachmentText', function () {
* Returns child attachments of this item
*
* @param {Boolean} includeTrashed Include trashed child items
- * @return {Integer[]} Array of itemIDs, or FALSE if none
+ * @return {Integer[]} Array of itemIDs
*/
Zotero.Item.prototype.getAttachments = function(includeTrashed) {
if (this.isAttachment()) {
@@ -3232,7 +3236,7 @@ Zotero.Item.prototype.getAttachments = function(includeTrashed) {
var attachments = Zotero.DB.query(sql, this.id);
if (!attachments) {
- return false;
+ return [];
}
// Sort by title
@@ -3322,7 +3326,7 @@ Zotero.Item.prototype.addTag = function(name, type) {
var matchingTags = Zotero.Tags.getIDs(name, this.libraryID);
var itemTags = this.getTags();
- if (matchingTags && itemTags) {
+ if (matchingTags && itemTags.length) {
for each(var id in matchingTags) {
if (itemTags.indexOf(id) != -1) {
var tag = Zotero.Tags.get(id);
@@ -3422,17 +3426,17 @@ Zotero.Item.prototype.hasTags = function(tagIDs) {
/**
* Returns all tags assigned to an item
*
- * @return array Array of Zotero.Tag objects
+ * @return Array Array of Zotero.Tag objects
*/
Zotero.Item.prototype.getTags = function() {
if (!this.id) {
- return false;
+ return [];
}
var sql = "SELECT tagID, name FROM tags WHERE tagID IN "
+ "(SELECT tagID FROM itemTags WHERE itemID=?)";
var tags = Zotero.DB.query(sql, this.id);
if (!tags) {
- return false;
+ return [];
}
var collation = Zotero.getLocaleCollation();
@@ -3756,6 +3760,50 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) {
/**
+ * Compare multiple items against this item and return fields that differ
+ *
+ * Currently compares only item data, not primary fields
+ */
+Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) {
+ var thisData = this.serialize();
+
+ var alternatives = {};
+ var hasDiffs = false;
+
+ for each(var otherItem in otherItems) {
+ var diff = [];
+ var otherData = otherItem.serialize();
+ var numDiffs = Zotero.Items.diff(thisData, otherData, diff);
+
+ if (numDiffs) {
+ for (var field in diff[1].fields) {
+ if (ignoreFields && ignoreFields.indexOf(field) != -1) {
+ continue;
+ }
+
+ var value = diff[1].fields[field];
+
+ if (!alternatives[field]) {
+ hasDiffs = true;
+ alternatives[field] = [value];
+ }
+ else if (alternatives[field].indexOf(value) == -1) {
+ hasDiffs = true;
+ alternatives[field].push(value);
+ }
+ }
+ }
+ }
+
+ if (!hasDiffs) {
+ return false;
+ }
+
+ return alternatives;
+}
+
+
+/**
* Returns an unsaved copy of the item
*
* @param {Boolean} [includePrimary=false]
@@ -3781,13 +3829,14 @@ Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved) {
var sameLibrary = newItem.libraryID == this.libraryID;
}
else {
- var newItem = new Zotero.Item(itemTypeID);
+ var newItem = new Zotero.Item;
var sameLibrary = true;
if (includePrimary) {
newItem.id = this.id;
newItem.libraryID = this.libraryID;
newItem.key = this.key;
+ newItem.itemTypeID = itemTypeID;
for (var field in obj.primary) {
switch (field) {
case 'itemID':
@@ -3799,6 +3848,9 @@ Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved) {
newItem.setField(field, obj.primary[field]);
}
}
+ else {
+ newItem.setType(itemTypeID);
+ }
}
var changedFields = {};
@@ -4026,13 +4078,11 @@ Zotero.Item.prototype.erase = function() {
// Flag related items for notification
var relateds = this._getRelatedItemsBidirectional();
- if (relateds) {
- for each(var id in relateds) {
- var relatedItem = Zotero.Items.get(id);
- if (changedItems.indexOf(id) != -1) {
- changedItemsNotifierData[id] = { old: relatedItem.serialize() };
- changedItems.push(id);
- }
+ for each(var id in relateds) {
+ var relatedItem = Zotero.Items.get(id);
+ if (changedItems.indexOf(id) != -1) {
+ changedItemsNotifierData[id] = { old: relatedItem.serialize() };
+ changedItems.push(id);
}
}
@@ -4042,9 +4092,9 @@ Zotero.Item.prototype.erase = function() {
//Zotero.Fulltext.clearItemContent(this.id);
}
- // Remove relations
- var relation = Zotero.URI.getItemURI(this);
- Zotero.Relations.eraseByURIPrefix(relation);
+ // Remove relations (except for merge tracker)
+ var uri = Zotero.URI.getItemURI(this);
+ Zotero.Relations.eraseByURIPrefix(uri, [Zotero.Relations.deletedItemPredicate]);
Zotero.DB.query('DELETE FROM annotations WHERE itemID=?', this.id);
Zotero.DB.query('DELETE FROM highlights WHERE itemID=?', this.id);
@@ -4061,7 +4111,7 @@ Zotero.Item.prototype.erase = function() {
Zotero.DB.query('DELETE FROM itemSeeAlso WHERE linkedItemID=?', this.id);
var tags = this.getTags();
- if (tags) {
+ if (tags.length) {
var hasTags = true;
Zotero.DB.query('DELETE FROM itemTags WHERE itemID=?', this.id);
// DEBUG: Hack to reload linked items -- replace with something better
@@ -4236,19 +4286,14 @@ Zotero.Item.prototype.toArray = function (mode) {
arr.tags = [];
var tags = this.getTags();
- if (tags) {
- for (var i=0; i<tags.length; i++) {
- var tag = tags[i].serialize();
- tag.tag = tag.fields.name;
- tag.type = tag.fields.type;
- arr.tags.push(tag);
- }
+ for (var i=0, len=tags.length; i<len; i++) {
+ var tag = tags[i].serialize();
+ tag.tag = tag.fields.name;
+ tag.type = tag.fields.type;
+ arr.tags.push(tag);
}
arr.related = this._getRelatedItemsBidirectional();
- if (!arr.related) {
- arr.related = [];
- }
return arr;
}
@@ -4376,16 +4421,12 @@ Zotero.Item.prototype.serialize = function(mode) {
arr.tags = [];
var tags = this.getTags();
- if (tags) {
- for (var i=0; i<tags.length; i++) {
- arr.tags.push(tags[i].serialize());
- }
+ for (var i=0, len=tags.length; i<len; i++) {
+ arr.tags.push(tags[i].serialize());
}
- var related = this._getRelatedItems(true);
- var reverse = this._getRelatedItemsReverse();
- arr.related = related ? related : [];
- arr.relatedReverse = reverse ? reverse : [];
+ arr.related = this._getRelatedItems(true);
+ arr.relatedReverse = this._getRelatedItemsReverse();
return arr;
}
@@ -4502,7 +4543,7 @@ Zotero.Item.prototype._loadRelatedItems = function() {
* Returns related items this item point to
*
* @param bool asIDs Return as itemIDs
- * @return array Array of itemIDs, or FALSE if none
+ * @return array Array of itemIDs
*/
Zotero.Item.prototype._getRelatedItems = function (asIDs) {
if (!this._relatedItemsLoaded) {
@@ -4510,7 +4551,7 @@ Zotero.Item.prototype._getRelatedItems = function (asIDs) {
}
if (this._relatedItems.length == 0) {
- return false;
+ return [];
}
// Return itemIDs
@@ -4534,28 +4575,33 @@ Zotero.Item.prototype._getRelatedItems = function (asIDs) {
/**
* Returns related items that point to this item
*
- * @return array Array of itemIDs, or FALSE if none
+ * @return array Array of itemIDs
*/
Zotero.Item.prototype._getRelatedItemsReverse = function () {
if (!this.id) {
- return false;
+ return [];
}
var sql = "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?";
- return Zotero.DB.columnQuery(sql, this.id);
+ var ids = Zotero.DB.columnQuery(sql, this.id);
+ if (!ids) {
+ return [];
+ }
+ return ids;
}
/**
* Returns related items this item points to and that point to this item
*
- * @return array|bool Array of itemIDs, or false if none
+ * @return array Array of itemIDs
*/
Zotero.Item.prototype._getRelatedItemsBidirectional = function () {
var related = this._getRelatedItems(true);
var reverse = this._getRelatedItemsReverse();
- if (reverse) {
- if (!related) {
+
+ if (reverse.length) {
+ if (!related.length) {
return reverse;
}
@@ -4566,7 +4612,7 @@ Zotero.Item.prototype._getRelatedItemsBidirectional = function () {
}
}
else if (!related) {
- return false;
+ return [];
}
return related;
}
@@ -4582,9 +4628,6 @@ Zotero.Item.prototype._setRelatedItems = function (itemIDs) {
}
var currentIDs = this._getRelatedItems(true);
- if (!currentIDs) {
- currentIDs = [];
- }
var oldIDs = []; // children being kept
var newIDs = []; // new children
diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js
@@ -374,6 +374,68 @@ Zotero.Items = new function() {
}
+ this.merge = function (item, otherItems) {
+ Zotero.DB.beginTransaction();
+
+ var otherItemIDs = [];
+ var itemURI = Zotero.URI.getItemURI(item);
+
+ for each(var otherItem in otherItems) {
+ // Move child items to master
+ var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true));
+ for each(var id in ids) {
+ var attachment = Zotero.Items.get(id);
+
+ // TODO: Skip identical children?
+
+ attachment.setSource(item.id);
+ attachment.save();
+ }
+
+ // All other operations are additive only and do not affect the,
+ // old item, which will be put in the trash
+
+ // Add collections to master
+ var collectionIDs = otherItem.getCollections();
+ for each(var collectionID in collectionIDs) {
+ var collection = Zotero.Collections.get(collectionID);
+ collection.addItem(item.id);
+ }
+
+ // Add tags to master
+ var tags = otherItem.getTags();
+ for each(var tag in tags) {
+ item.addTagByID(tag.id);
+ }
+
+ // Related items
+ var relatedItems = otherItem.relatedItemsBidirectional;
+ Zotero.debug(item._getRelatedItems(true));
+ for each(var relatedItemID in relatedItems) {
+ item.addRelatedItem(relatedItemID);
+ }
+ item.save();
+
+ // Relations
+ Zotero.Relations.copyURIs(
+ item.libraryID,
+ Zotero.URI.getItemURI(item),
+ Zotero.URI.getItemURI(otherItem)
+ );
+
+ // Add relation to track merge
+ var otherItemURI = Zotero.URI.getItemURI(otherItem);
+ Zotero.Relations.add(item.libraryID, otherItemURI, Zotero.Relations.deletedItemPredicate, itemURI);
+
+ // Trash other item
+ otherItem.deleted = true;
+ otherItem.save();
+ }
+
+ Zotero.DB.commitTransaction();
+ }
+
+
this.trash = function (ids) {
ids = Zotero.flattenArguments(ids);
diff --git a/chrome/content/zotero/xpcom/data/relation.js b/chrome/content/zotero/xpcom/data/relation.js
@@ -176,6 +176,27 @@ Zotero.Relation.prototype.save = function () {
}
+Zotero.Relation.prototype.erase = function () {
+ if (!this.id) {
+ throw ("ID not set in Zotero.Relation.erase()");
+ }
+
+ Zotero.DB.beginTransaction();
+
+ var deleteData = {};
+ deleteData[this.id] = {
+ old: this.serialize()
+ }
+
+ var sql = "DELETE FROM relations WHERE ROWID=?";
+ Zotero.DB.query(sql, [this.id]);
+
+ Zotero.DB.commitTransaction();
+
+ Zotero.Notifier.trigger('delete', 'relation', [this.id], deleteData);
+}
+
+
Zotero.Relation.prototype.toXML = function () {
var xml = <relation/>;
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
@@ -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) {
- Zotero.DB.beginTransaction();
-
- var sql = "DELETE FROM relations WHERE ROWID=?";
- Zotero.DB.query(sql, [id]);
-
- // TODO: log to syncDeleteLog
+ /**
+ * 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);
+ }
- Zotero.DB.commitTransaction();
+ var rels = this.getByURIs(false, false, fromURI);
+ for each(var rel in rels) {
+ this.add(libraryID, rel.subject, rel.predicate, toURI);
+ }
}
- this.eraseByURIPrefix = function (prefix) {
+ /**
+ * @param {String} prefix
+ * @param {String[]} ignorePredicates
+ */
+ this.eraseByURIPrefix = function (prefix, ignorePredicates) {
+ Zotero.DB.beginTransaction();
+
prefix = prefix + '%';
- var sql = "DELETE FROM relations WHERE subject LIKE ? OR object LIKE ?";
- Zotero.DB.query(sql, [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);
+
+ for each(var id in ids) {
+ var relation = this.get(id);
+ relation.erase();
+ }
+
+ Zotero.DB.commitTransaction();
}
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
@@ -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 <http://www.gnu.org/licenses/>.
-
- ***** 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
@@ -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 <http://www.gnu.org/licenses/>.
+
+ ***** 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
@@ -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
@@ -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);
+ // 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;
+ }
+ var key = String.fromCharCode(event.which);
if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
- obj.expandAllRows(treebox);
+ obj.expandAllRows();
+ event.preventDefault();
return;
}
- else if (key == '-' && !(event.shiftKey || event.ctrlKey ||
- event.altKey || event.metaKey)) {
- obj.collapseAllRows(treebox);
+ 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,40 +357,50 @@ 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<len; i++)
- {
- if (action == 'delete' || action == 'trash' ||
- !itemGroup.ref.hasItem(ids[i])) {
- // Row might already be gone (e.g. if this is a child and
- // 'modify' was sent to parent)
- if (this._itemRowMap[ids[i]] != undefined) {
- rows.push(this._itemRowMap[ids[i]]);
+ // On a delete in duplicates mode, just refresh rather than figuring
+ // out what to remove
+ if (itemGroup.isDuplicates()) {
+ previousRow = this._itemRowMap[ids[0]];
+ this.refresh();
+ madeChanges = true;
+ sort = true;
+ }
+ else {
+ // 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<len; i++) {
+ if (action == 'delete' || action == 'trash' ||
+ !itemGroup.ref.hasItem(ids[i])) {
+ // Row might already be gone (e.g. if this is a child and
+ // 'modify' was sent to parent)
+ if (this._itemRowMap[ids[i]] != undefined) {
+ rows.push(this._itemRowMap[ids[i]]);
+ }
}
}
- }
-
- if(rows.length > 0)
- {
- rows.sort(function(a,b) { return a-b });
- for(var i=0, len=rows.length; i<len; i++)
- {
- var row = rows[i];
- if(row != null)
+ if (rows.length > 0) {
+ rows.sort(function(a,b) { return a-b });
+
+ for(var i=0, len=rows.length; i<len; i++)
{
- this._hideItem(row-i);
- this._treebox.rowCountChanged(row-i,-1);
+ var row = rows[i];
+ if(row != null)
+ {
+ this._hideItem(row-i);
+ this._treebox.rowCountChanged(row-i,-1);
+ }
}
+
+ madeChanges = true;
+ sort = true;
}
-
- madeChanges = true;
- sort = true;
}
}
else if (action == 'modify')
@@ -435,6 +466,8 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
if (item.deleted) {
continue;
}
+
+ // Otherwise the item has to be added
if(item.isRegularItem() || !item.getSource())
{
//most likely, the note or attachment's parent was removed.
@@ -580,7 +613,9 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
}
else
{
- var previousRow = this._itemRowMap[ids[0]];
+ if (previousRow === false) {
+ previousRow = this._itemRowMap[ids[0]];
+ }
if (sort) {
this.sort(typeof sort == 'number' ? sort : false);
@@ -589,14 +624,25 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
this._refreshHashMap();
}
- // On delete, select item at previous position
- if (action == 'delete' || action == 'remove') {
- if (this._dataItems[previousRow]) {
- this.selection.select(previousRow);
+ // On removal of a row, select item at previous position
+ if (action == 'remove' || action == 'trash' || action == 'delete') {
+ // In duplicates view, select the next set on delete
+ if (itemGroup.isDuplicates()) {
+ if (this._dataItems[previousRow]) {
+ // Mirror ZoteroPane.onTreeMouseDown behavior
+ var itemID = this._dataItems[previousRow].ref.id;
+ var setItemIDs = itemGroup.ref.getSetItemsByItemID(itemID);
+ this.selectItems(setItemIDs);
+ }
}
- // If no item at previous position, select last item in list
- else if (this._dataItems[this._dataItems.length - 1]) {
- this.selection.select(this._dataItems.length - 1);
+ else {
+ if (this._dataItems[previousRow]) {
+ this.selection.select(previousRow);
+ }
+ // If no item at previous position, select last item in list
+ else if (this._dataItems[this._dataItems.length - 1]) {
+ this.selection.select(this._dataItems.length - 1);
+ }
}
}
else {
@@ -631,7 +677,6 @@ Zotero.ItemTreeView.prototype.unregister = function()
////////////////////////////////////////////////////////////////////////////////
///
/// nsITreeView functions
-/// http://www.xulplanet.com/references/xpcomref/ifaces/nsITreeView.html
///
////////////////////////////////////////////////////////////////////////////////
@@ -1378,6 +1423,41 @@ Zotero.ItemTreeView.prototype.selectItem = function(id, expand, noRecurse)
return true;
}
+
+/**
+ * Select multiple top-level items
+ *
+ * @param {Integer[]} ids An array of itemIDs
+ */
+Zotero.ItemTreeView.prototype.selectItems = function(ids) {
+ if (ids.length == 0) {
+ return;
+ }
+
+ var rows = [];
+ for each(var id in ids) {
+ rows.push(this._itemRowMap[id]);
+ }
+ rows.sort(function (a, b) {
+ return a - b;
+ });
+
+ this.selection.clearSelection();
+
+ this.selection.selectEventsSuppressed = true;
+
+ var lastStart = 0;
+ for (var i = 0, len = rows.length; i < len; i++) {
+ if (i == len - 1 || rows[i + 1] != rows[i] + 1) {
+ this.selection.rangedSelect(rows[lastStart], rows[i], true);
+ lastStart = i + 1;
+ }
+ }
+
+ this.selection.selectEventsSuppressed = false;
+}
+
+
/*
* Return an array of Item objects for selected items
*
@@ -1702,7 +1782,7 @@ Zotero.ItemTreeView.prototype.rememberFirstRow = function(firstRow) {
}
-Zotero.ItemTreeView.prototype.expandAllRows = function(treebox) {
+Zotero.ItemTreeView.prototype.expandAllRows = function() {
this._treebox.beginUpdateBatch();
for (var i=0; i<this.rowCount; i++) {
if (this.isContainer(i) && !this.isContainerOpen(i)) {
@@ -1714,7 +1794,7 @@ Zotero.ItemTreeView.prototype.expandAllRows = function(treebox) {
}
-Zotero.ItemTreeView.prototype.collapseAllRows = function(treebox) {
+Zotero.ItemTreeView.prototype.collapseAllRows = function() {
this._treebox.beginUpdateBatch();
for (var i=0; i<this.rowCount; i++) {
if (this.isContainer(i) && this.isContainerOpen(i)) {
@@ -1726,6 +1806,38 @@ Zotero.ItemTreeView.prototype.collapseAllRows = function(treebox) {
}
+Zotero.ItemTreeView.prototype.expandSelectedRows = function() {
+ var start = {}, end = {};
+ this._treebox.beginUpdateBatch();
+ for (var i = 0, len = this.selection.getRangeCount(); i<len; i++) {
+ this.selection.getRangeAt(i, start, end);
+ for (var j = start.value; j <= end.value; j++) {
+ if (this.isContainer(j) && !this.isContainerOpen(j)) {
+ this.toggleOpenState(j, true);
+ }
+ }
+ }
+ this._refreshHashMap();
+ this._treebox.endUpdateBatch();
+}
+
+
+Zotero.ItemTreeView.prototype.collapseSelectedRows = function() {
+ var start = {}, end = {};
+ this._treebox.beginUpdateBatch();
+ for (var i = 0, len = this.selection.getRangeCount(); i<len; i++) {
+ this.selection.getRangeAt(i, start, end);
+ for (var j = start.value; j <= end.value; j++) {
+ if (this.isContainer(j) && this.isContainerOpen(j)) {
+ this.toggleOpenState(j, true);
+ }
+ }
+ }
+ this._refreshHashMap();
+ this._treebox.endUpdateBatch();
+}
+
+
Zotero.ItemTreeView.prototype.getVisibleFields = function() {
var columns = [];
for (var i=0, len=this._treebox.columns.count; i<len; i++) {
diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js
@@ -28,7 +28,7 @@ Zotero.Notifier = new function(){
var _disabled = false;
var _types = [
'collection', 'creator', 'search', 'share', 'share-items', 'item',
- 'collection-item', 'item-tag', 'tag', 'group', 'bucket'
+ 'collection-item', 'item-tag', 'tag', 'group', 'bucket', 'relation'
];
var _inTransaction;
var _locked = false;
@@ -90,7 +90,7 @@ Zotero.Notifier = new function(){
*
* event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
* 'remove' (ci, it), 'refresh', 'redraw', 'trash'
- * type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag', 'group'
+ * type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag', 'group', 'relation'
* ids - single id or array of ids
*
* Notes:
@@ -152,7 +152,7 @@ Zotero.Notifier = new function(){
}
for (var i in _observers.items){
- Zotero.debug("Calling notify() on observer with hash '" + i + "'", 4);
+ Zotero.debug("Calling notify('" + event + "') on observer with hash '" + i + "'", 4);
// Find observers that handle notifications for this type (or all types)
if (!_observers.get(i).types || _observers.get(i).types.indexOf(type)!=-1){
// Catch exceptions so all observers get notified even if
diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js
@@ -984,7 +984,7 @@ Zotero.Search.prototype._buildQuery = function(){
var data = Zotero.SearchConditions.get(this._conditions[i]['condition']);
// Has a table (or 'savedSearch', which doesn't have a table but isn't special)
- if (data.table || data.name == 'savedSearch') {
+ if (data.table || data.name == 'savedSearch' || data.name == 'tempTable') {
conditions.push({
name: data['name'],
alias: data['name']!=this._conditions[i]['condition']
@@ -1283,6 +1283,14 @@ Zotero.Search.prototype._buildQuery = function(){
openParens++;
break;
+ case 'tempTable':
+ if (!condition.value.match(/^[a-zA-Z0-9]+$/)) {
+ throw ("Invalid temp table '" + condition.value + "'");
+ }
+ condSQL += "itemID IN (SELECT id FROM " + condition.value + ")";
+ skipOperators = true;
+ break;
+
// For quicksearch blocks
case 'blockStart':
case 'blockEnd':
@@ -2142,6 +2150,13 @@ Zotero.SearchConditions = new function(){
doesNotContain: true
},
special: false
+ },
+
+ {
+ name: 'tempTable',
+ operators: {
+ is: true
+ }
}
];
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
@@ -223,6 +223,10 @@ Zotero.Sync = new function() {
function _loadObjectTypes() {
+ // TEMP: Take this out once system.sql > 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
@@ -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<len; i++) {
+ str = str.replace(map[i].letters, map[i].base);
+ }
+
+ if (!lowercaseOnly) {
+ var map = this._diacriticsRemovalMap.uppercase;
+ for (var i=0, len=map.length; i<len; i++) {
+ str = str.replace(map[i].letters, map[i].base);
+ }
+ }
+
+ return str;
+ },
+
+ "_diacriticsRemovalMap": {
+ uppercase: [
+ {'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},
+ {'base':'AA','letters':/[\uA732]/g},
+ {'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g},
+ {'base':'AO','letters':/[\uA734]/g},
+ {'base':'AU','letters':/[\uA736]/g},
+ {'base':'AV','letters':/[\uA738\uA73A]/g},
+ {'base':'AY','letters':/[\uA73C]/g},
+ {'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},
+ {'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},
+ {'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},
+ {'base':'DZ','letters':/[\u01F1\u01C4]/g},
+ {'base':'Dz','letters':/[\u01F2\u01C5]/g},
+ {'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},
+ {'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},
+ {'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},
+ {'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},
+ {'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},
+ {'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g},
+ {'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},
+ {'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},
+ {'base':'LJ','letters':/[\u01C7]/g},
+ {'base':'Lj','letters':/[\u01C8]/g},
+ {'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},
+ {'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},
+ {'base':'NJ','letters':/[\u01CA]/g},
+ {'base':'Nj','letters':/[\u01CB]/g},
+ {'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},
+ {'base':'OI','letters':/[\u01A2]/g},
+ {'base':'OO','letters':/[\uA74E]/g},
+ {'base':'OU','letters':/[\u0222]/g},
+ {'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},
+ {'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},
+ {'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},
+ {'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},
+ {'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},
+ {'base':'TZ','letters':/[\uA728]/g},
+ {'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},
+ {'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},
+ {'base':'VY','letters':/[\uA760]/g},
+ {'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},
+ {'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},
+ {'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},
+ {'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},
+ ],
+
+ lowercase: [
+ {'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},
+ {'base':'aa','letters':/[\uA733]/g},
+ {'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g},
+ {'base':'ao','letters':/[\uA735]/g},
+ {'base':'au','letters':/[\uA737]/g},
+ {'base':'av','letters':/[\uA739\uA73B]/g},
+ {'base':'ay','letters':/[\uA73D]/g},
+ {'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},
+ {'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},
+ {'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},
+ {'base':'dz','letters':/[\u01F3\u01C6]/g},
+ {'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},
+ {'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},
+ {'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},
+ {'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},
+ {'base':'hv','letters':/[\u0195]/g},
+ {'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},
+ {'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},
+ {'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},
+ {'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},
+ {'base':'lj','letters':/[\u01C9]/g},
+ {'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},
+ {'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},
+ {'base':'nj','letters':/[\u01CC]/g},
+ {'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},
+ {'base':'oi','letters':/[\u01A3]/g},
+ {'base':'ou','letters':/[\u0223]/g},
+ {'base':'oo','letters':/[\uA74F]/g},
+ {'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},
+ {'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},
+ {'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},
+ {'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},
+ {'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},
+ {'base':'tz','letters':/[\uA729]/g},
+ {'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},
+ {'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},
+ {'base':'vy','letters':/[\uA761]/g},
+ {'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},
+ {'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},
+ {'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},
+ {'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}
+ ]
+ },
+
/**
* Run sets of data through multiple asynchronous callbacks
*
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
@@ -53,7 +53,6 @@ var ZoteroPane = new function()
this.clearTagSelection = clearTagSelection;
this.updateTagFilter = updateTagFilter;
this.onCollectionSelected = onCollectionSelected;
- this.itemSelected = itemSelected;
this.reindexItem = reindexItem;
this.duplicateSelectedItem = duplicateSelectedItem;
this.editSelectedCollection = editSelectedCollection;
@@ -93,9 +92,6 @@ var ZoteroPane = new function()
var titlebarcolorState, titleState, observerService;
var _reloadFunctions = [];
- // Also needs to be changed in collectionTreeView.js
- var _lastViewedFolderRE = /^(?:(C|S|G)([0-9]+)|L)$/;
-
/**
* Called when the window containing Zotero pane is open
*/
@@ -167,6 +163,7 @@ var ZoteroPane = new function()
var itemsTree = document.getElementById('zotero-items-tree');
itemsTree.controllers.appendController(new Zotero.ItemTreeCommandController(itemsTree));
+ itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true);
itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true);
var menu = document.getElementById("contentAreaContextMenu");
@@ -249,10 +246,6 @@ var ZoteroPane = new function()
sep.nextSibling.nextSibling.hidden = false;
sep.nextSibling.nextSibling.nextSibling.hidden = false;
}
-
- if (Zotero.Prefs.get('debugShowDuplicates')) {
- document.getElementById('zotero-tb-actions-showDuplicates').hidden = false;
- }
}
@@ -793,9 +786,25 @@ var ZoteroPane = new function()
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
}
- this.setUnfiled = function (libraryID, show) {
+
+ this.setVirtual = function (libraryID, mode, show) {
+ switch (mode) {
+ case 'duplicates':
+ var prefKey = 'duplicateLibraries';
+ var lastViewedFolderID = 'D' + (libraryID ? libraryID : 0);
+ break;
+
+ case 'unfiled':
+ var prefKey = 'unfiledLibraries';
+ var lastViewedFolderID = 'U' + (libraryID ? libraryID : 0);
+ break;
+
+ default:
+ throw ("Invalid virtual mode '" + mode + "' in ZoteroPane.setVirtual()");
+ }
+
try {
- var ids = Zotero.Prefs.get('unfiledLibraries').split(',');
+ var ids = Zotero.Prefs.get(prefKey).split(',');
}
catch (e) {
var ids = [];
@@ -829,11 +838,10 @@ var ZoteroPane = new function()
newids.sort();
- Zotero.Prefs.set('unfiledLibraries', newids.join());
+ Zotero.Prefs.set(prefKey, newids.join());
if (show) {
- // 'UNFILED' + '000' + libraryID
- Zotero.Prefs.set('lastViewedFolder', 'S' + '8634533000' + libraryID);
+ Zotero.Prefs.set('lastViewedFolder', lastViewedFolderID);
}
this.collectionsView.refresh();
@@ -843,6 +851,7 @@ var ZoteroPane = new function()
this.collectionsView.selection.select(row);
}
+
this.openLookupWindow = function () {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
@@ -1039,7 +1048,6 @@ var ZoteroPane = new function()
itemgroup.setSearch('');
itemgroup.setTags(getTagSelection());
- itemgroup.showDuplicates = false;
try {
Zotero.UnresponsiveScriptIndicator.disable();
@@ -1052,50 +1060,28 @@ var ZoteroPane = new function()
Zotero.UnresponsiveScriptIndicator.enable();
}
- if (itemgroup.isLibrary()) {
- Zotero.Prefs.set('lastViewedFolder', 'L');
- }
- if (itemgroup.isCollection()) {
- Zotero.Prefs.set('lastViewedFolder', 'C' + itemgroup.ref.id);
- }
- else if (itemgroup.isSearch()) {
- Zotero.Prefs.set('lastViewedFolder', 'S' + itemgroup.ref.id);
- }
- else if (itemgroup.isGroup()) {
- Zotero.Prefs.set('lastViewedFolder', 'G' + itemgroup.ref.id);
- }
+ Zotero.Prefs.set('lastViewedFolder', itemgroup.id);
}
- this.showDuplicates = function () {
- if (this.collectionsView.selection.count == 1 && this.collectionsView.selection.currentIndex != -1) {
- var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
- itemGroup.showDuplicates = true;
-
- try {
- Zotero.UnresponsiveScriptIndicator.disable();
- this.itemsView.refresh();
- }
- finally {
- Zotero.UnresponsiveScriptIndicator.enable();
- }
- }
- }
-
this.getItemGroup = function () {
return this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
}
-
- function itemSelected()
- {
+
+ this.itemSelected = function (event) {
if (!Zotero.stateCheck()) {
this.displayErrorMessage();
return;
}
+ // DEBUG: Is this actually possible?
+ if (!this.itemsView) {
+ Components.utils.reportError("this.itemsView is not defined in ZoteroPane.itemSelected()");
+ }
+
// Display restore button if items selected in Trash
- if (this.itemsView && this.itemsView.selection.count) {
+ if (this.itemsView.selection.count) {
document.getElementById('zotero-item-restore-button').hidden
= !this.itemsView._itemGroup.isTrash()
|| _nonDeletedItemsSelected(this.itemsView);
@@ -1111,32 +1097,34 @@ var ZoteroPane = new function()
document.getElementById('zotero-note-editor').save();
}
+ var itemGroup = this.getItemGroup();
+
// Single item selected
- if (this.itemsView && this.itemsView.selection.count == 1 && this.itemsView.selection.currentIndex != -1)
+ if (this.itemsView.selection.count == 1 && this.itemsView.selection.currentIndex != -1)
{
- var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex);
+ var item = this.itemsView.getSelectedItems()[0];
- if(item.ref.isNote()) {
+ if (item.isNote()) {
var noteEditor = document.getElementById('zotero-note-editor');
noteEditor.mode = this.collectionsView.editable ? 'edit' : 'view';
// If loading new or different note, disable undo while we repopulate the text field
// so Undo doesn't end up clearing the field. This also ensures that Undo doesn't
// undo content from another note into the current one.
- if (!noteEditor.item || noteEditor.item.id != item.ref.id) {
+ if (!noteEditor.item || noteEditor.item.id != item.id) {
noteEditor.disableUndo();
}
noteEditor.parent = null;
- noteEditor.item = item.ref;
+ noteEditor.item = item;
noteEditor.enableUndo();
var viewButton = document.getElementById('zotero-view-note-button');
if (this.collectionsView.editable) {
viewButton.hidden = false;
- viewButton.setAttribute('noteID', item.ref.id);
- if (item.ref.getSource()) {
- viewButton.setAttribute('sourceID', item.ref.getSource());
+ viewButton.setAttribute('noteID', item.id);
+ if (item.getSource()) {
+ viewButton.setAttribute('sourceID', item.getSource());
}
else {
viewButton.removeAttribute('sourceID');
@@ -1149,17 +1137,17 @@ var ZoteroPane = new function()
document.getElementById('zotero-item-pane-content').selectedIndex = 2;
}
- else if(item.ref.isAttachment()) {
+ else if (item.isAttachment()) {
var attachmentBox = document.getElementById('zotero-attachment-box');
attachmentBox.mode = this.collectionsView.editable ? 'edit' : 'view';
- attachmentBox.item = item.ref;
+ attachmentBox.item = item;
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
}
// Regular item
else {
- var isCommons = this.getItemGroup().isBucket();
+ var isCommons = itemGroup.isBucket();
document.getElementById('zotero-item-pane-content').selectedIndex = 1;
var tabBox = document.getElementById('zotero-view-tabbox');
@@ -1176,26 +1164,65 @@ var ZoteroPane = new function()
}
if (this.collectionsView.editable) {
- ZoteroItemPane.viewItem(item.ref, null, pane);
+ ZoteroItemPane.viewItem(item, null, pane);
tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
}
else {
- ZoteroItemPane.viewItem(item.ref, 'view', pane);
+ ZoteroItemPane.viewItem(item, 'view', pane);
tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
}
}
}
// Zero or multiple items selected
else {
- document.getElementById('zotero-item-pane-content').selectedIndex = 0;
+ var count = this.itemsView.selection.count;
- var label = document.getElementById('zotero-view-selected-label');
-
- if (this.itemsView && this.itemsView.selection.count) {
- label.value = Zotero.getString('pane.item.selected.multiple', this.itemsView.selection.count);
+ // Display duplicates merge interface in item pane
+ if (itemGroup.isDuplicates()) {
+ if (!itemGroup.editable) {
+ if (count) {
+ // TODO: localize
+ var msg = "Library write access is required to merge items.";
+ }
+ else {
+ var msg = Zotero.getString('pane.item.selected.zero');
+ }
+ this.setItemPaneMessage(msg);
+ }
+ else if (count) {
+ document.getElementById('zotero-item-pane-content').selectedIndex = 4;
+
+ // Load duplicates UI code
+ 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");
+ }
+
+ // On a Select All of more than a few items, display a row
+ // count instead of the usual item type mismatch error
+ var displayNumItemsOnTypeError = count > 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<items.length; i++) {
- if (canIndex && !Zotero.Fulltext.canReindex(items[i].id)) {
+
+ for each(var item in items) {
+ if (canMerge && !item.isRegularItem() || itemGroup.isDuplicates()) {
+ canMerge = false;
+ }
+
+ if (canIndex && !Zotero.Fulltext.canReindex(item.id)) {
canIndex = false;
}
- if (canRecognize && !Zotero_RecognizePDF.canRecognize(items[i])) {
+
+ if (canRecognize && !Zotero_RecognizePDF.canRecognize(item)) {
canRecognize = false;
}
- if (!canIndex && !canRecognize) {
- break;
+
+ // Show rename option only if all items are child attachments
+ if (canRename && (!item.isAttachment() || !item.getSource() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL)) {
+ canRename = false;
}
}
+
+ if (canMerge) {
+ show.push(m.mergeItems);
+ }
+
if (canIndex) {
show.push(m.reindexItem);
}
- else {
- hide.push(m.reindexItem);
- }
+
if (canRecognize) {
show.push(m.recognizePDF);
- hide.push(m.createParent);
}
else {
- hide.push(m.recognizePDF);
-
var canCreateParent = true;
- for (var i=0; i<items.length; i++) {
- if (!items[i].isTopLevelItem() || items[i].isRegularItem() || Zotero_RecognizePDF.canRecognize(items[i])) {
+ for each(var item in items) {
+ if (!item.isTopLevelItem() || !item.isAttachment() || Zotero_RecognizePDF.canRecognize(item)) {
canCreateParent = false;
break;
}
@@ -2172,35 +2225,16 @@ var ZoteroPane = new function()
if (canCreateParent) {
show.push(m.createParent);
}
- else {
- hide.push(m.createParent);
- }
}
- // If all items are child attachments, show rename option
- var canRename = true;
- for (var i=0; i<items.length; i++) {
- var item = items[i];
- // Same check as in rename function
- if (!item.isAttachment() || !item.getSource() || item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
- canRename = false;
- break;
- }
- }
if (canRename) {
show.push(m.renameAttachments);
}
- else {
- hide.push(m.renameAttachments);
- }
// Add in attachment separator
if (canCreateParent || canRecognize || canRename || canIndex) {
show.push(m.sep4);
}
- else {
- hide.push(m.sep4);
- }
// Block certain actions on files if no access and at least one item
// is an imported attachment
@@ -2217,10 +2251,6 @@ var ZoteroPane = new function()
var d = [m.deleteFromLibrary, m.createParent, m.renameAttachments];
for each(var val in d) {
disable.push(val);
- var index = enable.indexOf(val);
- if (index != -1) {
- enable.splice(index, 1);
- }
}
}
}
@@ -2229,7 +2259,7 @@ var ZoteroPane = new function()
// Single item selected
else
{
- var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex).ref;
+ var item = this.getSelectedItems()[0];
var itemID = item.id;
menu.setAttribute('itemID', itemID);
@@ -2237,39 +2267,29 @@ var ZoteroPane = new function()
if (!itemGroup.isLibrary() && !itemGroup.isWithinGroup()) {
show.push(m.showInLibrary, m.sep1);
}
- else {
- hide.push(m.showInLibrary, m.sep1);
+
+ // Disable actions in the trash
+ if (itemGroup.isTrash()) {
+ disable.push(m.deleteItem, m.deleteFromLibrary);
}
- if (item.isRegularItem())
- {
+ if (item.isRegularItem()) {
show.push(m.addNote, m.addAttachments, m.sep2);
}
- else
- {
- hide.push(m.addNote, m.addAttachments, m.sep2);
- }
if (item.isAttachment()) {
var showSep4 = false;
- hide.push(m.duplicateItem);
if (Zotero_RecognizePDF.canRecognize(item)) {
show.push(m.recognizePDF);
- hide.push(m.createParent);
showSep4 = true;
}
else {
- hide.push(m.recognizePDF);
-
// If not a PDF, allow parent item creation
if (item.isTopLevelItem()) {
show.push(m.createParent);
showSep4 = true;
}
- else {
- hide.push(m.createParent);
- }
}
// Attachment rename option
@@ -2277,16 +2297,6 @@ var ZoteroPane = new function()
show.push(m.renameAttachments);
showSep4 = true;
}
- else {
- hide.push(m.renameAttachments);
- }
-
- if (showSep4) {
- show.push(m.sep4);
- }
- else {
- hide.push(m.sep4);
- }
// If not linked URL, show reindex line
if (Zotero.Fulltext.pdfConverterIsRegistered()
@@ -2294,20 +2304,13 @@ var ZoteroPane = new function()
show.push(m.reindexItem);
showSep4 = true;
}
- else {
- hide.push(m.reindexItem);
+
+ if (showSep4) {
+ show.push(m.sep4);
}
}
else {
- if (item.isNote() && item.isTopLevelItem()) {
- show.push(m.sep4, m.createParent);
- }
- else {
- hide.push(m.sep4, m.createParent);
- }
-
show.push(m.duplicateItem);
- hide.push(m.recognizePDF, m.renameAttachments, m.reindexItem);
}
// Update attachment submenu
@@ -2319,10 +2322,6 @@ var ZoteroPane = new function()
var d = [m.deleteFromLibrary, m.createParent, m.renameAttachments];
for each(var val in d) {
disable.push(val);
- var index = enable.indexOf(val);
- if (index != -1) {
- enable.splice(index, 1);
- }
}
}
}
@@ -2334,14 +2333,9 @@ var ZoteroPane = new function()
if (!itemGroup.isLibrary()) {
show.push(m.showInLibrary, m.sep1);
}
- else {
- hide.push(m.showInLibrary, m.sep1);
- }
disable.push(m.showInLibrary, m.duplicateItem, m.deleteItem,
m.deleteFromLibrary, m.exportItems, m.createBib, m.loadReport);
- hide.push(m.addNote, m.addAttachments, m.sep2, m.sep4, m.reindexItem,
- m.createParent, m.recognizePDF, m.renameAttachments);
}
// TODO: implement menu for remote items
@@ -2358,23 +2352,15 @@ var ZoteroPane = new function()
}
}
disable.push(m[i]);
- var index = enable.indexOf(m[i]);
- if (index != -1) {
- enable.splice(index, 1);
- }
}
}
// Remove from collection
- if (this.itemsView._itemGroup.isCollection() && !(item && item.getSource()))
+ if (itemGroup.isCollection() && !(item && item.getSource()))
{
menu.childNodes[m.deleteItem].setAttribute('label', Zotero.getString('pane.items.menu.remove' + multiple));
show.push(m.deleteItem);
}
- else
- {
- hide.push(m.deleteItem);
- }
// Plural if necessary
menu.childNodes[m.deleteFromLibrary].setAttribute('label', Zotero.getString('pane.items.menu.erase' + multiple));
@@ -2386,19 +2372,15 @@ var ZoteroPane = new function()
menu.childNodes[m.renameAttachments].setAttribute('label', Zotero.getString('pane.items.menu.renameAttachments' + multiple));
menu.childNodes[m.reindexItem].setAttribute('label', Zotero.getString('pane.items.menu.reindexItem' + multiple));
- 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 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 hide)
+ for (var i in disable)
{
- menu.childNodes[hide[i]].setAttribute('hidden', true);
+ menu.childNodes[disable[i]].setAttribute('disabled', true);
}
for (var i in show)
@@ -2411,16 +2393,80 @@ var ZoteroPane = new function()
}
+ this.onTreeMouseDown = function (event) {
+ var itemGroup = ZoteroPane_Local.getItemGroup();
+
+ // Automatically select all equivalent items when clicking on an item
+ // in duplicates view
+ if (itemGroup.isDuplicates()) {
+ // Trigger only on primary-button single clicks with modifiers
+ // (so that items can still be selected and deselected manually)
+ if (!event || event.detail != 1 || event.button != 0 || event.metaKey || event.shiftKey) {
+ return;
+ }
+
+ var t = event.originalTarget;
+
+ if (t.localName != 'treechildren') {
+ return;
+ }
+
+ var tree = t.parentNode;
+
+ var row = {}, col = {}, obj = {};
+ tree.treeBoxObject.getCellAt(event.clientX, event.clientY, row, col, obj);
+
+ // obj.value == 'cell'/'text'/'image'
+ if (!obj.value) {
+ return;
+ }
+
+ // Duplicated in itemTreeView.js::notify()
+ var itemID = ZoteroPane_Local.itemsView._getItemAtRow(row.value).ref.id;
+ var setItemIDs = itemGroup.ref.getSetItemsByItemID(itemID);
+ ZoteroPane_Local.itemsView.selectItems(setItemIDs);
+
+ // Prevent the tree's select event from being called here,
+ // since it's triggered by the multi-select
+ event.stopPropagation();
+ }
+ }
+
+
// Adapted from: http://www.xulplanet.com/references/elemref/ref_tree.html#cmnote-9
this.onTreeClick = function (event) {
- // We only care about primary button double and triple clicks
+ var t = event.originalTarget;
+
+ if (t.localName != 'treechildren') {
+ return;
+ }
+
+ // We care only about primary-button double and triple clicks
if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) {
+ // The Mozilla tree binding fires select() in mousedown(),
+ // but if when it gets to click() the selection differs from
+ // what it expects (say, because multiple items had been
+ // selected during mousedown()), it fires select() again.
+ // We prevent that here.
+ var itemGroup = ZoteroPane_Local.getItemGroup();
+
+ if (itemGroup.isDuplicates()) {
+ if (event.metaKey || event.shiftKey) {
+ return;
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
return;
}
- var t = event.originalTarget;
+ var itemGroup = ZoteroPane_Local.getItemGroup();
- if (t.localName != 'treechildren') {
+ // Ignore all double-clicks in duplicates view
+ if (itemGroup.isDuplicates()) {
+ event.stopPropagation();
+ event.preventDefault();
return;
}
@@ -2440,7 +2486,6 @@ var ZoteroPane = new function()
return;
}
- var itemGroup = ZoteroPane_Local.collectionsView._getItemAtRow(tree.view.selection.currentIndex);
if (itemGroup.isLibrary()) {
var uri = Zotero.URI.getCurrentUserLibraryURI();
if (uri) {
@@ -2451,10 +2496,6 @@ var ZoteroPane = new function()
}
if (itemGroup.isSearch()) {
- // Don't do anything on double-click of Unfiled Items
- if ((itemGroup.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
- return;
- }
ZoteroPane_Local.editSelectedCollection();
return;
}
@@ -2466,6 +2507,11 @@ var ZoteroPane = new function()
return;
}
+ // Ignore double-clicks on Unfiled Items source row
+ if (itemGroup.isUnfiled()) {
+ return;
+ }
+
if (itemGroup.isHeader()) {
if (itemGroup.ref.id == 'group-libraries-header') {
var uri = Zotero.URI.getGroupsURL();
@@ -2498,11 +2544,6 @@ var ZoteroPane = new function()
var item = ZoteroPane_Local.getSelectedItems()[0];
if (item) {
if (item.isRegularItem()) {
- // Double-click on Commons item should load IA page
- var itemGroup = ZoteroPane_Local.collectionsView._getItemAtRow(
- ZoteroPane_Local.collectionsView.selection.currentIndex
- );
-
if (itemGroup.isBucket()) {
var uri = itemGroup.ref.getItemURI(item);
ZoteroPane_Local.loadURI(uri);
@@ -2669,6 +2710,14 @@ var ZoteroPane = new function()
}
+ this.setItemPaneMessage = function (msg) {
+ document.getElementById('zotero-item-pane-content').selectedIndex = 0;
+
+ var label = document.getElementById('zotero-item-pane-message');
+ label.value = msg;
+ }
+
+
// Updates browser context menu options
function contextPopupShowing()
{
diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul
@@ -110,8 +110,6 @@
label="Search for Shared Libraries" oncommand="Zotero.Zeroconf.findInstances()"/>
<menuseparator id="zotero-tb-actions-plugins-separator"/>
<menuitem id="zotero-tb-actions-timeline" label="&zotero.toolbar.timeline.label;" command="cmd_zotero_createTimeline"/>
- <!-- TODO: localize <menuitem id="zotero-tb-actions-duplicate" label="&zotero.toolbar.duplicate.label;" oncommand="ZoteroPane_Local.showDuplicates()"/>-->
- <menuitem id="zotero-tb-actions-showDuplicates" label="Show Duplicates" oncommand="ZoteroPane_Local.showDuplicates()" hidden="true"/>
<menuseparator hidden="true" id="zotero-tb-actions-sync-separator"/>
<menuitem hidden="true" label="WebDAV Sync Debugging" disabled="true"/>
<menuitem hidden="true" label=" Purge Deleted Storage Files" oncommand="Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav', function(results) { Zotero.debug(results); })"/>
@@ -153,8 +151,9 @@
<menuitem class="menuitem-iconic zotero-menuitem-attachments-snapshot" label="&zotero.items.menu.attach.snapshot;" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromPage(false, itemID)"/>
<menuitem class="menuitem-iconic zotero-menuitem-attachments-web-link" label="&zotero.items.menu.attach.link;" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromPage(true, itemID)"/>
<menuitem class="menuitem-iconic zotero-menuitem-attachments-web-link" label="&zotero.items.menu.attach.link.uri;" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromURI(true, itemID);"/>
- <menuitem class="menuitem-iconic zotero-menuitem-attachments-file" label="Attach Stored Copy of File..." oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(false, itemID);"/>
- <menuitem class="menuitem-iconic zotero-menuitem-attachments-link" label="Attach Link to File..." oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(true, itemID);"/>
+ <!-- TODO: localize -->
+ <menuitem class="menuitem-iconic zotero-menuitem-attachments-file" label="Attach Stored Copy of File…" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(false, itemID);"/>
+ <menuitem class="menuitem-iconic zotero-menuitem-attachments-link" label="Attach Link to File…" oncommand="var itemID = ZoteroPane_Local.getSelectedItems()[0].id; ZoteroPane_Local.addAttachmentFromDialog(true, itemID);"/>
</menupopup>
</toolbarbutton>
<toolbarseparator/>
@@ -231,7 +230,8 @@
<menuitem label="&zotero.toolbar.newSavedSearch.label;" command="cmd_zotero_newSavedSearch"/>
<menuitem label="&zotero.toolbar.newSubcollection.label;" oncommand="ZoteroPane_Local.newCollection(ZoteroPane_Local.getSelectedCollection().id)"/>
<menuseparator/>
- <menuitem label="&zotero.collections.showUnfiledItems;" oncommand="ZoteroPane_Local.setUnfiled(ZoteroPane_Local.getSelectedLibraryID(), true)"/>
+ <menuitem label="&zotero.toolbar.duplicate.label;" oncommand="ZoteroPane_Local.setVirtual(ZoteroPane_Local.getSelectedLibraryID(), 'duplicates', true)"/>
+ <menuitem label="&zotero.collections.showUnfiledItems;" oncommand="ZoteroPane_Local.setVirtual(ZoteroPane_Local.getSelectedLibraryID(), 'unfiled', true)"/>
<menuitem oncommand="ZoteroPane_Local.editSelectedCollection();"/>
<menuitem oncommand="ZoteroPane_Local.deleteSelectedCollection();"/>
<menuseparator/>
@@ -261,6 +261,8 @@
<menuitem label="&zotero.items.menu.duplicateItem;" oncommand="ZoteroPane_Local.duplicateSelectedItem();"/>
<menuitem oncommand="ZoteroPane_Local.deleteSelectedItems();"/>
<menuitem oncommand="ZoteroPane_Local.deleteSelectedItems(true);"/>
+ <!-- TODO: localize -->
+ <menuitem oncommand="ZoteroPane_Local.mergeSelectedItems();" label="Merge Items…"/>
<menuseparator/>
<menuitem oncommand="Zotero_File_Interface.exportItems();"/>
<menuitem oncommand="Zotero_File_Interface.bibliographyFromItems();"/>
@@ -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()"/>
- <vbox id="zotero-item-pane" zotero-persist="width">
- <!-- TODO: localize -->
- <button id="zotero-item-restore-button" label="Restore to Library"
- oncommand="ZoteroPane_Local.restoreSelectedItems()" hidden="true"/>
- <!-- TODO: localize -->
- <button id="zotero-item-show-original" label="Show Original"
- oncommand="ZoteroPane_Local.showOriginalItem()" hidden="true"/>
- <deck id="zotero-item-pane-content" selectedIndex="0" flex="1">
- <groupbox pack="center" align="center">
- <label id="zotero-view-selected-label"/>
- </groupbox>
- <tabbox id="zotero-view-tabbox" flex="1" onselect="if (!ZoteroPane_Local.collectionsView.selection || event.originalTarget.localName != 'tabpanels') { return; }; ZoteroItemPane.viewItem(ZoteroPane_Local.getSelectedItems()[0], ZoteroPane_Local.collectionsView.editable ? 'edit' : 'view', this.selectedIndex)">
- <tabs>
- <tab label="&zotero.tabs.info.label;"/>
- <tab label="&zotero.tabs.notes.label;"/>
- <tab label="&zotero.tabs.tags.label;"/>
- <tab label="&zotero.tabs.related.label;"/>
- </tabs>
- <tabpanels id="zotero-view-item" flex="1"/>
- </tabbox>
- <!-- Note info pane -->
- <groupbox id="zotero-view-note" flex="1">
- <zoteronoteeditor id="zotero-note-editor" flex="1" notitle="1"/>
- <button id="zotero-view-note-button" label="&zotero.notes.separate;" oncommand="ZoteroPane_Local.openNoteWindow(this.getAttribute('noteID')); if(this.hasAttribute('sourceID')) ZoteroPane_Local.selectItem(this.getAttribute('sourceID'));"/>
- </groupbox>
- <!-- Attachment info pane -->
- <groupbox flex="1">
- <zoteroattachmentbox id="zotero-attachment-box" flex="1"/>
- </groupbox>
- </deck>
- </vbox>
+ <!-- itemPane.xul -->
+ <vbox id="zotero-item-pane"/>
</hbox>
</vbox>
diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd
@@ -37,6 +37,7 @@
<!ENTITY zotero.tabs.related.label "Related">
<!ENTITY zotero.notes.separate "Edit in a separate window">
+<!ENTITY zotero.toolbar.duplicate.label "Show Duplicates">
<!ENTITY zotero.collections.showUnfiledItems "Show Unfiled Items">
<!ENTITY zotero.items.itemType "Item Type">
@@ -85,7 +86,6 @@
<!ENTITY zotero.toolbar.export.label "Export Library…">
<!ENTITY zotero.toolbar.rtfScan.label "RTF Scan…">
<!ENTITY zotero.toolbar.timeline.label "Create Timeline">
-<!ENTITY zotero.toolbar.duplicate.label "Show Duplicates">
<!ENTITY zotero.toolbar.preferences.label "Preferences…">
<!ENTITY zotero.toolbar.supportAndDocumentation "Support and Documentation">
<!ENTITY zotero.toolbar.about.label "About Zotero">
diff --git 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
@@ -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
@@ -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
Binary files differ.
diff --git a/chrome/skin/default/zotero/treesource-search-virtual.png b/chrome/skin/default/zotero/treesource-search-virtual.png
Binary files differ.
diff --git 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
@@ -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');