diff --git a/chrome/content/zotero-platform/mac/overlay.css b/chrome/content/zotero-platform/mac/overlay.css index 6ed923252..a3fa35a42 100644 --- a/chrome/content/zotero-platform/mac/overlay.css +++ b/chrome/content/zotero-platform/mac/overlay.css @@ -111,7 +111,7 @@ background-color: #ffffff; } -#zotero-view-selected-label { +#zotero-item-pane-message { color: #7f7f7f; } diff --git a/chrome/content/zotero/bindings/itembox.xml b/chrome/content/zotero/bindings/itembox.xml index 927d76448..cdf4c8060 100644 --- a/chrome/content/zotero/bindings/itembox.xml +++ b/chrome/content/zotero/bindings/itembox.xml @@ -79,7 +79,6 @@ break; case 'merge': - //this.hideEmptyFields = true; this.clickByItem = true; break; @@ -92,6 +91,11 @@ this.blurHandler = this.hideEditor; break; + case 'fieldmerge': + this.hideEmptyFields = true; + this._fieldAlternatives = {}; + break; + default: throw ("Invalid mode '" + val + "' in itembox.xml"); } @@ -103,15 +107,22 @@ </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" diff --git a/chrome/content/zotero/bindings/tagselector.xml b/chrome/content/zotero/bindings/tagselector.xml index ef251c871..654603c16 100644 --- 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 new file mode 100644 index 000000000..8dc7868f5 --- /dev/null +++ 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 index a22919af7..bec6deca7 100644 --- 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> + <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> \ No newline at end of file diff --git a/chrome/content/zotero/selectItemsDialog.js b/chrome/content/zotero/selectItemsDialog.js index ec9750bb4..02ba2370c 100644 --- a/chrome/content/zotero/selectItemsDialog.js +++ b/chrome/content/zotero/selectItemsDialog.js @@ -45,7 +45,7 @@ function doLoad() collectionsView = new Zotero.CollectionTreeView(); // Don't show Commons when citing - collectionsView.showCommons = false; + collectionsView.hideSources = ['duplicates', 'trash', 'commons']; document.getElementById('zotero-collections-tree').view = collectionsView; if(io.select) itemsView.selectItem(io.select); diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js index b340d857d..690d85847 100644 --- a/chrome/content/zotero/xpcom/collectionTreeView.js +++ b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -40,8 +40,7 @@ Zotero.CollectionTreeView = function() this.itemToSelect = null; this._highlightedRows = {}; this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share', 'group', 'bucket']); - this.showDuplicates = false; - this.showCommons = true; + this.hideSources = []; } /* @@ -78,6 +77,12 @@ Zotero.CollectionTreeView.prototype.setTree = function(treebox) var row = this.getLastViewedRow(); this.selection.select(row); + + // TODO: make better + var tb = this._treebox; + setTimeout(function () { + tb.ensureRowIsVisible(row); + }, 1); } @@ -102,6 +107,17 @@ Zotero.CollectionTreeView.prototype.refresh = function() this._dataItems = []; this.rowCount = 0; + if (this.hideSources.indexOf('duplicates') == -1) { + try { + var duplicateLibraries = Zotero.Prefs.get('duplicateLibraries').split(','); + } + catch (e) { + // Add to personal library by default + Zotero.Prefs.set('duplicateLibraries', '0'); + duplicateLibraries = ['0']; + } + } + try { var unfiledLibraries = Zotero.Prefs.get('unfiledLibraries').split(','); } @@ -136,24 +152,31 @@ Zotero.CollectionTreeView.prototype.refresh = function() } } - // Unfiled items - if (unfiledLibraries.indexOf('0') != -1) { - var s = new Zotero.Search; - // Give virtual search an id so it can be reselected automatically - s.id = 86345330000; // 'UNFILED' + '000' + libraryID - s.name = Zotero.getString('pane.collections.unfiled'); - s.addCondition('libraryID', 'is', null); - s.addCondition('unfiled', 'true'); - self._showItem(new Zotero.ItemGroup('search', s), 1, newRows+1); + // Duplicate items + if (self.hideSources.indexOf('duplicates') == -1 && duplicateLibraries.indexOf('0') != -1) { + var d = new Zotero.Duplicates(0); + self._showItem(new Zotero.ItemGroup('duplicates', d), 1, newRows+1); newRows++; } - var deletedItems = Zotero.Items.getDeleted(); - if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) { - self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1); + // Unfiled items + if (unfiledLibraries.indexOf('0') != -1) { + var s = new Zotero.Search; + s.name = Zotero.getString('pane.collections.unfiled'); + s.addCondition('libraryID', 'is', null); + s.addCondition('unfiled', 'true'); + self._showItem(new Zotero.ItemGroup('unfiled', s), 1, newRows+1); newRows++; } - self.trashNotEmpty = !!deletedItems; + + if (self.hideSources.indexOf('trash') == -1) { + var deletedItems = Zotero.Items.getDeleted(); + if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) { + self._showItem(new Zotero.ItemGroup('trash', false), 1, newRows+1); + newRows++; + } + self.trashNotEmpty = !!deletedItems; + } return newRows; } @@ -195,15 +218,22 @@ Zotero.CollectionTreeView.prototype.refresh = function() } } + // Duplicate items + if (self.hideSources.indexOf('duplicates') == -1 + && duplicateLibraries.indexOf(groups[i].libraryID + '') != -1) { + var d = new Zotero.Duplicates(groups[i].libraryID); + self._showItem(new Zotero.ItemGroup('duplicates', d), 2); + newRows++; + } + // Unfiled items if (unfiledLibraries.indexOf(groups[i].libraryID + '') != -1) { var s = new Zotero.Search; - s.id = parseInt('8634533000' + groups[i].libraryID); // 'UNFILED' + '000' + libraryID s.libraryID = groups[i].libraryID; s.name = Zotero.getString('pane.collections.unfiled'); s.addCondition('libraryID', 'is', groups[i].libraryID); s.addCondition('unfiled', 'true'); - self._showItem(new Zotero.ItemGroup('search', s), 2); + self._showItem(new Zotero.ItemGroup('unfiled', s), 2); newRows++; } } @@ -221,7 +251,7 @@ Zotero.CollectionTreeView.prototype.refresh = function() } } - if (this.showCommons && Zotero.Commons.enabled) { + if (this.hideSources.indexOf('commons') == -1 && Zotero.Commons.enabled) { this._showItem(new Zotero.ItemGroup('separator', false)); var header = { id: "commons-header", @@ -246,7 +276,14 @@ Zotero.CollectionTreeView.prototype.refresh = function() } } - this._refreshHashMap(); + try { + this._refreshHashMap(); + } + catch (e) { + Components.utils.reportError(e); + Zotero.debug(e); + throw (e); + } // Update the treebox's row count var diff = this.rowCount - oldCount; @@ -274,7 +311,7 @@ Zotero.CollectionTreeView.prototype.reload = function() for(var i = 0; i < openCollections.length; i++) { var row = this._collectionRowMap[openCollections[i]]; - if (row != null) { + if (typeof row != 'undefined') { this.toggleOpenState(row); } } @@ -313,22 +350,20 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids) switch (type) { case 'collection': - if(this._collectionRowMap[ids[i]] != null) - { - rows.push(this._collectionRowMap[ids[i]]); + if (typeof this._rowMap['C' + ids[i]] != 'undefined') { + rows.push(this._rowMap['C' + ids[i]]); } break; case 'search': - if(this._searchRowMap[ids[i]] != null) - { - rows.push(this._searchRowMap[ids[i]]); + if (typeof this._rowMap['S' + ids[i]] != 'undefined') { + rows.push(this._rowMap['S' + ids[i]]); } break; case 'group': - //if (this._groupRowMap[ids[i]] != null) { - // rows.push(this._groupRowMap[ids[i]]); + //if (this._rowMap['G' + ids[i]] != null) { + // rows.push(this._rowMap['G' + ids[i]]); //} // For now, just reload if a group is removed, since otherwise @@ -410,7 +445,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids) this.rememberSelection(savedSelection); break; } - this.selection.select(this._searchRowMap[ids]); + this.selection.select(this._rowMap['S' + ids]); break; case 'group': @@ -454,21 +489,6 @@ Zotero.CollectionTreeView.prototype.unregister = function() Zotero.Notifier.unregisterObserver(this._unregisterID); } -Zotero.CollectionTreeView.prototype.isLibrary = function(row) -{ - return this._getItemAtRow(row).isLibrary(); -} - -Zotero.CollectionTreeView.prototype.isCollection = function(row) -{ - return this._getItemAtRow(row).isCollection(); -} - -Zotero.CollectionTreeView.prototype.isSearch = function(row) -{ - return this._getItemAtRow(row).isSearch(); -} - //////////////////////////////////////////////////////////////////////////////// /// @@ -498,17 +518,6 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col) } break; - case 'collection': - // TODO: group collection - return "chrome://zotero-platform/content/treesource-collection.png"; - - case 'search': - if ((source.ref.id + "").match(/^8634533000/)) { // 'UNFILED000' - collectionType = "search-virtual"; - break; - } - return "chrome://zotero-platform/content/treesource-search.png"; - case 'header': if (source.ref.id == 'group-libraries-header') { collectionType = 'groups'; @@ -521,7 +530,12 @@ Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col) case 'group': collectionType = 'library'; break; + + case 'collection': + case 'search': + return "chrome://zotero-platform/content/treesource-" + collectionType + ".png"; } + return "chrome://zotero/skin/treesource-" + collectionType + ".png"; } @@ -752,12 +766,13 @@ Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) { return false; } + /** * Select the last-viewed source */ Zotero.CollectionTreeView.prototype.getLastViewedRow = function () { var lastViewedFolder = Zotero.Prefs.get('lastViewedFolder'); - var matches = lastViewedFolder.match(/^(?:(C|S|G)([0-9]+)|L)$/); + var matches = lastViewedFolder.match(/^([A-Z])([0-9]+)?$/); var select = 0; if (matches) { if (matches[1] == 'C') { @@ -813,11 +828,11 @@ Zotero.CollectionTreeView.prototype.getLastViewedRow = function () { } } } - else if (matches[1] == 'S' && this._searchRowMap[matches[2]]) { - select = this._searchRowMap[matches[2]]; - } - else if (matches[1] == 'G' && this._groupRowMap[matches[2]]) { - select = this._groupRowMap[matches[2]]; + else { + var id = matches[1] + (matches[2] ? matches[2] : ""); + if (this._rowMap[id]) { + select = this._rowMap[id]; + } } } @@ -929,20 +944,12 @@ Zotero.CollectionTreeView.prototype.saveSelection = function() for (var i=0, len=this.rowCount; i<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 index 66eb19198..07ec7778f 100644 --- 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(); @@ -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 diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index 6e099622d..241dc4f95 100644 --- 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 index a2d764846..99a643929 100644 --- 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 index b4ddc066d..3254cbfab 100644 --- a/chrome/content/zotero/xpcom/data/relations.js +++ b/chrome/content/zotero/xpcom/data/relations.js @@ -27,7 +27,10 @@ Zotero.Relations = new function () { Zotero.DataObjects.apply(this, ['relation']); this.constructor.prototype = new Zotero.DataObjects(); + this.__defineGetter__('deletedItemPredicate', function () 'dc:isReplacedBy'); + var _namespaces = { + dc: 'http://purl.org/dc/elements/1.1/', owl: 'http://www.w3.org/2002/07/owl#' }; @@ -46,7 +49,10 @@ Zotero.Relations = new function () { * @return {Object[]} */ this.getByURIs = function (subject, predicate, object) { - predicate = _getPrefixAndValue(predicate).join(':'); + if (predicate) { + predicate = _getPrefixAndValue(predicate).join(':'); + } + if (!subject && !predicate && !object) { throw ("No values provided in Zotero.Relations.get()"); } @@ -151,34 +157,66 @@ Zotero.Relations = new function () { } - this.erase = function (id) { + /** + * Copy relations from one object to another within the same library + */ + this.copyURIs = function (libraryID, fromURI, toURI) { + var rels = this.getByURIs(fromURI); + for each(var rel in rels) { + this.add(libraryID, toURI, rel.predicate, rel.object); + } + + var rels = this.getByURIs(false, false, fromURI); + for each(var rel in rels) { + this.add(libraryID, rel.subject, rel.predicate, toURI); + } + } + + + /** + * @param {String} prefix + * @param {String[]} ignorePredicates + */ + this.eraseByURIPrefix = function (prefix, ignorePredicates) { Zotero.DB.beginTransaction(); - var sql = "DELETE FROM relations WHERE ROWID=?"; - Zotero.DB.query(sql, [id]); + prefix = prefix + '%'; + var sql = "SELECT ROWID FROM relations WHERE (subject LIKE ? OR object LIKE ?)"; + var params = [prefix, prefix]; + if (ignorePredicates) { + sql += " AND predicate != ?"; + params = params.concat(ignorePredicates); + } + var ids = Zotero.DB.columnQuery(sql, params); - // TODO: log to syncDeleteLog + for each(var id in ids) { + var relation = this.get(id); + relation.erase(); + } Zotero.DB.commitTransaction(); } - this.eraseByURIPrefix = function (prefix) { - prefix = prefix + '%'; - var sql = "DELETE FROM relations WHERE subject LIKE ? OR object LIKE ?"; - Zotero.DB.query(sql, [prefix, prefix]); - } - - this.eraseByURI = function (uri) { - var sql = "DELETE FROM relations WHERE subject=? OR object=?"; - Zotero.DB.query(sql, [uri, uri]); + Zotero.DB.beginTransaction(); + + var sql = "SELECT ROWID FROM relations WHERE subject=? OR object=?"; + var ids = Zotero.DB.columnQuery(sql, [uri, uri]); + + for each(var id in ids) { + var relation = this.get(id); + relation.erase(); + } + + Zotero.DB.commitTransaction(); } this.purge = function () { - var sql = "SELECT subject FROM relations UNION SELECT object FROM relations"; - var uris = Zotero.DB.columnQuery(sql); + var sql = "SELECT subject FROM relations WHERE predicate != ? " + + "UNION SELECT object FROM relations WHERE predicate != ?"; + var uris = Zotero.DB.columnQuery(sql, [this.deletedItemPredicate, this.deletedItemPredicate]); if (uris) { var prefix = Zotero.URI.defaultPrefix; Zotero.DB.beginTransaction(); diff --git a/chrome/content/zotero/xpcom/duplicate.js b/chrome/content/zotero/xpcom/duplicate.js deleted file mode 100644 index 6f8b2aba1..000000000 --- a/chrome/content/zotero/xpcom/duplicate.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright © 2009 Center for History and New Media - George Mason University, Fairfax, Virginia, USA - http://zotero.org - - This file is part of Zotero. - - Zotero is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Zotero is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Zotero. If not, see <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 new file mode 100644 index 000000000..fccc30c6d --- /dev/null +++ b/chrome/content/zotero/xpcom/duplicates.js @@ -0,0 +1,286 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <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 index f43654990..c75ea060d 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -1203,7 +1203,40 @@ Zotero.Integration.Session.prototype.addCitation = function(index, noteIndex, ar var zoteroItem = false; if(citationItem.uri) { [zoteroItem, needUpdate] = this.uriMap.getZoteroItemForURIs(citationItem.uri); - if(needUpdate) this.updateIndices[index] = true; + if(zoteroItem) { + if(needUpdate) this.updateIndices[index] = true; + } else { + // Try merged item mappings + for each(var uri in citationItem.uri) { + var seen = []; + + // Follow merged item relations until we find an item + // or hit a dead end + while (!zoteroItem) { + var relations = Zotero.Relations.getByURIs(uri, Zotero.Relations.deletedItemPredicate); + // No merged items found + if(!relations.length) { + break; + } + + uri = relations[0].object; + + // Keep track of mapped URIs in case there's a circular relation + if(seen.indexOf(uri) != -1) { + var msg = "Circular relation for '" + uri + "' in merged item mapping resolution"; + Zotero.debug(msg, 2); + Components.utils.reportError(msg); + break; + } + seen.push(uri); + + [zoteroItem, needUpdate] = this.uriMap.getZoteroItemForURIs([uri]); + } + } + + if(zoteroItem && needUpdate) this.updateIndices[index] = true; + } + } else { if(citationItem.key) { zoteroItem = Zotero.Items.getByKey(citationItem.key); @@ -2044,8 +2077,13 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = function(uris) { for(var i in uris) { try { - zoteroItem = Zotero.URI.getURIItem(uris[i]); - if(zoteroItem) break; + zoteroItem = Zotero.URI.getURIItem(uris[i]); + if(zoteroItem) { + // Ignore items in the trash + if(zoteroItem.deleted) { + zoteroItem = false; + } + } } catch(e) {} } diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js index ed9feebc4..095c19b75 100644 --- a/chrome/content/zotero/xpcom/itemTreeView.js +++ b/chrome/content/zotero/xpcom/itemTreeView.js @@ -122,19 +122,37 @@ Zotero.ItemTreeView.prototype.setTree = function(treebox) obj.refresh(); // Add a keypress listener for expand/collapse - var expandAllRows = obj.expandAllRows; - var collapseAllRows = obj.collapseAllRows; var tree = obj._treebox.treeBody.parentNode; var listener = function(event) { - var key = String.fromCharCode(event.which); - - if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) { - obj.expandAllRows(treebox); + // Handle arrow keys specially on multiple selection, since + // otherwise the tree just applies it to the last-selected row + if (event.keyCode == 39 || event.keyCode == 37) { + if (obj._treebox.view.selection.count > 1) { + switch (event.keyCode) { + case 39: + obj.expandSelectedRows(); + break; + + case 37: + obj.collapseSelectedRows(); + break; + } + + event.preventDefault(); + } + return; } - else if (key == '-' && !(event.shiftKey || event.ctrlKey || - event.altKey || event.metaKey)) { - obj.collapseAllRows(treebox); + + var key = String.fromCharCode(event.which); + if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) { + obj.expandAllRows(); + event.preventDefault(); + return; + } + else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) { + obj.collapseAllRows(); + event.preventDefault(); return; } }; @@ -206,7 +224,8 @@ Zotero.ItemTreeView.prototype.refresh = function() Zotero.DB.beginTransaction(); Zotero.Items.cacheFields(cacheFields); - var newRows = this._itemGroup.getChildItems(); + var newRows = this._itemGroup.getItems(); + var added = 0; for (var i=0, len=newRows.length; i < len; i++) { @@ -276,6 +295,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) var sort = false; var savedSelection = this.saveSelection(); + var previousRow = false; // Redraw the tree (for tag color changes) if (action == 'redraw') { @@ -303,26 +323,27 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) return; } - if (this._itemGroup.isShare()) { + if (itemGroup.isShare()) { return; } - this.selection.selectEventsSuppressed = true; - // See if we're in the active window var zp = Zotero.getActiveZoteroPane(); var activeWindow = zp && zp.itemsView == this; var quicksearch = this._ownerDocument.getElementById('zotero-tb-search'); - // 'collection-item' ids are in the form collectionID-itemID if (type == 'collection-item') { + if (!itemGroup.isCollection()) { + return; + } + var splitIDs = []; for each(var id in ids) { var split = id.split('-'); - // Skip if not collection or not an item in this collection - if (!itemGroup.isCollection() || split[0] != this._itemGroup.ref.id) { + // Skip if not an item in this collection + if (split[0] != itemGroup.ref.id) { continue; } splitIDs.push(split[1]); @@ -336,41 +357,51 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) } } + this.selection.selectEventsSuppressed = true; + if ((action == 'remove' && !itemGroup.isLibrary(true)) || action == 'delete' || action == 'trash') { - // Since a remove involves shifting of rows, we have to do it in order, - // so sort the ids by row - var rows = []; - for(var i=0, len=ids.length; i<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++) { diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js index deb96bf78..14fab27df 100644 --- 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 index 57d453724..88c3f2f38 100644 --- 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 index 53e1813da..f6e30101a 100644 --- 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 index 4e2920b5e..ceff75210 100644 --- a/chrome/content/zotero/xpcom/utilities.js +++ b/chrome/content/zotero/xpcom/utilities.js @@ -558,7 +558,125 @@ Zotero.Utilities = { return newString; }, - + + /** + * Replaces accented characters in a string with ASCII equivalents + * + * @param {String} str + * @param {Boolean} [lowercaseOnly] Limit conversions to lowercase characters + * (for improved performance on lowercase input) + * @return {String} + * + * From http://lehelk.com/2011/05/06/script-to-remove-diacritics/ + */ + "removeDiacritics": function (str, lowercaseOnly) { + var map = this._diacriticsRemovalMap.lowercase; + for (var i=0, len=map.length; i<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 index 31d6ca817..a052a5a12 100644 --- 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,21 +2372,17 @@ 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)); + // 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 disable) { menu.childNodes[disable[i]].setAttribute('disabled', true); } - for (var i in enable) - { - menu.childNodes[enable[i]].setAttribute('disabled', false); - } - - for (var i in hide) - { - menu.childNodes[hide[i]].setAttribute('hidden', true); - } - for (var i in show) { menu.childNodes[show[i]].setAttribute('hidden', false); @@ -2411,19 +2393,83 @@ 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 - if (!event || (event.detail != 2 && event.detail != 3) || event.button != 0) { - return; - } - 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 itemGroup = ZoteroPane_Local.getItemGroup(); + + // Ignore all double-clicks in duplicates view + if (itemGroup.isDuplicates()) { + event.stopPropagation(); + event.preventDefault(); + return; + } + var tree = t.parentNode; var row = {}, col = {}, obj = {}; @@ -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 index 0c28ac25d..d38a7eb22 100644 --- 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 index fc366b0f5..8528c1fd5 100644 --- 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 index e25d355cf..688b00824 100644 --- a/chrome/skin/default/zotero/bindings/itembox.css +++ b/chrome/skin/default/zotero/bindings/itembox.css @@ -148,4 +148,10 @@ hbox.zotero-date-field-status > label background-position: center !important; border-width: 0 !important; -moz-border-radius: 4px !important; +} + +/* Merge pane in duplicates view */ +.zotero-field-version-button { + margin: 0; + padding: 0; } \ No newline at end of file diff --git a/chrome/skin/default/zotero/itemPane.css b/chrome/skin/default/zotero/itemPane.css index 87f4f391a..cd22c282d 100644 --- a/chrome/skin/default/zotero/itemPane.css +++ b/chrome/skin/default/zotero/itemPane.css @@ -1,4 +1,54 @@ +#zotero-item-pane-message { + padding: 0 2em; +} + +#zotero-view-tabbox, #zotero-item-pane-content > groupbox, #zotero-item-pane-content > groupbox > .groupbox-body +{ + margin: 0 !important; + padding: 0 !important; +} + +#zotero-view-tabbox tabs tab +{ + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +#zotero-view-tabbox tabs tab .tab-text +{ + margin-top: .2em !important; + margin-bottom: .25em !important; +} + +#zotero-view-item +{ + padding: 1.5em .25em .25em; +} + #zotero-view-item > tabpanel > * { overflow: auto; } + +#zotero-view-item > vbox +{ + overflow: auto; + margin-left: 5px; +} + + +/* Merge pane in duplicates view */ +#zotero-duplicates-merge-button +{ + font-size: 13px; +} + +#zotero-duplicates-merge-pane > groupbox +{ + margin: 0; +} + +#zotero-duplicates-merge-item-box row +{ + min-height: 20px; +} diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css index 9e748467c..6b0a3d7d9 100644 --- a/chrome/skin/default/zotero/overlay.css +++ b/chrome/skin/default/zotero/overlay.css @@ -429,35 +429,6 @@ cursor: default; } -#zotero-view-tabbox, #zotero-item-pane-content > groupbox, #zotero-item-pane-content > groupbox > .groupbox-body -{ - margin: 0 !important; - padding: 0 !important; -} - -#zotero-view-tabbox tabs tab -{ - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -#zotero-view-tabbox tabs tab .tab-text -{ - margin-top: .2em !important; - margin-bottom: .25em !important; -} - -#zotero-view-item -{ - padding: 1.5em .25em .25em; -} - -#zotero-view-item > vbox -{ - overflow: auto; - margin-left: 5px; -} - #zotero-splitter { border-top: none; diff --git a/chrome/skin/default/zotero/treesource-duplicates.png b/chrome/skin/default/zotero/treesource-duplicates.png new file mode 100644 index 000000000..ca779f323 Binary files /dev/null and b/chrome/skin/default/zotero/treesource-duplicates.png differ diff --git a/chrome/skin/default/zotero/treesource-search-virtual.png b/chrome/skin/default/zotero/treesource-search-virtual.png deleted file mode 100644 index 44084add7..000000000 Binary files a/chrome/skin/default/zotero/treesource-search-virtual.png and /dev/null differ diff --git a/components/zotero-service.js b/components/zotero-service.js index d47b08629..1f8631ca6 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -76,7 +76,7 @@ const xpcomFilesLocal = [ 'data/tags', 'date', 'db', - 'duplicate', + 'duplicates', 'enstyle', 'fulltext', 'id', diff --git a/system.sql b/system.sql index 623ffeff9..d7560b176 100644 --- a/system.sql +++ b/system.sql @@ -1359,4 +1359,4 @@ INSERT INTO "syncObjectTypes" VALUES(2, 'creator'); INSERT INTO "syncObjectTypes" VALUES(3, 'item'); INSERT INTO "syncObjectTypes" VALUES(4, 'search'); INSERT INTO "syncObjectTypes" VALUES(5, 'tag'); -INSERT INTO "syncObjectTypes" VALUES(6, 'relations'); +INSERT INTO "syncObjectTypes" VALUES(6, 'relation');