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.
This commit is contained in:
Dan Stillman 2011-07-22 21:24:38 +00:00
parent e945b84b5f
commit 56c7afc47e
29 changed files with 1856 additions and 849 deletions

View file

@ -111,7 +111,7 @@
background-color: #ffffff;
}
#zotero-view-selected-label {
#zotero-item-pane-message {
color: #7f7f7f;
}

View file

@ -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>
@ -132,6 +143,22 @@
</setter>
</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"

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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>
<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 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>
<!-- Commons -->
<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">
<!-- Center label (for zero or multiple item selection) -->
<vbox pack="center" align="center">
<label id="zotero-item-pane-message"/>
</vbox>
</tabpanel>
<tabpanel>
<tagsbox id="zotero-editpane-tags" flex="1"/>
</tabpanel>
<tabpanel>
<seealsobox id="zotero-editpane-related" flex="1"/>
</tabpanel>
</tabpanels>
<!-- 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>

View file

@ -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);

View file

@ -40,8 +40,7 @@ Zotero.CollectionTreeView = function()
this.itemToSelect = null;
this._highlightedRows = {};
this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share', 'group', 'bucket']);
this.showDuplicates = false;
this.showCommons = true;
this.hideSources = [];
}
/*
@ -78,6 +77,12 @@ Zotero.CollectionTreeView.prototype.setTree = function(treebox)
var row = this.getLastViewedRow();
this.selection.select(row);
// TODO: make better
var tb = this._treebox;
setTimeout(function () {
tb.ensureRowIsVisible(row);
}, 1);
}
@ -102,6 +107,17 @@ Zotero.CollectionTreeView.prototype.refresh = function()
this._dataItems = [];
this.rowCount = 0;
if (this.hideSources.indexOf('duplicates') == -1) {
try {
var duplicateLibraries = Zotero.Prefs.get('duplicateLibraries').split(',');
}
catch (e) {
// Add to personal library by default
Zotero.Prefs.set('duplicateLibraries', '0');
duplicateLibraries = ['0'];
}
}
try {
var unfiledLibraries = Zotero.Prefs.get('unfiledLibraries').split(',');
}
@ -136,24 +152,31 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
// Unfiled items
if (unfiledLibraries.indexOf('0') != -1) {
var s = new Zotero.Search;
// Give virtual search an id so it can be reselected automatically
s.id = 86345330000; // 'UNFILED' + '000' + libraryID
s.name = Zotero.getString('pane.collections.unfiled');
s.addCondition('libraryID', 'is', null);
s.addCondition('unfiled', 'true');
self._showItem(new Zotero.ItemGroup('search', s), 1, newRows+1);
// Duplicate items
if (self.hideSources.indexOf('duplicates') == -1 && duplicateLibraries.indexOf('0') != -1) {
var d = new Zotero.Duplicates(0);
self._showItem(new Zotero.ItemGroup('duplicates', d), 1, newRows+1);
newRows++;
}
var deletedItems = Zotero.Items.getDeleted();
if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) {
self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1);
// Unfiled items
if (unfiledLibraries.indexOf('0') != -1) {
var s = new Zotero.Search;
s.name = Zotero.getString('pane.collections.unfiled');
s.addCondition('libraryID', 'is', null);
s.addCondition('unfiled', 'true');
self._showItem(new Zotero.ItemGroup('unfiled', s), 1, newRows+1);
newRows++;
}
self.trashNotEmpty = !!deletedItems;
if (self.hideSources.indexOf('trash') == -1) {
var deletedItems = Zotero.Items.getDeleted();
if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) {
self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1);
newRows++;
}
self.trashNotEmpty = !!deletedItems;
}
return newRows;
}
@ -195,15 +218,22 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
// Duplicate items
if (self.hideSources.indexOf('duplicates') == -1
&& duplicateLibraries.indexOf(groups[i].libraryID + '') != -1) {
var d = new Zotero.Duplicates(groups[i].libraryID);
self._showItem(new Zotero.ItemGroup('duplicates', d), 2);
newRows++;
}
// Unfiled items
if (unfiledLibraries.indexOf(groups[i].libraryID + '') != -1) {
var s = new Zotero.Search;
s.id = parseInt('8634533000' + groups[i].libraryID); // 'UNFILED' + '000' + libraryID
s.libraryID = groups[i].libraryID;
s.name = Zotero.getString('pane.collections.unfiled');
s.addCondition('libraryID', 'is', groups[i].libraryID);
s.addCondition('unfiled', 'true');
self._showItem(new Zotero.ItemGroup('search', s), 2);
self._showItem(new Zotero.ItemGroup('unfiled', s), 2);
newRows++;
}
}
@ -221,7 +251,7 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
if (this.showCommons && Zotero.Commons.enabled) {
if (this.hideSources.indexOf('commons') == -1 && Zotero.Commons.enabled) {
this._showItem(new Zotero.ItemGroup('separator', false));
var header = {
id: "commons-header",
@ -246,7 +276,14 @@ Zotero.CollectionTreeView.prototype.refresh = function()
}
}
this._refreshHashMap();
try {
this._refreshHashMap();
}
catch (e) {
Components.utils.reportError(e);
Zotero.debug(e);
throw (e);
}
// Update the treebox's row count
var diff = this.rowCount - oldCount;
@ -274,7 +311,7 @@ Zotero.CollectionTreeView.prototype.reload = function()
for(var i = 0; i < openCollections.length; i++)
{
var row = this._collectionRowMap[openCollections[i]];
if (row != null) {
if (typeof row != 'undefined') {
this.toggleOpenState(row);
}
}
@ -313,22 +350,20 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
switch (type)
{
case 'collection':
if(this._collectionRowMap[ids[i]] != null)
{
rows.push(this._collectionRowMap[ids[i]]);
if (typeof this._rowMap['C' + ids[i]] != 'undefined') {
rows.push(this._rowMap['C' + ids[i]]);
}
break;
case 'search':
if(this._searchRowMap[ids[i]] != null)
{
rows.push(this._searchRowMap[ids[i]]);
if (typeof this._rowMap['S' + ids[i]] != 'undefined') {
rows.push(this._rowMap['S' + ids[i]]);
}
break;
case 'group':
//if (this._groupRowMap[ids[i]] != null) {
// rows.push(this._groupRowMap[ids[i]]);
//if (this._rowMap['G' + ids[i]] != null) {
// rows.push(this._rowMap['G' + ids[i]]);
//}
// For now, just reload if a group is removed, since otherwise
@ -410,7 +445,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
this.rememberSelection(savedSelection);
break;
}
this.selection.select(this._searchRowMap[ids]);
this.selection.select(this._rowMap['S' + ids]);
break;
case 'group':
@ -454,21 +489,6 @@ Zotero.CollectionTreeView.prototype.unregister = function()
Zotero.Notifier.unregisterObserver(this._unregisterID);
}
Zotero.CollectionTreeView.prototype.isLibrary = function(row)
{
return this._getItemAtRow(row).isLibrary();
}
Zotero.CollectionTreeView.prototype.isCollection = function(row)
{
return this._getItemAtRow(row).isCollection();
}
Zotero.CollectionTreeView.prototype.isSearch = function(row)
{
return this._getItemAtRow(row).isSearch();
}
////////////////////////////////////////////////////////////////////////////////
///
@ -498,17 +518,6 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
}
break;
case 'collection':
// TODO: group collection
return "chrome://zotero-platform/content/treesource-collection.png";
case 'search':
if ((source.ref.id + "").match(/^8634533000/)) { // 'UNFILED000'
collectionType = "search-virtual";
break;
}
return "chrome://zotero-platform/content/treesource-search.png";
case 'header':
if (source.ref.id == 'group-libraries-header') {
collectionType = 'groups';
@ -521,7 +530,12 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
case 'group':
collectionType = 'library';
break;
case 'collection':
case 'search':
return "chrome://zotero-platform/content/treesource-" + collectionType + ".png";
}
return "chrome://zotero/skin/treesource-" + collectionType + ".png";
}
@ -752,12 +766,13 @@ Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) {
return false;
}
/**
* Select the last-viewed source
*/
Zotero.CollectionTreeView.prototype.getLastViewedRow = function () {
var lastViewedFolder = Zotero.Prefs.get('lastViewedFolder');
var matches = lastViewedFolder.match(/^(?:(C|S|G)([0-9]+)|L)$/);
var matches = lastViewedFolder.match(/^([A-Z])([0-9]+)?$/);
var select = 0;
if (matches) {
if (matches[1] == 'C') {
@ -813,11 +828,11 @@ Zotero.CollectionTreeView.prototype.getLastViewedRow = function () {
}
}
}
else if (matches[1] == 'S' && this._searchRowMap[matches[2]]) {
select = this._searchRowMap[matches[2]];
}
else if (matches[1] == 'G' && this._groupRowMap[matches[2]]) {
select = this._groupRowMap[matches[2]];
else {
var id = matches[1] + (matches[2] ? matches[2] : "");
if (this._rowMap[id]) {
select = this._rowMap[id];
}
}
}
@ -929,20 +944,12 @@ Zotero.CollectionTreeView.prototype.saveSelection = function()
for (var i=0, len=this.rowCount; i<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);

View file

@ -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();
@ -3755,6 +3759,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
*
@ -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

View file

@ -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);

