![Dan Stillman](/assets/img/avatar_default.png)
- Adds a per-library "Duplicate Items" virtual search to the source list -- shows up by default for "My Library" but can be added to and removed from all libraries - Current matching algorithm is very basic: finds exact title matches (after normalizing case/diacritics/punctuation/spacing) and DOI/ISBN matches (untested) - In duplicates view, sets are selected automatically; in other views, duplicate items can be selected manually and the merge interface can be brought up with "Merge Items" in the context menu - Can select a master item and individual fields to merge from other versions - Word processor integration code will automatically find mapped replacements and update documents with new item keys Possible future improvements: - Improved detection algorithms - UI tweaks - Currently if any items differ, all available versions will be shown as master item options, even if only one item is different; probably the earliest equivalent item should be shown for each distinct version - Caching of results for performance - Confidence scale - Creator version selection (currently the creators from the chosen master item are kept) - Merging of matching child items - Better sorting of duplicates if not clustered together by the selected sort column - Relation path compression when merging items that are already mapped to previously removed duplicates Other changes in this commit: - Don't show Trash in word processor integration windows - Consider items in trash to be missing in word processor documents - Selection of special views (Trash, Unfiled, Duplicates) is now restored properly in new windows - Disabled field transform context menu when item isn't editable - Left/right arrow now expands/collapses all selected items instead of just the last-selected row - Relation deletions are now synced - The same items row is now reselected after item deletion - (dev) Zotero.Item.getNotes(), Zotero.Item.getAttachments(), and Zotero.Item.getTags() now return empty arrays rather than FALSE if no matches -- tests on those return values in third-party code will need to be changed - (dev) New function Zotero.Utilities.removeDiacritics(str, lowercaseOnly) -- could be used to generate ASCII BibTeX keys - (dev) New 'tempTable' search condition can take a table to join against -- useful for implementing virtual source lists - (dev) Significant UI code cleanup - (dev) Moved all item pane content into itemPane.xul - Probably various other things Needless to say, this needs testing.
343 lines
8.6 KiB
JavaScript
343 lines
8.6 KiB
JavaScript
/*
|
|
***** 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.Notifier = new function(){
|
|
var _observers = new Zotero.Hash();
|
|
var _disabled = false;
|
|
var _types = [
|
|
'collection', 'creator', 'search', 'share', 'share-items', 'item',
|
|
'collection-item', 'item-tag', 'tag', 'group', 'bucket', 'relation'
|
|
];
|
|
var _inTransaction;
|
|
var _locked = false;
|
|
var _queue = [];
|
|
|
|
this.registerObserver = registerObserver;
|
|
this.unregisterObserver = unregisterObserver;
|
|
this.trigger = trigger;
|
|
this.untrigger = untrigger;
|
|
this.begin = begin;
|
|
this.commit = commit;
|
|
this.reset = reset;
|
|
this.disable = disable;
|
|
this.enable = enable;
|
|
this.isEnabled = isEnabled;
|
|
|
|
|
|
function registerObserver(ref, types){
|
|
if (types){
|
|
types = Zotero.flattenArguments(types);
|
|
|
|
for (var i=0; i<types.length; i++){
|
|
if (_types.indexOf(types[i]) == -1){
|
|
throw ('Invalid type ' + types[i] + ' in registerObserver()');
|
|
}
|
|
}
|
|
}
|
|
|
|
var len = 2;
|
|
var tries = 10;
|
|
do {
|
|
// Increase the hash length if we can't find a unique key
|
|
if (!tries){
|
|
len++;
|
|
tries = 10;
|
|
}
|
|
|
|
var hash = Zotero.randomString(len);
|
|
tries--;
|
|
}
|
|
while (_observers.get(hash));
|
|
|
|
Zotero.debug('Registering observer for '
|
|
+ (types ? '[' + types.join() + ']' : 'all types')
|
|
+ ' in notifier with hash ' + hash + "'", 4);
|
|
_observers.set(hash, {ref: ref, types: types});
|
|
return hash;
|
|
}
|
|
|
|
function unregisterObserver(hash){
|
|
Zotero.debug("Unregistering observer in notifier with hash '" + hash + "'", 4);
|
|
_observers.remove(hash);
|
|
}
|
|
|
|
/**
|
|
* Trigger a notification to the appropriate observers
|
|
*
|
|
* Possible values:
|
|
*
|
|
* 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', 'relation'
|
|
* ids - single id or array of ids
|
|
*
|
|
* Notes:
|
|
*
|
|
* - If event queuing is on, events will not fire until commit() is called
|
|
* unless _force_ is true.
|
|
*
|
|
* - New events and types should be added to the order arrays in commit()
|
|
**/
|
|
function trigger(event, type, ids, extraData, force){
|
|
if (_disabled){
|
|
Zotero.debug("Notifications are disabled");
|
|
return false;
|
|
}
|
|
|
|
if (_types && _types.indexOf(type) == -1){
|
|
throw ('Invalid type ' + type + ' in Notifier.trigger()');
|
|
}
|
|
|
|
switch (type) {
|
|
case 'modify':
|
|
case 'delete':
|
|
if (!extraData) {
|
|
throw ("Extra data must be supplied with Notifier type '" + type + "'");
|
|
}
|
|
}
|
|
|
|
ids = Zotero.flattenArguments(ids);
|
|
|
|
var queue = _inTransaction && !force;
|
|
|
|
Zotero.debug("Notifier.trigger('" + event + "', '" + type + "', " + '[' + ids.join() + '])'
|
|
+ (queue ? " queued" : " called " + "[observers: " + _observers.length + "]"));
|
|
|
|
// Merge with existing queue
|
|
if (queue) {
|
|
if (!_queue[type]) {
|
|
_queue[type] = [];
|
|
}
|
|
if (!_queue[type][event]) {
|
|
_queue[type][event] = {};
|
|
}
|
|
if (!_queue[type][event].ids) {
|
|
_queue[type][event].ids = [];
|
|
_queue[type][event].data = {};
|
|
}
|
|
|
|
// Merge ids
|
|
_queue[type][event].ids = _queue[type][event].ids.concat(ids);
|
|
|
|
// Merge extraData keys
|
|
if (extraData) {
|
|
for (var dataID in extraData) {
|
|
_queue[type][event].data[dataID] = extraData[dataID];
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
for (var i in _observers.items){
|
|
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
|
|
// one throws an error
|
|
try {
|
|
_observers.get(i).ref.notify(event, type, ids, extraData);
|
|
}
|
|
catch (e) {
|
|
Zotero.debug(e);
|
|
Components.utils.reportError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
function untrigger(event, type, ids) {
|
|
if (!_inTransaction) {
|
|
throw ("Zotero.Notifier.untrigger() called with no active event queue")
|
|
}
|
|
|
|
ids = Zotero.flattenArguments(ids);
|
|
|
|
for each(var id in ids) {
|
|
var index = _queue[type][event].ids.indexOf(id);
|
|
if (index == -1) {
|
|
Zotero.debug(event + '-' + type + ' id ' + id +
|
|
' not found in queue in Zotero.Notifier.untrigger()');
|
|
continue;
|
|
}
|
|
_queue[type][event].ids.splice(index, 1);
|
|
delete _queue[type][event].data[id];
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Begin queueing event notifications (i.e. don't notify the observers)
|
|
*
|
|
* _lock_ will prevent subsequent commits from running the queue until commit() is called
|
|
* with the _unlock_ being true
|
|
*
|
|
* Note: Be sure the matching commit() gets called (e.g. in a finally{...} block) or
|
|
* notifications will break until Firefox is restarted or commit(true)/reset() is called manually
|
|
*/
|
|
function begin(lock) {
|
|
if (lock && !_locked) {
|
|
_locked = true;
|
|
var unlock = true;
|
|
}
|
|
else {
|
|
var unlock = false;
|
|
}
|
|
|
|
if (_inTransaction) {
|
|
//Zotero.debug("Notifier queue already open", 4);
|
|
}
|
|
else {
|
|
Zotero.debug("Beginning Notifier event queue");
|
|
_inTransaction = true;
|
|
}
|
|
|
|
return unlock;
|
|
}
|
|
|
|
|
|
/*
|
|
* Send notifications for ids in the event queue
|
|
*
|
|
* If the queue is locked, notifications will only run if _unlock_ is true
|
|
*/
|
|
function commit(unlock) {
|
|
// If there's a lock on the event queue and _unlock_ isn't given, don't commit
|
|
if ((unlock == undefined && _locked) || (unlock != undefined && !unlock)) {
|
|
//Zotero.debug("Keeping Notifier event queue open", 4);
|
|
return;
|
|
}
|
|
|
|
var runQueue = [];
|
|
|
|
function sorter(a, b) {
|
|
return order.indexOf(b) - order.indexOf(a);
|
|
}
|
|
var order = ['collection', 'search', 'item', 'collection-item', 'item-tag', 'tag'];
|
|
_queue.sort();
|
|
|
|
var order = ['add', 'modify', 'remove', 'move', 'delete', 'trash'];
|
|
var totals = '';
|
|
for (var type in _queue) {
|
|
if (!runQueue[type]) {
|
|
runQueue[type] = [];
|
|
}
|
|
|
|
_queue[type].sort();
|
|
|
|
for (var event in _queue[type]) {
|
|
runQueue[type][event] = {
|
|
ids: [],
|
|
data: {}
|
|
};
|
|
|
|
// Remove redundant ids
|
|
for (var i=0; i<_queue[type][event].ids.length; i++) {
|
|
var id = _queue[type][event].ids[i];
|
|
var data = _queue[type][event].data[id];
|
|
|
|
// Don't send modify on nonexistent items or tags
|
|
if (event == 'modify') {
|
|
if (type == 'item' && !Zotero.Items.get(id)) {
|
|
continue;
|
|
}
|
|
else if (type == 'tag' && !Zotero.Tags.get(id)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (runQueue[type][event].ids.indexOf(id) == -1) {
|
|
runQueue[type][event].ids.push(id);
|
|
runQueue[type][event].data[id] = data;
|
|
}
|
|
}
|
|
|
|
if (runQueue[type][event].ids.length) {
|
|
totals += ' [' + event + '-' + type + ': ' + runQueue[type][event].ids.length + ']';
|
|
}
|
|
}
|
|
}
|
|
|
|
reset();
|
|
|
|
if (totals) {
|
|
Zotero.debug("Committing Notifier event queue" + totals);
|
|
|
|
for (var type in runQueue) {
|
|
for (var event in runQueue[type]) {
|
|
if (runQueue[type][event].ids.length) {
|
|
trigger(event, type, runQueue[type][event].ids,
|
|
runQueue[type][event].data, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Reset the event queue
|
|
*/
|
|
function reset() {
|
|
Zotero.debug("Resetting Notifier event queue");
|
|
_locked = false;
|
|
_queue = [];
|
|
_inTransaction = false;
|
|
}
|
|
|
|
|
|
//
|
|
// These should rarely be used now that we have event queuing
|
|
//
|
|
|
|
/*
|
|
* Disables Notifier notifications
|
|
*
|
|
* Returns false if the Notifier was already disabled, true otherwise
|
|
*/
|
|
function disable() {
|
|
if (_disabled) {
|
|
Zotero.debug('Notifier notifications are already disabled');
|
|
return false;
|
|
}
|
|
Zotero.debug('Disabling Notifier notifications');
|
|
_disabled = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
function enable() {
|
|
Zotero.debug('Enabling Notifier notifications');
|
|
_disabled = false;
|
|
}
|
|
|
|
|
|
function isEnabled() {
|
|
return !_disabled;
|
|
}
|
|
}
|