View file

@ -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;
}

View file

@ -27,7 +27,10 @@ Zotero.Relations = new function () {
Zotero.DataObjects.apply(this, ['relation']);
this.constructor.prototype = new Zotero.DataObjects();
this.__defineGetter__('deletedItemPredicate', function () 'dc:isReplacedBy');
var _namespaces = {
dc: 'http://purl.org/dc/elements/1.1/',
owl: 'http://www.w3.org/2002/07/owl#'
};
@ -46,7 +49,10 @@ Zotero.Relations = new function () {
* @return {Object[]}
*/
this.getByURIs = function (subject, predicate, object) {
predicate = _getPrefixAndValue(predicate).join(':');
if (predicate) {
predicate = _getPrefixAndValue(predicate).join(':');
}
if (!subject && !predicate && !object) {
throw ("No values provided in Zotero.Relations.get()");
}
@ -151,34 +157,66 @@ Zotero.Relations = new function () {
}
this.erase = function (id) {
/**
* Copy relations from one object to another within the same library
*/
this.copyURIs = function (libraryID, fromURI, toURI) {
var rels = this.getByURIs(fromURI);
for each(var rel in rels) {
this.add(libraryID, toURI, rel.predicate, rel.object);
}
var rels = this.getByURIs(false, false, fromURI);
for each(var rel in rels) {
this.add(libraryID, rel.subject, rel.predicate, toURI);
}
}
/**
* @param {String} prefix
* @param {String[]} ignorePredicates
*/
this.eraseByURIPrefix = function (prefix, ignorePredicates) {
Zotero.DB.beginTransaction();
var sql = "DELETE FROM relations WHERE ROWID=?";
Zotero.DB.query(sql, [id]);
prefix = prefix + '%';
var sql = "SELECT ROWID FROM relations WHERE (subject LIKE ? OR object LIKE ?)";
var params = [prefix, prefix];
if (ignorePredicates) {
sql += " AND predicate != ?";
params = params.concat(ignorePredicates);
}
var ids = Zotero.DB.columnQuery(sql, params);
// TODO: log to syncDeleteLog
for each(var id in ids) {
var relation = this.get(id);
relation.erase();
}
Zotero.DB.commitTransaction();
}
this.eraseByURIPrefix = function (prefix) {
prefix = prefix + '%';
var sql = "DELETE FROM relations WHERE subject LIKE ? OR object LIKE ?";
Zotero.DB.query(sql, [prefix, prefix]);
}
this.eraseByURI = function (uri) {
var sql = "DELETE FROM relations WHERE subject=? OR object=?";
Zotero.DB.query(sql, [uri, uri]);
Zotero.DB.beginTransaction();
var sql = "SELECT ROWID FROM relations WHERE subject=? OR object=?";
var ids = Zotero.DB.columnQuery(sql, [uri, uri]);
for each(var id in ids) {
var relation = this.get(id);
relation.erase();
}
Zotero.DB.commitTransaction();
}
this.purge = function () {
var sql = "SELECT subject FROM relations UNION SELECT object FROM relations";
var uris = Zotero.DB.columnQuery(sql);
var sql = "SELECT subject FROM relations WHERE predicate != ? "
+ "UNION SELECT object FROM relations WHERE predicate != ?";
var uris = Zotero.DB.columnQuery(sql, [this.deletedItemPredicate, this.deletedItemPredicate]);
if (uris) {
var prefix = Zotero.URI.defaultPrefix;
Zotero.DB.beginTransaction();

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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) {}
}

View file

@ -122,19 +122,37 @@ Zotero.ItemTreeView.prototype.setTree = function(treebox)
obj.refresh();
// Add a keypress listener for expand/collapse
var expandAllRows = obj.expandAllRows;
var collapseAllRows = obj.collapseAllRows;
var tree = obj._treebox.treeBody.parentNode;
var listener = function(event) {
var key = String.fromCharCode(event.which);
if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
obj.expandAllRows(treebox);
// Handle arrow keys specially on multiple selection, since
// otherwise the tree just applies it to the last-selected row
if (event.keyCode == 39 || event.keyCode == 37) {
if (obj._treebox.view.selection.count > 1) {
switch (event.keyCode) {
case 39:
obj.expandSelectedRows();
break;
case 37:
obj.collapseSelectedRows();
break;
}
event.preventDefault();
}
return;
}
else if (key == '-' && !(event.shiftKey || event.ctrlKey ||
event.altKey || event.metaKey)) {
obj.collapseAllRows(treebox);
var key = String.fromCharCode(event.which);
if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) {
obj.expandAllRows();
event.preventDefault();
return;
}
else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) {
obj.collapseAllRows();
event.preventDefault();
return;
}
};
@ -206,7 +224,8 @@ Zotero.ItemTreeView.prototype.refresh = function()
Zotero.DB.beginTransaction();
Zotero.Items.cacheFields(cacheFields);
var newRows = this._itemGroup.getChildItems();
var newRows = this._itemGroup.getItems();
var added = 0;
for (var i=0, len=newRows.length; i < len; i++) {
@ -276,6 +295,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
var sort = false;
var savedSelection = this.saveSelection();
var previousRow = false;
// Redraw the tree (for tag color changes)
if (action == 'redraw') {
@ -303,26 +323,27 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
return;
}
if (this._itemGroup.isShare()) {
if (itemGroup.isShare()) {
return;
}
this.selection.selectEventsSuppressed = true;
// See if we're in the active window
var zp = Zotero.getActiveZoteroPane();
var activeWindow = zp && zp.itemsView == this;
var quicksearch = this._ownerDocument.getElementById('zotero-tb-search');
// 'collection-item' ids are in the form collectionID-itemID
if (type == 'collection-item') {
if (!itemGroup.isCollection()) {
return;
}
var splitIDs = [];
for each(var id in ids) {
var split = id.split('-');
// Skip if not collection or not an item in this collection
if (!itemGroup.isCollection() || split[0] != this._itemGroup.ref.id) {
// Skip if not an item in this collection
if (split[0] != itemGroup.ref.id) {
continue;
}
splitIDs.push(split[1]);
@ -336,41 +357,51 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
}
}
this.selection.selectEventsSuppressed = true;
if ((action == 'remove' && !itemGroup.isLibrary(true))
|| action == 'delete' || action == 'trash') {
// Since a remove involves shifting of rows, we have to do it in order,
// so sort the ids by row
var rows = [];
for(var i=0, len=ids.length; i<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)
{
this._hideItem(row-i);
this._treebox.rowCountChanged(row-i,-1);
}
}
// 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)
{
this._hideItem(row-i);
this._treebox.rowCountChanged(row-i,-1);
}
}
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++) {

View file

@ -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

View file

@ -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
}
}
];

View file

@ -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) {

View file

@ -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
*

File diff suppressed because it is too large Load diff

View file

@ -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 &amp;&amp; !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>

View file

@ -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">

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 B

View file

@ -76,7 +76,7 @@ const xpcomFilesLocal = [
'data/tags',
'date',
'db',
'duplicate',
'duplicates',
'enstyle',
'fulltext',
'id',

View file

@ -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');