www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 3de1789f264930d581cd2e96b1a2d2df2b131e7e
parent ca91b534f2979a16341dd32a38fbe8b9c4ffa160
Author: Dan Stillman <dstillman@zotero.org>
Date:   Sun,  4 May 2008 08:32:48 +0000

Initial Zotero 1.5 Megacommit

Apologies for the massive (and, due to data_access.js splitting, difficult-to-follow) commit. Please note that external code that accesses the data layer may need to be tweaked for compatibility. Here's a comprehensive-as-possible changelog:

- Added server sync functionality (incomplete)
- Overhaul of data layer
  - Split data_access.js into separate files (item.js, items.js, creator.js, etc.)
  - Made creators and collections first-class objects, similar to items
  - Constructors now take id as first parameter, e.g. new Zotero.Item(1234, 'book'), to allow explicit id setting and id changing
  - Made various data layer operations (including attachment fields) require a save() rather than making direct DB changes
  - Better handling of unsaved objects
    - Item.setCreator() now takes creator objects instead of creator ids, and Item.save() will auto-save unsaved creators
    - clone() now works on unsaved objects
  - Newly created object instances are now disabled after save() to force refetch of globally accessible instance using Zotero.(Items|Creators|etc.).get()
  - Added secondary lookup key to data objects
  - Deprecated getID() and getItemType() methods in favor of .id and .itemTypeID properties
  - toArray() deprecated in favor of serialize(), which has a somewhat modified format
  - Added support for multiple creators with identical data -- currently unimplemented in interface and most of data layer
  - Added Item.diff() for comparing item metadata
- Database changes
  - Added SQLite triggers to enforce foreign key constraints
  - Added Zotero.DB.transactionVacuum flag to run a VACUUM after a transaction
  - Added Zotero.DB.transactionDate, .transactionDateTime, and transactionTimestamp to retrieve consistent timestamps for entire transaction
  - Properly store 64-bit integers
  - Set PRAGMA locking_mode=EXCLUSIVE on database
  - Set SQLite page size to 4096 on new databases
  - Set SQLite page cache to 8MB
  - Do some database cleanup and integrity checking on migration from 1.0 branch
  - Removed IF NOT EXISTS from userdata.sql CREATE statements -- userdata.sql is now processed only on DB initialization
  - Removed itemNoteTitles table and moved titles into itemNotes
- Abstracted metadata edit box and note box into flexible XBL bindings with various modes, including read-only states
- Massive speed-up of item tree view
- Several fixes from 1.0 branch for Fx3 compatibility
- Added Notifier observer to log delete events for syncing
- Zotero.Utilities changes
  - New methods getSQLDataType() and md5()
  - Removed onError from Zotero.Utilities.HTTP.doGet()
  - Don't display more than 1024 characters in doPost() debug output
  - Don't display passwords in doPost() debug output
- Added Zotero.Notifier.untrigger() -- currently unused
- Added Zotero.reloadDataObjects() to reset all in-memory objects
- Added |chars| parameter to Zotero.randomString(len, chars)
- Added Zotero.Date.getUnixTimestamp() and Date.toUnixTimestamp(JSDate)
- Adjusted zotero-service.js to simplify file inclusion

Various things (such as tags) are temporarily broken.



Diffstat:
Rchrome/content/zotero-platform/mac/itemPane.css -> chrome/content/zotero-platform/mac/itembox.css | 0
Rchrome/content/zotero-platform/unix/itemPane.css -> chrome/content/zotero-platform/unix/itembox.css | 0
Dchrome/content/zotero-platform/win/itemPane.css | 31-------------------------------
Achrome/content/zotero-platform/win/itembox.css | 31+++++++++++++++++++++++++++++++
Achrome/content/zotero/bindings/itembox.xml | 2076+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/bindings/merge.xml | 443+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/bindings/noteeditor.xml | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mchrome/content/zotero/browser.js | 20+++++++++++++++++---
Mchrome/content/zotero/fileInterface.js | 3++-
Mchrome/content/zotero/itemPane.js | 1571++++---------------------------------------------------------------------------
Mchrome/content/zotero/itemPane.xul | 52+++++-----------------------------------------------
Achrome/content/zotero/merge.js | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/merge.xul | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/note.js | 50++++++++++++++++++++++++++------------------------
Mchrome/content/zotero/note.xul | 2+-
Mchrome/content/zotero/overlay.js | 105++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mchrome/content/zotero/overlay.xul | 12+++++++++---
Mchrome/content/zotero/preferences/preferences.xul | 31+++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/attachments.js | 201++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mchrome/content/zotero/xpcom/cite.js | 6+++---
Mchrome/content/zotero/xpcom/collectionTreeView.js | 46++++++++++++++++++++++++++++------------------
Achrome/content/zotero/xpcom/data/cachedTypes.js | 273+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/collection.js | 930+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/collections.js | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/creator.js | 451+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/creators.js | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/item.js | 3392+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/itemFields.js | 391+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/items.js | 577+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/notes.js | 43+++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/tags.js | 418+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/data_access.js | 5191+------------------------------------------------------------------------------
Mchrome/content/zotero/xpcom/db.js | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mchrome/content/zotero/xpcom/file.js | 2+-
Mchrome/content/zotero/xpcom/fulltext.js | 44+++++++++++++++++++++++++++++---------------
Achrome/content/zotero/xpcom/id.js | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/itemTreeView.js | 274+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mchrome/content/zotero/xpcom/notifier.js | 48+++++++++++++++++++++++++++++++++++++++---------
Mchrome/content/zotero/xpcom/progressWindow.js | 3+++
Mchrome/content/zotero/xpcom/quickCopy.js | 1-
Mchrome/content/zotero/xpcom/schema.js | 284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mchrome/content/zotero/xpcom/search.js | 36++++++++++++++++++++----------------
Achrome/content/zotero/xpcom/sync.js | 1852+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/translate.js | 61++++++++++++++++++++++++++++++++++++++++++++-----------------
Mchrome/content/zotero/xpcom/utilities.js | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mchrome/content/zotero/xpcom/zotero.js | 55++++++++++++++++++++++++++++++++++++++++---------------
Achrome/skin/default/zotero/bindings/itembox.css | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/skin/default/zotero/bindings/noteeditor.css | 8++------
Dchrome/skin/default/zotero/itemPane.css | 85-------------------------------------------------------------------------------
Achrome/skin/default/zotero/merge.css | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/skin/default/zotero/overlay.css | 6------
Achrome/skin/default/zotero/prefs-sync.png | 0
Mchrome/skin/default/zotero/zotero.css | 14+++++++++++++-
Mcomponents/zotero-autocomplete.js | 14++++++++++----
Mcomponents/zotero-service.js | 108++++++++++---------------------------------------------------------------------
Mdefaults/preferences/zotero.js | 8++++++--
Msystem.sql | 14+++++++++++++-
Atriggers.sql | 659+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Muserdata.sql | 162+++++++++++++++++++++++++++++++++++--------------------------------------------
59 files changed, 14237 insertions(+), 7545 deletions(-)

diff --git a/chrome/content/zotero-platform/mac/itemPane.css b/chrome/content/zotero-platform/mac/itembox.css diff --git a/chrome/content/zotero-platform/unix/itemPane.css b/chrome/content/zotero-platform/unix/itembox.css diff --git a/chrome/content/zotero-platform/win/itemPane.css b/chrome/content/zotero-platform/win/itemPane.css @@ -1,31 +0,0 @@ -#zotero-editpane-dynamic-fields row > hbox, -#zotero-editpane-dynamic-fields row > vbox -{ - margin-top: 0 !important; - margin-bottom: 0 !important; - padding-top: 0 !important; - padding-bottom: 0 !important; -} - -#zotero-editpane-dynamic-fields row > hbox > hbox -{ - -moz-box-align: center; -} - -#zotero-editpane-dynamic-fields row hbox hbox label -{ - margin-top: 0; - margin-bottom: 0; -} - -#zotero-editpane-dynamic-fields row > toolbarbutton -{ - margin-right: 5px; - -moz-image-region: rect(2px, 14px, 18px, 0px); -} - -#zotero-editpane-dynamic-fields row vbox[fieldname=abstractNote], -#zotero-editpane-dynamic-fields row vbox[fieldname=extra] -{ - margin-left: 1px; -} diff --git a/chrome/content/zotero-platform/win/itembox.css b/chrome/content/zotero-platform/win/itembox.css @@ -0,0 +1,31 @@ +row > hbox, +row > vbox +{ + margin-top: 0 !important; + margin-bottom: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +row > hbox > hbox +{ + -moz-box-align: center; +} + +row hbox hbox label +{ + margin-top: 0; + margin-bottom: 0; +} + +row > toolbarbutton +{ + margin-right: 5px; + -moz-image-region: rect(2px, 14px, 18px, 0px); +} + +row vbox[fieldname=abstractNote], +row vbox[fieldname=extra] +{ + margin-left: 1px; +} diff --git a/chrome/content/zotero/bindings/itembox.xml b/chrome/content/zotero/bindings/itembox.xml @@ -0,0 +1,2076 @@ +<?xml version="1.0"?> +<!-- + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +--> + +<!DOCTYPE bindings SYSTEM "chrome://zotero/locale/zotero.dtd"> +<!-- <!DOCTYPE bindings SYSTEM "chrome://zotero/locale/itembox.dtd"> --> + +<bindings xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="item-box"> + <resources> + <stylesheet src="chrome://zotero/skin/bindings/itembox.css"/> + <stylesheet src="chrome://zotero-platform/content/itembox.css"/> + </resources> + + <implementation> + <!-- + Public properties + --> + <field name="clickable">false</field> + <field name="editable">false</field> + <field name="saveOnEdit">false</field> + <field name="displayGoButtons">false</field> + <field name="showTypeMenu">false</field> + <field name="hideEmptyFields">false</field> + <field name="clickByRow">false</field> <!-- Click entire row rather than just field value --> + <field name="clickByItem">false</field> + + <field name="clickHandler"/> + <field name="blurHandler"/> + + <!-- Modes are predefined settings groups for particular tasks --> + <field name="_mode">"view"</field> + <property name="mode" onget="return this._mode;"> + <setter> + <![CDATA[ + this.clickable = false; + this.editable = false; + this.saveOnEdit = false; + this.displayGoButtons = false; + this.showTypeMenu = false; + this.hideEmptyFields = false; + this.clickByRow = false; + this.clickByItem = false; + + switch (val) { + case 'view': + break; + + case 'edit': + this.clickable = true; + this.editable = true; + this.saveOnEdit = true + this.displayGoButtons = true; + this.showTypeMenu = true; + this.clickHandler = this.showEditor; + this.blurHandler = this.hideEditor; + break; + + case 'merge': + //this.hideEmptyFields = true; + this.clickByItem = true; + break; + + case 'mergeedit': + this.clickable = true; + this.editable = true; + this.saveOnEdit = false; + this.displayGoButtons = false; + this.showTypeMenu = true; + this.clickHandler = this.showEditor; + this.blurHandler = this.hideEditor; + break; + + default: + throw ("Invalid mode '" + val + "' in itembox.xml"); + } + + this._mode = val; + document.getAnonymousNodes(this)[0].setAttribute('mode', val); + ]]> + </setter> + </property> + + <field name="_item"/> + <property name="item" + onget="return this._item;" + onset="this._item = val; this.refresh();"> + </property> + + <!-- .ref is an alias for .item --> + <property name="ref" + onget="return this._item;" + onset="this._item = val; this.refresh();"> + </property> + + + <!-- + An array of field names that should be shown + even if they're empty and hideEmptyFields is set + --> + <field name="_visibleFields">[]</field> + <property name="visibleFields"> + <setter> + <![CDATA[ + if (val.constructor.name != 'Array') { + throw ('visibleFields must be an array in <itembox>.visibleFields'); + } + + this._visibleFields = val; + ]]> + </setter> + </property> + + <!-- + An array of field names that should be clickable + even if this.clickable is false + --> + <field name="_clickableFields">[]</field> + <property name="clickableFields"> + <setter> + <![CDATA[ + if (val.constructor.name != 'Array') { + throw ('clickableFields must be an array in <itembox>.clickableFields'); + } + + this._clickableFields = val; + ]]> + </setter> + </property> + + <!-- + An array of field names that should be editable + even if this.editable is false + --> + <field name="_editableFields">[]</field> + <property name="editableFields"> + <setter> + <![CDATA[ + if (val.constructor.name != 'Array') { + throw ('editableFields must be an array in <itembox>.editableFields'); + } + + this._editableFields = val; + ]]> + </setter> + </property> + + + <!-- + An array of field names in the order they should appear + in the list; empty spaces can be created with null + --> + <field name="_fieldOrder">[]</field> + <property name="fieldOrder"> + <setter> + <![CDATA[ + if (val.constructor.name != 'Array') { + throw ('fieldOrder must be an array in <itembox>.fieldOrder'); + } + + this._fieldOrder = val; + ]]> + </setter> + </property> + + + <!-- Private properties --> + <property name="_dynamicFields" onget="return this._id('dynamic-fields')"/> + <property name="_itemTypeMenu" onget="return this._id('item-type-menu')"/> + <property name="_creatorTypeMenu" onget="return this._id('creator-type-menu')"/> + + <field name="_selectField"/> + <field name="_beforeRow"/> + <field name="_activeScrollbox"/> + <field name="_addCreatorRow"/> + <field name="_creatorCount"/> + + <field name="_lastTabIndex"/> + <field name="_tabDirection"/> + <field name="_tabIndexMinCreators" readonly="true">10</field> + <field name="_tabIndexMaxCreators" readonly="true">0</field> + <field name="_tabIndexMinFields" readonly="true">1000</field> + <field name="_tabIndexMaxInfoFields" readonly="true">0</field> + <field name="_tabIndexMaxTagsFields" readonly="true">0</field> + + <property name="_defaultFirstName" + onget="return '(' + Zotero.getString('pane.item.defaultFirstName') + ')'"/> + <property name="_defaultLastName" + onget="return '(' + Zotero.getString('pane.item.defaultLastName') + ')'"/> + <property name="_defaultFullName" + onget="return '(' + Zotero.getString('pane.item.defaultFullName') + ')'"/> + + <method name="refresh"> + <body> + <![CDATA[ + Zotero.debug('Refreshing item box'); + + if (this.clickByItem) { + var itemBox = document.getAnonymousNodes(this)[0]; + itemBox.setAttribute('onclick', + 'document.getBindingParent(this).clickHandler(this)'); + } + + if (this.displayGoButtons) { + // Enable/disable "View =>" button + testView: try { + var viewButton = document.getElementById('view-button'); + + viewButton.removeAttribute('viewSnapshot'); + viewButton.removeAttribute('viewURL'); + viewButton.setAttribute('label', + Zotero.getString('pane.item.goToURL.online.label')); + viewButton.setAttribute('tooltiptext', + Zotero.getString('pane.item.goToURL.online.tooltip')); + + var spec = false, validURI = false; + + var uri = Components.classes["@mozilla.org/network/standard-url;1"]. + createInstance(Components.interfaces.nsIURI); + + // First try to find a snapshot matching the item's URL field + var snapID = this.item.getBestSnapshot(); + if (snapID) { + spec = Zotero.Items.get(snapID).getLocalFileURL(); + uri.spec = spec; + if (!uri.scheme || uri.scheme != 'file') { + snapID = false; + spec = false; + } + } + + // If that fails, try the URL field itself + if (!spec) { + spec = this.item.getField('url'); + uri.spec = spec; + if (!(uri.scheme && (uri.host || uri.scheme == 'file'))) { + spec = false; + } + } + + if (!spec) { + break testView; + } + + validURI = true; + + if (snapID) { + viewButton.setAttribute('label', + Zotero.getString('pane.item.goToURL.snapshot.label')); + viewButton.setAttribute('tooltiptext', + Zotero.getString('pane.item.goToURL.snapshot.tooltip')); + viewButton.setAttribute('viewSnapshot', snapID); + } + else { + viewButton.setAttribute('viewURL', spec); + } + } + catch (e) { + Zotero.debug(e); + } + viewButton.setAttribute('disabled', !validURI); + + // Enable/disable "Locate =>" (OpenURL) button + switch (this.item.itemTypeID) + { + // DEBUG: handle descendents of these types as well? + case Zotero.ItemTypes.getID('book'): + case Zotero.ItemTypes.getID('bookSection'): + case Zotero.ItemTypes.getID('journalArticle'): + case Zotero.ItemTypes.getID('thesis'): + var openURL = true; + break; + + default: + var openURL = false; + } + document.getElementById('openurl-button').setAttribute('disabled', !openURL); + + this._id('go-buttons').hidden = false; + } + else { + this._id('go-buttons').hidden = true; + } + + // Item type menu + if (this.showTypeMenu) { + // Build item type menu if it hasn't been built yet + if (!this._itemTypeMenu.firstChild.hasChildNodes()) { + var itemTypes = Zotero.ItemTypes.getTypes(); + for (var i=0; i<itemTypes.length; i++) { + var name = itemTypes[i].name; + if (name != 'attachment' && name != 'note') { + this._itemTypeMenu.appendItem(Zotero.getString("itemTypes." + name), itemTypes[i].id); + } + } + } + + var listitems = this._itemTypeMenu.firstChild.childNodes; + for (var i=0, len=listitems.length; i < len; i++) { + if (listitems[i].getAttribute('value') == this.item.itemTypeID) { + this._itemTypeMenu.selectedIndex = i; + } + } + + this._itemTypeMenu.parentNode.hidden = false; + } + else { + this._itemTypeMenu.parentNode.hidden = true; + } + + + // + // Clear and rebuild metadata fields + // + while (this._dynamicFields.hasChildNodes()) { + this._dynamicFields.removeChild(this._dynamicFields.firstChild); + } + + var fieldNames = []; + + // Manual field order + if (this._fieldOrder.length) { + for each(var field in this._fieldOrder) { + fieldNames.push(field); + } + } + // Get field order from database + else { + if (!this.showTypeMenu) { + fieldNames.push("itemType"); + } + + var fields = Zotero.ItemFields.getItemTypeFields(this.item.getField("itemTypeID")); + + for (var i=0; i<fields.length; i++) { + fieldNames.push(Zotero.ItemFields.getName(fields[i])); + } + + fieldNames.push("dateAdded", "dateModified"); + } + + for (var i=0; i<fieldNames.length; i++) { + var fieldName = fieldNames[i]; + var val = ''; + + if (fieldName) { + var fieldID = Zotero.ItemFields.getID(fieldName); + if (fieldID && !Zotero.ItemFields.isValidForType(fieldID, this.item.itemTypeID)) { + fieldName = null; + } + } + + if (fieldName) { + // createValueElement() adds the itemTypeID as an attribute + // and converts it to a localized string for display + if (fieldName == 'itemType') { + val = this.item.getField('itemTypeID'); + } + else { + val = this.item.getField(fieldName); + } + + var fieldIsClickable = this._fieldIsClickable(fieldName); + + if (!val && this.hideEmptyFields + && this._visibleFields.indexOf(fieldName) == -1) { + continue; + } + + // Start tabindex at 1000 after creators + var tabindex = fieldIsClickable + ? (i>0 ? this._tabIndexMinFields + i : 1) : 0; + this._tabIndexMaxInfoFields = Math.max(this._tabIndexMaxInfoFields, tabindex); + + if (fieldIsClickable && + !this.item.isPrimaryField(fieldName) && + Zotero.ItemFields.isFieldOfBase(Zotero.ItemFields.getID(fieldName), 'date')) { + this.addDateRow(fieldNames[i], this.item.getField(fieldName, true), tabindex); + continue; + } + } + + var valueElement = this.createValueElement( + val, fieldName, tabindex + ); + + var label = document.createElement("label"); + label.setAttribute('fieldname', fieldName); + + var prefix = ''; + // Add '(...)' before 'Abstract:' for collapsed abstracts + if (fieldName == 'abstractNote') { + if (val && !Zotero.Prefs.get('lastAbstractExpand')) { + prefix = '(...) '; + } + } + + if (fieldName) { + label.setAttribute("value", prefix + + Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, fieldName) + ":"); + } + + if (fieldName == 'url' && val) { + label.setAttribute("isButton", true); + // TODO: make getFieldValue non-private and use below instead + label.setAttribute("onclick", "ZoteroPane.loadURI(this.nextSibling.firstChild ? this.nextSibling.firstChild.nodeValue : this.nextSibling.value, event)"); + label.setAttribute("tooltiptext", Zotero.getString('pane.item.goToURL.online.tooltip')); + } + else if (fieldName == 'abstractNote') { + label.setAttribute("onclick", + "if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); } " + + "else { document.getBindingParent(this).toggleAbstractExpand(this); }"); + } + else { + label.setAttribute("onclick", + "if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); }"); + } + + this.addDynamicRow(label, valueElement); + + if (fieldName && this._selectField == fieldName) { + this.showEditor(valueElement); + } + } + this._selectField = false; + + // + // Creators + // + + // Creator type menu + if (this.editable) { + while (this._creatorTypeMenu.hasChildNodes()) { + this._creatorTypeMenu.removeChild(this._creatorTypeMenu.firstChild); + } + + var creatorTypes = Zotero.CreatorTypes.getTypesForItemType(this.item.itemTypeID); + var localized = {}; + for (var i=0; i<creatorTypes.length; i++) { + localized[creatorTypes[i]['name']] + = Zotero.getString('creatorTypes.' + creatorTypes[i]['name']); + } + + for (var i in localized) { + var menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", localized[i]); + menuitem.setAttribute("typeid", Zotero.CreatorTypes.getID(i)); + this._creatorTypeMenu.appendChild(menuitem); + } + } + + // Creator rows + + // Place, in order of preference, after title, after type, + // or at beginning + var field = this._dynamicFields.getElementsByAttribute('fieldname', 'title').item(0); + if (!field) { + var field = this._dynamicFields.getElementsByAttribute('fieldname', 'itemType').item(0); + } + if (field) { + this._beforeRow = field.parentNode.nextSibling; + } + else { + this._beforeRow = this._dynamicFields.firstChild; + } + + this._creatorCount = 0; + if (this.item.numCreators() > 0) { + for (var i = 0, len=this.item.numCreators(); i<len; i++) { + this.addCreatorRow(this.item.getCreator(i).ref, + this.item.getCreator(i).creatorTypeID); + } + + if (this._addCreatorRow) { + this.addCreatorRow(false, false, true); + this._addCreatorRow = false; + } + } + else if (this.editable) { + // Add default row + this.addCreatorRow(false, false, true, true); + } + + + var focusMode = 'info'; + var focusBox = this._dynamicFields; + ]]> + </body> + </method> + + + <method name="addDynamicRow"> + <parameter name="label"/> + <parameter name="value"/> + <parameter name="beforeElement"/> + <body> + <![CDATA[ + var row = document.createElement("row"); + + // Add click event to row + if (this._rowIsClickable(value.getAttribute('fieldname'))) { + row.className = 'zotero-clicky'; + row.addEventListener('click', function (event) { + document.getBindingParent(this).clickHandler(this); + }, false); + } + + row.appendChild(label); + row.appendChild(value); + if (beforeElement) { + this._dynamicFields.insertBefore(row, this._beforeRow); + } + else { + this._dynamicFields.appendChild(row); + } + ]]> + </body> + </method> + + + <method name="addCreatorRow"> + <parameter name="creator"/> + <parameter name="creatorTypeID"/> + <parameter name="unsaved"/> + <parameter name="defaultRow"/> + <body> + <![CDATA[ + if (!creator) { + creator = { + firstName: '', + lastName: '', + fieldMode: Zotero.Prefs.get('lastCreatorFieldMode') + }; + } + + // Disable the "+" button on previous rows + var elems = this._dynamicFields.getElementsByAttribute('value', '+'); + if (elems.length) { + this.disableButton(elems[elems.length-1]); + } + + if (creator.fieldMode == 1) { + var firstName = ''; + var lastName = creator.lastName ? creator.lastName : this._defaultFullName; + } + else { + var firstName = creator.firstName ? creator.firstName : this._defaultFirstName; + var lastName = creator.lastName ? creator.lastName : this._defaultLastName; + } + + // Use the first entry in the drop-down for the default type if none specified + var typeID = creatorTypeID ? + creatorTypeID : this._creatorTypeMenu.childNodes[0].getAttribute('typeid'); + + var label = document.createElement("toolbarbutton"); + label.setAttribute("label", + Zotero.getString('creatorTypes.' + + Zotero.CreatorTypes.getName(typeID)) + ":"); + label.setAttribute("typeid", typeID); + label.setAttribute("popup", "creator-type-menu"); + label.setAttribute("fieldname", 'creator-' + this._creatorCount + '-typeID'); + label.className = 'zotero-clicky'; + + // getCreatorFields(), switchCreatorMode() and handleCreatorAutoCompleteSelect() + // may need need to be adjusted if this DOM structure changes + var hbox = document.createElement("hbox"); + + // Name + var firstlast = document.createElement("hbox"); + firstlast.setAttribute("flex","1"); + var tabindex = this._tabIndexMinCreators + (this._creatorCount * 2); + var fieldName = 'creator-' + this._creatorCount + '-lastName'; + var lastNameLabel = firstlast.appendChild( + this.createValueElement( + lastName, + fieldName, + tabindex + ) + ); + + // Comma + var comma = document.createElement('label'); + comma.setAttribute('value', ','); + comma.className = 'comma'; + firstlast.appendChild(comma); + + var fieldName = 'creator-' + this._creatorCount + '-firstName'; + firstlast.appendChild( + this.createValueElement( + firstName, + fieldName, + tabindex + 1 + ) + ); + if (creator.fieldMode) { + firstlast.lastChild.setAttribute('hidden', true); + } + this._tabIndexMaxCreators = Math.max(this._tabIndexMaxCreators, tabindex); + + hbox.appendChild(firstlast); + + // Single/double field toggle + var toggleButton = document.createElement('toolbarbutton'); + toggleButton.setAttribute('fieldname', + 'creator-' + this._creatorCount + '-fieldMode'); + toggleButton.className = 'zotero-clicky'; + hbox.appendChild(toggleButton); + + // Minus (-) button + var removeButton = document.createElement('label'); + removeButton.setAttribute("value","-"); + // If default first row, don't let user remove it + if (defaultRow) { + this.disableButton(removeButton); + } + else { + removeButton.setAttribute("class","zotero-clicky"); + removeButton.setAttribute("onclick", + "document.getBindingParent(this).removeCreator(" + + this._creatorCount + + ", this.parentNode.parentNode)"); + } + hbox.appendChild(removeButton); + + // Plus (+) button + var addButton = document.createElement('label'); + addButton.setAttribute("value","+"); + addButton.setAttribute("class", "zotero-clicky"); + // If row isn't saved, don't let user add more + if (unsaved) { + this.disableButton(addButton); + } + else { + this._enablePlusButton(addButton, typeID, creator.fieldMode); + } + hbox.appendChild(addButton); + + this._creatorCount++; + + if (!this.editable) { + toggleButton.hidden = true; + removeButton.hidden = true; + addButton.hidden = true; + } + + this.addDynamicRow(label, hbox, true); + + // Set single/double field toggle mode + if (creator.fieldMode) { + this.switchCreatorMode(hbox.parentNode, 1, true); + } + else { + this.switchCreatorMode(hbox.parentNode, 0, true); + } + + // Focus new rows + if (unsaved && !defaultRow){ + lastNameLabel.click(); + } + ]]> + </body> + </method> + + + <method name="addDateRow"> + <parameter name="field"/> + <parameter name="value"/> + <parameter name="tabindex"/> + <body> + <![CDATA[ + var label = document.createElement("label"); + label.setAttribute("value", Zotero.getString("itemFields." + field) + ':'); + label.setAttribute("fieldname", field); + label.setAttribute("onclick", "this.nextSibling.firstChild.blur()"); + + var hbox = document.createElement("hbox"); + var elem = this.createValueElement( + Zotero.Date.multipartToStr(value), + field, + tabindex + ); + + // y-m-d status indicator + var datebox = document.createElement('hbox'); + datebox.className = 'zotero-date-field-status'; + var year = document.createElement('label'); + var month = document.createElement('label'); + var day = document.createElement('label'); + year.setAttribute('value', Zotero.getString('date.abbreviation.year')); + month.setAttribute('value', Zotero.getString('date.abbreviation.month')); + day.setAttribute('value', Zotero.getString('date.abbreviation.day')); + + // Display the date parts we have and hide the others + var sqldate = Zotero.Date.multipartToSQL(value); + year.setAttribute('hidden', !Zotero.Date.sqlHasYear(sqldate)); + month.setAttribute('hidden', !Zotero.Date.sqlHasMonth(sqldate)); + day.setAttribute('hidden', !Zotero.Date.sqlHasDay(sqldate)); + + datebox.appendChild(year); + datebox.appendChild(month); + datebox.appendChild(day); + + var hbox = document.createElement('hbox'); + hbox.setAttribute('flex', 1); + hbox.appendChild(elem); + hbox.appendChild(datebox); + + this.addDynamicRow(label, hbox); + ]]> + </body> + </method> + + + <method name="switchCreatorMode"> + <parameter name="row"/> + <parameter name="fieldMode"/> + <parameter name="initial"/> + <body> + <![CDATA[ + // Change if button position changes + // row->hbox->label->label->toolbarbutton + var button = row.lastChild.lastChild.previousSibling.previousSibling; + var hbox = button.previousSibling; + var lastName = hbox.firstChild; + var comma = hbox.firstChild.nextSibling; + var firstName = hbox.lastChild; + + // Switch to single-field mode + if (fieldMode == 1) { + button.setAttribute('image', 'chrome://zotero/skin/textfield-dual.png'); + button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.two')); + lastName.setAttribute('fieldMode', '1'); + button.setAttribute('onclick', "document.getBindingParent(this).switchCreatorMode(this.parentNode.parentNode, 0)"); + lastName.setAttribute('flex', '1'); + + // Remove firstname field from tabindex + var tab = parseInt(firstName.getAttribute('ztabindex')); + firstName.setAttribute('ztabindex', -1); + if (this._tabIndexMaxCreators == tab) { + this._tabIndexMaxCreators--; + } + + // Hide first name field and prepend to last name field + firstName.setAttribute('hidden', true); + comma.setAttribute('hidden', true); + + if (!initial) { + var first = this._getFieldValue(firstName); + if (first && first != this._defaultFirstName) { + var last = this._getFieldValue(lastName); + this._setFieldValue(lastName, first + ' ' + last); + } + } + + if (this._getFieldValue(lastName) == this._defaultLastName) { + this._setFieldValue(lastName, this._defaultFullName); + } + } + // Switch to two-field mode + else { + button.setAttribute('image', 'chrome://zotero/skin/textfield-single.png'); + button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.one')); + lastName.setAttribute('fieldMode', '0'); + button.setAttribute('onclick', "document.getBindingParent(this).switchCreatorMode(this.parentNode.parentNode, 1)"); + lastName.setAttribute('flex', '0'); + + // Add firstname field to tabindex + var tab = parseInt(lastName.getAttribute('ztabindex')); + firstName.setAttribute('ztabindex', tab + 1); + if (this._tabIndexMaxCreators == tab) + { + this._tabIndexMaxCreators++; + } + + if (!initial) { + // Move all but last word to first name field and show it + var last = this._getFieldValue(lastName); + if (last && last != this._defaultFullName) { + var lastNameRE = /(.*?)[ ]*([^ ]+[ ]*)$/; + var parts = lastNameRE.exec(last); + if (parts[2] && parts[2] != last) + { + this._setFieldValue(lastName, parts[2]); + this._setFieldValue(firstName, parts[1]); + } + } + } + + if (!this._getFieldValue(firstName)) { + this._setFieldValue(firstName, this._defaultFirstName); + } + + if (this._getFieldValue(lastName) == this._defaultFullName) { + this._setFieldValue(lastName, this._defaultLastName); + } + + firstName.setAttribute('hidden', false); + comma.setAttribute('hidden', false); + } + + // Save the last-used field mode + Zotero.debug("Switching lastCreatorFieldMode to " + fieldMode); + Zotero.Prefs.set('lastCreatorFieldMode', fieldMode); + + if (!initial) + { + var index = button.getAttribute('fieldname').split('-')[1]; + var fields = this.getCreatorFields(row); + fields.fieldMode = fieldMode; + this.modifyCreator(index, fields); + } + ]]> + </body> + </method> + + + <method name="scrollToTop"> + <body> + <![CDATA[ + if (!this._activeScrollbox) { + return; + } + var sbo = this._activeScrollbox.boxObject; + sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject); + sbo.scrollTo(0,0); + ]]> + </body> + </method> + + + <method name="ensureElementIsVisible"> + <parameter name="elem"/> + <body> + <![CDATA[ + if (!this._activeScrollbox) { + return; + } + var sbo = this._activeScrollbox.boxObject; + sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject); + sbo.ensureElementIsVisible(elem); + ]]> + </body> + </method> + + + <method name="changeTypeTo"> + <parameter name="itemTypeID"/> + <parameter name="menu"/> + <body> + <![CDATA[ + if (itemTypeID == this.item.itemTypeID) { + return true; + } + + var fieldsToDelete = this.item.getFieldsNotInType(itemTypeID, true); + + // Generate list of localized field names for display in pop-up + if (fieldsToDelete) { + var fieldNames = ""; + for (var i=0; i<fieldsToDelete.length; i++) { + fieldNames += "\n - " + + Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, fieldsToDelete[i]); + } + + var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + } + + if (!fieldsToDelete || + promptService.confirm(null, + Zotero.getString('pane.item.changeType.title'), + Zotero.getString('pane.item.changeType.text') + "\n" + fieldNames)) { + this.item.setType(itemTypeID); + + if (this.saveOnEdit) { + this.item.save(); + } + else { + this.refresh(); + } + + return true; + } + + // Revert the menu (which changes before the pop-up) + if (menu) { + menu.value = this.item.itemTypeID; + } + + return false; + ]]> + </body> + </method> + + + <method name="onViewClick"> + <parameter name="button"/> + <parameter name="event"/> + <body> + <![CDATA[ + if (button.getAttribute('viewURL')) { + ZoteroPane.loadURI(button.getAttribute('viewURL'), event); + } + else if (button.getAttribute('viewSnapshot')) { + ZoteroPane.viewAttachment(button.getAttribute('viewSnapshot'), event); + } + ]]> + </body> + </method> + + + <method name="onOpenURLClick"> + <parameter name="event"/> + <body> + <![CDATA[ + var url = Zotero.OpenURL.resolve(this.item); + if (url) { + ZoteroPane.loadURI(url, event); + } + ]]> + </body> + </method> + + + <method name="toggleAbstractExpand"> + <parameter name="label"/> + <body> + <![CDATA[ + var cur = Zotero.Prefs.get('lastAbstractExpand'); + Zotero.Prefs.set('lastAbstractExpand', !cur); + + var ab = label.nextSibling; + var valueText = this.item.getField('abstractNote'); + var tabindex = ab.getAttribute('ztabindex'); + var elem = this.createValueElement( + valueText, + 'abstractNote', + tabindex + ); + ab.parentNode.replaceChild(elem, ab); + + var text = Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, 'abstractNote') + ':'; + // Add '(...)' before "Abstract:" for collapsed abstracts + if (valueText && cur) { + text = '(...) ' + text; + } + label.setAttribute('value', text); + ]]> + </body> + </method> + + + <method name="disableButton"> + <parameter name="button"/> + <body> + <![CDATA[ + button.setAttribute('disabled', true); + button.setAttribute('onclick', false); + ]]> + </body> + </method> + + + <method name="_enablePlusButton"> + <parameter name="button"/> + <parameter name="creatorTypeID"/> + <parameter name="fieldMode"/> + <body> + <![CDATA[ + button.setAttribute('disabled', false); + button.setAttribute("onclick", + "document.getBindingParent(this).disableButton(this); " + + "var creator = new Zotero.Creator; " + + "creator.fieldMode = " + (fieldMode ? fieldMode : 0) + "; " + + "document.getBindingParent(this).addCreatorRow(creator, " + + (creatorTypeID ? creatorTypeID : 'false') + ", true);"); + ]]> + </body> + </method> + + + + + + + <method name="createValueElement"> + <parameter name="valueText"/> + <parameter name="fieldName"/> + <parameter name="tabindex"/> + <body> + <![CDATA[ + if (fieldName) { + var fieldID = Zotero.ItemFields.getID(fieldName); + } + + // If an abstract, check last expand state + var abstractAsVbox = (fieldName == 'abstractNote') && + Zotero.Prefs.get('lastAbstractExpand'); + + if (fieldName == 'extra' || abstractAsVbox) { + var valueElement = document.createElement("vbox"); + } + else { + var valueElement = document.createElement("label"); + } + + valueElement.setAttribute('fieldname', fieldName); + valueElement.setAttribute('flex', 1); + + if (this._fieldIsClickable(fieldName)) { + valueElement.setAttribute('ztabindex', tabindex); + valueElement.addEventListener('click', function (event) { + /* Skip right-click on Windows */ + if (event.button) { + return; + } + document.getBindingParent(this).clickHandler(this); + }, false); + valueElement.className = 'zotero-clicky'; + } + + switch (fieldName) { + case 'itemType': + valueElement.setAttribute('itemTypeID', valueText); + valueText = Zotero.ItemTypes.getLocalizedString(valueText); + break; + + case 'tag': + this._tabIndexMaxTagsFields = Math.max(this._tabIndexMaxTagsFields, tabindex); + break; + + // Convert dates from UTC + case 'dateAdded': + case 'dateModified': + case 'accessDate': + if (valueText) { + var date = Zotero.Date.sqlToDate(valueText, true); + valueText = date ? date.toLocaleString() : ''; + + // Don't show time for access date if none + if (fieldName == 'accessDate') { + valueText = valueText.replace('00:00:00 ', ''); + } + } + break; + } + + if (fieldID) { + // Display the SQL date as a tooltip for date fields + if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { + valueElement.setAttribute('tooltiptext', + Zotero.Date.multipartToSQL(this.item.getField(fieldName, true))); + } + + // Display a context menu for certain fields + if (fieldName == 'seriesTitle' || fieldName == 'shortTitle' || + Zotero.ItemFields.isFieldOfBase(fieldID, 'title') || + Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle')) { + valueElement.setAttribute('contextmenu', 'field-menu'); + } + } + + + if (fieldName && fieldName.indexOf('firstName') != -1) { + valueElement.setAttribute('flex', '1'); + } + + var firstSpace; + if (typeof valueText == 'string') { + firstSpace = valueText.indexOf(" "); + } + + // To support newlines in 'extra' fields, we use multiple + // <description> elements inside a vbox + if (fieldName == 'extra' || abstractAsVbox) { + var lines = valueText.split("\n"); + for (var i = 0; i < lines.length; i++) { + var descriptionNode = document.createElement("description"); + var linetext = document.createTextNode(lines[i]); + descriptionNode.appendChild(linetext); + valueElement.appendChild(descriptionNode); + } + } + // 29 == arbitrary length at which to chop uninterrupted text + else if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29 + || (fieldName && + (fieldName.substr(0, 7) == 'creator') || fieldName == 'abstractNote')) { + if (fieldName == 'abstractNote') { + valueText = valueText.replace(/[\t\n]/g, ' '); + } + valueElement.setAttribute('crop', 'end'); + valueElement.setAttribute('value',valueText); + } + else { + // Wrap to multiple lines + valueElement.appendChild(document.createTextNode(valueText)); + } + + return valueElement; + ]]> + </body> + </method> + + + <method name="removeCreator"> + <parameter name="index"/> + <parameter name="labelToDelete"/> + <body> + <![CDATA[ + // If unsaved row, just remove element + if (!this.item.hasCreatorAt(index)) { + labelToDelete.parentNode.removeChild(labelToDelete); + + // Enable the "+" button on the previous row + var elems = this._dynamicFields.getElementsByAttribute('value', '+'); + var button = elems[elems.length-1]; + var creatorFields = this.getCreatorFields(Zotero.getAncestorByTagName(button, 'row')); + this._enablePlusButton(button, creatorFields.typeID, creatorFields.fieldMode); + + this._creatorCount--; + return; + } + this.item.removeCreator(index); + this.item.save(); + ]]> + </body> + </method> + + + <method name="showEditor"> + <parameter name="elem"/> + <body> + <![CDATA[ + // Blur any active fields + if (this._dynamicFields) { + this._dynamicFields.focus(); + } + + Zotero.debug('Showing editor'); + + var fieldName = elem.getAttribute('fieldname'); + var tabindex = elem.getAttribute('ztabindex'); + + var [field, creatorIndex, creatorField] = fieldName.split('-'); + if (field == 'creator') { + var c = this.item.getCreator(creatorIndex); + var value = c ? c.ref[creatorField] : ''; + var itemID = this.item.id; + } + else if (fieldName=='tag') { + var tagID = elem.parentNode.getAttribute('id').split('-')[1]; + var value = tagID ? Zotero.Tags.getName(tagID) : ''; + var itemID = Zotero.getAncestorByTagName(elem, 'tagsbox').item.id; + } + else { + var value = this.item.getField(fieldName); + var itemID = this.item.id; + + // Access date needs to be converted from UTC + if (fieldName=='accessDate' && value!='') { + var localDate = Zotero.Date.sqlToDate(value, true); + var value = Zotero.Date.dateToSQL(localDate); + } + } + + var t = document.createElement("textbox"); + t.setAttribute('value', value); + t.setAttribute('fieldname', fieldName); + t.setAttribute('ztabindex', tabindex); + t.setAttribute('flex', '1'); + + if (creatorField=='lastName') { + t.setAttribute('fieldMode', elem.getAttribute('fieldMode')); + } + + if (['title', 'abstractNote', 'extra'].indexOf(fieldName) != -1) { + t.setAttribute('multiline', true); + t.setAttribute('rows', 8); + } + else { + var autoCompleteFields = [ + 'creator', + 'journalAbbreviation', + 'seriesTitle', + 'seriesText', + 'repository', + 'callNumber', + 'archiveLocation', + 'language', + 'rights', + 'tag' + ]; + + // Add the type-specific versions of these base fields + var baseACFields = ['publisher', 'publicationTitle', 'type', + 'medium', 'place']; + autoCompleteFields = autoCompleteFields.concat(baseACFields); + + for (var i=0; i<baseACFields.length; i++) { + var add = Zotero.ItemFields.getTypeFieldsFromBase(baseACFields[i], true) + autoCompleteFields = autoCompleteFields.concat(add); + } + + // Add auto-complete for certain fields + if (autoCompleteFields.indexOf(field) != -1) { + t.setAttribute('type', 'autocomplete'); + t.setAttribute('autocompletesearch', 'zotero'); + var suffix = itemID ? itemID : ''; + if (field=='creator') { + suffix = elem.getAttribute('fieldMode') + '-' + suffix; + } + t.setAttribute('autocompletesearchparam', fieldName + '/' + suffix); + t.setAttribute('ontextentered', + 'document.getBindingParent(this).handleCreatorAutoCompleteSelect(this)'); + } + } + var box = elem.parentNode; + box.replaceChild(t, elem); + + // Prevent error when clicking between a changed field + // and another -- there's probably a better way + if (!t.select) { + return; + } + + t.select(); + + t.addEventListener('blur', function () { + document.getBindingParent(this).blurHandler(this); + }, false); + t.setAttribute('onkeypress', "return document.getBindingParent(this).handleKeyPress(event)"); + + this._tabDirection = false; + this._lastTabIndex = tabindex; + + return t; + ]]> + </body> + </method> + + + <!-- + Save a multiple-field selection for the creator autocomplete + (e.g. "Shakespeare, William") + --> + <method name="handleCreatorAutoCompleteSelect"> + <parameter name="textbox"/> + <body> + <![CDATA[ + var comment = Zotero.Utilities.AutoComplete.getResultComment(textbox); + if (!comment) + { + return; + } + + var [creatorID, numFields] = comment.split('-'); + + // If result uses two fields, save both + if (numFields==2) + { + // Manually clear autocomplete controller's reference to + // textbox to prevent error next time around + textbox.mController.input = null; + + var [field, creatorIndex, creatorField] = + textbox.getAttribute('fieldname').split('-'); + + var creator = Zotero.Creators.get(creatorID); + + var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName'; + + // Update this textbox + textbox.setAttribute('value', creator[creatorField]); + textbox.value = creator[creatorField]; + + // Update the other label + if (otherField=='firstName'){ + var label = textbox.nextSibling.nextSibling; + } + else if (otherField=='lastName'){ + var label = textbox.previousSibling.previousSibling; + } + + //this._setFieldValue(label, creator[otherField]); + if (label.firstChild){ + label.firstChild.nodeValue = creator[otherField]; + } + else { + label.value = creator[otherField]; + } + + var row = textbox.parentNode.parentNode.parentNode; + + var fields = this.getCreatorFields(row); + fields[creatorField] = creator[creatorField]; + fields[otherField] = creator[otherField]; + this.modifyCreator(creatorIndex, fields); + } + + // Otherwise let the autocomplete popup handle matters + ]]> + </body> + </method> + + + <method name="handleKeyPress"> + <parameter name="event"/> + <body> + <![CDATA[ + var target = event.target; + var focused = document.commandDispatcher.focusedElement; + + switch (event.keyCode) + { + case event.DOM_VK_RETURN: + var fieldname = target.getAttribute('fieldname'); + // Use shift-enter as the save action for the larger fields + if ((fieldname == 'abstractNote' || fieldname == 'extra') + && !event.shiftKey) + { + break; + } + + + // Prevent blur on containing textbox + // DEBUG: what happens if this isn't present? + event.preventDefault(); + + if (fieldname == 'tag') + { + // If last tag row, create new one + var row = target.parentNode.parentNode; + if (row == row.parentNode.lastChild) + { + this._tabDirection = 1; + var lastTag = true; + } + } + // Shift-enter adds new creator row + else if (fieldname.indexOf('creator-') == 0 && event.shiftKey) { + // Value hasn't changed + if (target.getAttribute('value') == target.value) { + Zotero.debug("Value hasn't changed"); + // If + button is disabled, just focus next creator row + if (Zotero.getAncestorByTagName(target, 'row').lastChild.lastChild.disabled) { + this._focusNextField('info', this._dynamicFields, this._lastTabIndex, false); + } + else { + // TODO: should use current creator type + this.addCreatorRow(false, false, true); + } + } + // Value has changed + else { + this._tabDirection = 1; + this._addCreatorRow = true; + focused.blur(); + } + return false; + } + focused.blur(); + + // Return focus to items pane + if (!lastTag) { + var tree = document.getElementById('zotero-items-tree'); + if (tree) { + tree.focus(); + } + } + + return false; + + case event.DOM_VK_ESCAPE: + // Reset field to original value + target.value = target.getAttribute('value'); + + var tagsbox = Zotero.getAncestorByTagName(focused, 'tagsbox'); + + focused.blur(); + + if (tagsbox) { + tagsbox.closePopup(); + } + + // Return focus to items pane + var tree = document.getElementById('zotero-items-tree'); + if (tree) { + tree.focus(); + } + + return false; + + case event.DOM_VK_TAB: + this._tabDirection = event.shiftKey ? -1 : 1; + // Blur the old manually -- not sure why this is necessary, + // but it prevents an immediate blur() on the next tag + focused.blur(); + return false; + } + + return true; + ]]> + </body> + </method> + + + <method name="hideEditor"> + <parameter name="textbox"/> + <body> + <![CDATA[ + Zotero.debug('Hiding editor'); + /* + var textbox = Zotero.getAncestorByTagName(t, 'textbox'); + if (!textbox){ + Zotero.debug('Textbox not found in hideEditor'); + return; + } + */ + + // TODO: get rid of this? + var saveChanges = this.saveOnEdit; + + // Prevent autocomplete breakage in Firefox 3 + if (textbox.mController) { + textbox.mController.input = null; + } + + var fieldName = textbox.getAttribute('fieldname'); + var tabindex = textbox.getAttribute('ztabindex'); + + //var value = t.value; + var value = textbox.value; + + var elem; + var [field, creatorIndex, creatorField] = fieldName.split('-'); + + // Creator fields + if (field == 'creator') { + var row = textbox.parentNode.parentNode.parentNode; + + var otherFields = this.getCreatorFields(row); + otherFields[creatorField] = value; + + this.modifyCreator(creatorIndex, otherFields); + + var val = this.item.getCreator(creatorIndex); + val = val ? val.ref[creatorField] : null; + + if (!val) { + // Reset to '(first)'/'(last)'/'(name)' + if (creatorField == 'lastName') { + val = otherFields.fieldMode + ? this._defaultFullName : this._defaultLastName; + } + else if (creatorField == 'firstName') { + val = this._defaultFirstName; + } + } + + elem = this.createValueElement( + val, + fieldName, + tabindex + ); + + // Reset creator mode settings + if (otherFields.fieldMode) { + this.switchCreatorMode(row, 1, true); + } + else { + this.switchCreatorMode(row, 0, true); + } + } + + // Tags + else if (fieldName=='tag') { + var tagsbox = Zotero.getAncestorByTagName(textbox, 'tagsbox'); + if (!tagsbox) + { + Zotero.debug('Tagsbox not found', 1); + return; + } + + var row = textbox.parentNode; + var rows = row.parentNode; + + // Tag id encoded as 'tag-1234' + var id = row.getAttribute('id').split('-')[1]; + + if (saveChanges) { + if (id) { + if (value) { + // If trying to replace with another existing tag + // (which causes a delete of the row), + // clear the tab direction so we don't advance + // when the notifier kicks in + var existing = Zotero.Tags.getID(value, 0); + if (existing && id != existing) + { + this._tabDirection = false; + } + var changed = tagsbox.replace(id, value); + if (changed) + { + return; + } + } + else { + tagsbox.remove(id); + return; + } + } + // New tag + else { + // If this is an existing automatic tag, it's going to be + // deleted and the number of rows will stay the same, + // so we have to compensate + var existingTypes = Zotero.Tags.getTypes(value); + if (existingTypes && existingTypes.indexOf(1) != -1) { + this._lastTabIndex--; + } + var id = tagsbox.add(value); + } + } + + if (id) { + elem = this.createValueElement( + value, + 'tag', + tabindex + ); + } + else { + // Just remove the row + // + // If there's an open popup, this throws NODE CANNOT BE FOUND + try { + var row = rows.removeChild(row); + } + catch (e) {} + tagsbox.fixPopup(); + tagsbox.closePopup(); + + this._tabDirection = false; + return; + } + + var focusMode = 'tags'; + var focusBox = tagsbox; + } + + // Fields + else { + // Access date needs to be parsed and converted to UTC + if (fieldName=='accessDate' && value!='') { + if (Zotero.Date.isSQLDate(value) || Zotero.Date.isSQLDateTime(value)) { + var localDate = Zotero.Date.sqlToDate(value); + value = Zotero.Date.dateToSQL(localDate, true); + } + else { + var d = Zotero.Date.strToDate(value); + value = null; + if (d.year && d.month != undefined && d.day) { + d = new Date(d.year, d.month, d.day); + value = Zotero.Date.dateToSQL(d, true); + } + } + } + + this._modifyField(fieldName, value, this.saveOnEdit); + + elem = this.createValueElement( + this.item.getField(fieldName), + fieldName, + tabindex + ); + } + + var box = textbox.parentNode; + box.replaceChild(elem,textbox); + + if (this._tabDirection) { + if (!focusMode) { + var focusMode = 'info'; + var focusBox = this._dynamicFields; + } + this._focusNextField(focusMode, focusBox, this._lastTabIndex, this._tabDirection == -1); + } + ]]> + </body> + </method> + + + <method name="_rowIsClickable"> + <parameter name="fieldName"/> + <body> + <![CDATA[ + return this.clickByRow && + (this.clickable || + this._clickableFields.indexOf(fieldName) != -1); + ]]> + </body> + </method> + + + <method name="_fieldIsClickable"> + <parameter name="fieldName"/> + <body> + <![CDATA[ + return !this.clickByRow && + ((this.clickable && !this.item.isPrimaryField(fieldName)) + || this._clickableFields.indexOf(fieldName) != -1); + ]]> + </body> + </method> + + <method name="_modifyField"> + <parameter name="field"/> + <parameter name="value"/> + <parameter name="save"/> + <body> + <![CDATA[ + this.item.setField(field,value); + if (save) { + this.item.save(); + } + ]]> + </body> + </method> + + + <method name="_getFieldValue"> + <parameter name="label"/> + <body> + <![CDATA[ + return label.firstChild + ? label.firstChild.nodeValue : label.value; + ]]> + </body> + </method> + + + <method name="_setFieldValue"> + <parameter name="label"/> + <parameter name="value"/> + <body> + <![CDATA[ + if (label.firstChild) { + label.firstChild.nodeValue = value; + } + else { + label.value = value; + } + ]]> + </body> + </method> + + + <!-- TODO: work with textboxes too --> + <method name="textTransform"> + <parameter name="label"/> + <parameter name="mode"/> + <body> + <![CDATA[ + var val = this._getFieldValue(label); + switch (mode) { + case 'lower': + var newVal = val.toLowerCase(); + break; + case 'title': + var utils = new Zotero.Utilities(); + var newVal = utils.capitalizeTitle(val.toLowerCase(), true); + break; + default: + throw ("Invalid transform mode '" + mode + "' in zoteroitembox.textTransform()"); + } + this._setFieldValue(label, newVal); + this._modifyField(label.getAttribute('fieldname'), newVal, this.saveOnEdit); + + ]]> + </body> + </method> + + + <method name="getCreatorFields"> + <parameter name="row"/> + <body> + <![CDATA[ + var typeID = row.getElementsByTagName('toolbarbutton')[0].getAttribute('typeid'); + var label1 = row.getElementsByTagName('hbox')[0].firstChild.firstChild; + var label2 = label1.parentNode.lastChild; + + var fields = { + lastName: label1.firstChild ? label1.firstChild.nodeValue + : label1.value, + firstName: label2.firstChild ? label2.firstChild.nodeValue + : label2.value, + fieldMode: label1.getAttribute('fieldMode') + ? parseInt(label1.getAttribute('fieldMode')) : 0, + creatorTypeID: parseInt(typeID), + }; + + // Ignore '(first)' + if (fields.fieldMode == 1 || fields.firstName == this._defaultFirstName) { + fields.firstName = ''; + } + // Ignore '(last)' or '(name)' + if (fields.lastName == this._defaultFullName + || fields.lastName == this._defaultLastName) { + fields.lastName = ''; + } + + return fields; + ]]> + </body> + </method> + + + <method name="modifyCreator"> + <parameter name="index"/> + <parameter name="fields"/> + <parameter name="changeGlobally"/> + <body> + <![CDATA[ + var firstName = fields.firstName; + var lastName = fields.lastName; + //var shortName = fields.shortName; + var fieldMode = fields.fieldMode; + var creatorTypeID = fields.creatorTypeID; + + var oldCreator = this.item.getCreator(index); + + // Don't save empty creators + if (!firstName && !lastName){ + if (!oldCreator) { + return; + } + this.item.removeCreator(index); + this.item.save(); + return; + } + + Zotero.DB.beginTransaction(); + + var newCreator = new Zotero.Creator; + newCreator.setFields(fields); + + var newLinkedCreators = []; + var creatorDataID = Zotero.Creators.getDataID(fields); + if (creatorDataID) { + newLinkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID); + } + + if (oldCreator) { + if (oldCreator.ref.equals(newCreator)) { + if (oldCreator.creatorTypeID == creatorTypeID) { + Zotero.debug("Creator " + oldCreator.ref.id + " hasn't changed"); + } + // Just change creatorTypeID + else { + this.item.setCreator(index, oldCreator.ref, creatorTypeID); + if (this.saveOnEdit) { + this.item.save(); + } + } + Zotero.DB.commitTransaction(); + return; + } + + oldCreator = oldCreator.ref; + } + + var creator; + var creatorID; + + if (oldCreator) { + var numLinkedItems = oldCreator.countLinkedItems(); + // Creator is linked only to the current item + if (numLinkedItems == 1) { + if (newLinkedCreators.length) { + // Use the first creator found with this data + // TODO: support choosing among options + creatorID = newLinkedCreators[0]; + creator = Zotero.Creators.get(creatorID); + } + else { + oldCreator.setFields(fields); + //creatorID = oldCreator.save(); + creator = oldCreator; + } + } + // Creator is linked to multiple items with changeGlobally off + else if (!changeGlobally) { + if (newLinkedCreators.length) { + // Use the first creator found with this data + // TODO: support choosing among options + creatorID = newLinkedCreators[0]; + creator = Zotero.Creators.get(creatorID); + } + else { + //creatorID = newCreator.save(); + creator = newCreator; + } + } + // Creator is linked to multiple items with changeGlobally on + else { + throw ('changeGlobally unimplemented'); + if (newLinkedCreators.length) { + // Use the first creator found with this data + // TODO: support choosing among options + creatorID = newLinkedCreators[0]; + + // TODO: switch all linked items to this creator + } + else { + creatorID = newCreator.save(); + + // TODO: switch all linked items to new creatorID + } + } + } + // No existing creator + else { + if (newLinkedCreators.length) { + creatorID = newLinkedCreators[0]; + creator = Zotero.Creators.get(creatorID); + } + else { + //creatorID = newCreator.save(); + creator = newCreator; + } + } + + this.item.setCreator(index, creator, creatorTypeID); + if (this.saveOnEdit) { + this.item.save(); + } + + Zotero.DB.commitTransaction(); + ]]> + </body> + </method> + + <!-- + /* + function modifyCreatorByID(index, creatorID, creatorTypeID) { + throw ('Unimplemented'); + var oldCreator = _itemBeingEdited.getCreator(index); + if (creator) { + oldCreator = creator.ref; + var oldCreatorID = oldCreator.creatorID; + } + + Zotero.debug("Old creatorID is " + oldCreatorID); + + _itemBeingEdited.setCreator(index, firstName, lastName, typeID, fieldMode); + _itemBeingEdited.save(); + } + */ + --> + + + <method name="focusFirstField"> + <parameter name="mode"/> + <body> + <![CDATA[ + switch (mode) { + case 'info': + this._focusNextField('info', this._dynamicFields, 0, false); + break; + } + ]]> + </body> + </method> + + + <!-- + Advance the field focus forward or backward + + Note: We're basically replicating the built-in tabindex functionality, + which doesn't work well with the weird label/textbox stuff we're doing. + (The textbox being tabbed away from is deleted before the blur() + completes, so it doesn't know where it's supposed to go next.) + --> + <method name="_focusNextField"> + <parameter name="mode"/> + <parameter name="box"/> + <parameter name="tabindex"/> + <parameter name="back"/> + <body> + <![CDATA[ + tabindex = parseInt(tabindex); + if (back) + { + if (mode=='info') + { + switch (tabindex) + { + case 1: + //Zotero.debug('At beginning'); + document.getElementById('item-type-menu').focus(); + return false; + + case this._tabIndexMinCreators: + var nextIndex = 1; + break; + + case this._tabIndexMinFields: + var nextIndex = this._tabIndexMaxCreators; + break; + + default: + var nextIndex = tabindex - 1; + } + } + else if (mode=='tags') + { + switch (tabindex) + { + case 1: + return false; + + default: + var nextIndex = tabindex - 1; + } + } + } + else + { + if (mode=='info') + { + switch (tabindex) + { + case 1: + var nextIndex = this._tabIndexMinCreators; + break; + + case this._tabIndexMaxCreators: + var nextIndex = this._tabIndexMinFields; + break; + + case this._tabIndexMaxInfoFields: + //Zotero.debug('At end'); + return false; + + default: + var nextIndex = tabindex + 1; + } + } + else if (mode=='tags') + { + switch (tabindex) + { + case this._tabIndexMaxTagsFields: + // In tags box, keep going to create new row + var nextIndex = tabindex + 1; + break; + + default: + var nextIndex = tabindex + 1; + } + } + } + + Zotero.debug('Looking for tabindex ' + nextIndex, 4); + switch (mode) + { + case 'info': + var next = box.getElementsByAttribute('ztabindex', nextIndex); + if (!next[0]) + { + //Zotero.debug("Next field not found"); + return this._focusNextField(mode, box, nextIndex, back); + } + break; + + // Tags pane + case 'tags': + var next = document.getAnonymousNodes(box)[0]. + getElementsByAttribute('ztabindex', nextIndex); + if (!next[0]) { + next[0] = box.addDynamicRow(); + } + break; + } + + next[0].click(); + this.ensureElementIsVisible(next[0]); + return true; + ]]> + </body> + </method> + + <method name="_id"> + <parameter name="id"/> + <body> + <![CDATA[ + return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0]; + ]]> + </body> + </method> + </implementation> + + <content> + <scrollbox id="item-box" flex="1" orient="vertical" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <popupset> + <popup id="creator-type-menu" position="after_start" + oncommand="var fields = document.getBindingParent(this).getCreatorFields(document.popupNode.parentNode); + var typeID = event.explicitOriginalTarget.getAttribute('typeid'); + fields['creatorTypeID'] = typeID; + document.popupNode.setAttribute('label', + Zotero.getString('creatorTypes.' + Zotero.CreatorTypes.getName(typeID)) + ':'); + document.popupNode.setAttribute('typeid', typeID); + var index = document.popupNode.getAttribute('fieldname').split('-')[1]; + document.getBindingParent(this).modifyCreator(index, fields)"/> + <popup id="field-menu"> + <menu label="&zotero.item.textTransform;"> + <menupopup> + <menuitem label="&zotero.item.textTransform.lowercase;" class="menuitem-non-iconic" + oncommand="document.getBindingParent(this).textTransform(document.popupNode, 'lower')"/> + <menuitem label="&zotero.item.textTransform.titlecase;" class="menuitem-non-iconic" + oncommand="document.getBindingParent(this).textTransform(document.popupNode, 'title')"/> + </menupopup> + </menu> + </popup> + </popupset> + <hbox id="go-buttons" align="center" hidden="true"> + <button id="view-button" + onfocus="document.getBindingParent(this).ensureElementIsVisible(this)" + oncommand="document.getBindingParent(this).onViewClick(this, event)" disabled="false"/> + <button id="openurl-button" label="&zotero.toolbar.openURL.label;" + tooltiptext="&zotero.toolbar.openURL.tooltip;" + onfocus="document.getBindingParent(this).ensureElementIsVisible(this)" + oncommand="document.getBindingParent(this).onOpenURLClick(event);"/> + </hbox> + <hbox align="center" hidden="true"> + <menulist id="item-type-menu" oncommand="document.getBindingParent(this).changeTypeTo(this.value, this)" flex="1" + onfocus="document.getBindingParent(this).ensureElementIsVisible(this)" + onkeypress="if (event.keyCode == event.DOM_VK_TAB){ if (!event.shiftKey) { document.getBindingParent(this).focusFirstField('info'); event.preventDefault(); } }"> + <menupopup/> + </menulist> + </hbox> + <grid flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows id="dynamic-fields" flex="1"/> + </grid> + </scrollbox> + </content> + </binding> +</bindings> diff --git a/chrome/content/zotero/bindings/merge.xml b/chrome/content/zotero/bindings/merge.xml @@ -0,0 +1,443 @@ +<?xml version="1.0"?> +<!-- + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +--> + +<!DOCTYPE bindings SYSTEM "chrome://zotero/locale/merge.dtd"> + +<bindings xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="merge-group"> + <resources> + <stylesheet src="chrome://zotero/skin/merge.css"/> + </resources> + + <implementation> + <constructor> + <![CDATA[ + this._leftpane = this._id('leftpane'); + this._rightpane = this._id('rightpane'); + this._mergepane = this._id('mergepane'); + ]]> + </constructor> + + <field name="_type"/> + <property name="type" onget="return this._type;"> + <setter> + <![CDATA[ + this._type = val; + var hbox = document.getAnonymousNodes(this)[0]; + hbox.setAttribute('mergetype', val); + ]]> + </setter> + </property> + + <property name="left" onget="return this._leftpane.ref;"> + <setter> + <![CDATA[ + // TODO: Make sure object is the correct type + + if (val == 'deleted') { + this._leftpane.ref = 'deleted'; + return; + } + + // Clone object so changes in merge pane don't affect it + this._leftpane.ref = val.clone(true); + this._leftpane.original = val; + ]]> + </setter> + </property> + + <property name="right" onget="return this._rightpane.ref;"> + <setter> + <![CDATA[ + // TODO: make sure left is set + if (!this._leftpane.ref) { + throw ("Left object must be set before setting <zoteromergegroup>.right"); + } + + if (val == 'deleted') { + this._rightpane.ref = 'deleted'; + } + else { + // TODO: Make sure object is the correct type + + // Clone object so changes in merge pane don't affect it + this._rightpane.ref = val.clone(true); + this._rightpane.original = val; + } + + this.refresh(); + ]]> + </setter> + </property> + + <property name="merge" onget="return this._mergepane.ref"> + <setter> + <![CDATA[ + // TODO: Make sure object is the correct type + this._mergepane.ref = val; + ]]> + </setter> + </property> + + <property name="leftCaption" onset="this._leftpane.caption.label = val"/> + <property name="rightCaption" onset="this._rightpane.caption.label = val"/> + <property name="mergeCaption" onset="this._mergepane.caption.label = val"/> + + + <property name="leftpane" onget="return this._leftpane"/> + <property name="rightpane" onget="return this._rightpane"/> + <property name="mergepane" onget="return this._mergepane"/> + + <field name="_leftpane"/> + <field name="_rightpane"/> + <field name="_mergepane"/> + + <method name="refresh"> + <body> + <![CDATA[ + // Set merge pane to most recently changed object + // If one object was deleted, set merge pane to other + // TODO: use delete timestamp + + if (this._leftpane.ref != 'deleted' + && this._rightpane.ref != 'deleted') { + var dm1 = this._leftpane.ref.getField('dateModified'); + if (dm1) { + dm1 = Zotero.Date.sqlToDate(dm1); + } + + var dm2 = this._rightpane.ref.getField('dateModified'); + if (dm2) { + dm2 = Zotero.Date.sqlToDate(dm2); + } + } + + if (this._leftpane.ref == 'deleted' || dm2 > dm1) { + var mergeItem = this._rightpane.original; + this._leftpane.removeAttribute("selected"); + this._rightpane.setAttribute("selected", "true"); + } + else { + var mergeItem = this._leftpane.original; + this._rightpane.removeAttribute("selected"); + this._leftpane.setAttribute("selected", "true"); + } + + this._mergepane.ref = mergeItem; + + /* + + Code to display only the different values -- not used + + var diff = this._leftpane.ref.diff(this._rightpane.ref, true); + + var fields = []; + var diffFields = []; + for (var field in diff[0].primary) { + fields.push(field); + if (diff[0].primary[field] != diff[1].primary[field]) { + diffFields.push(field); + } + } + for (var field in diff[0].fields) { + fields.push(field); + if (diff[0].fields[field] != diff[1].fields[field]) { + diffFields.push(field); + } + } + + this._leftpane.objectbox.fieldOrder = fields; + this._rightpane.objectbox.fieldOrder = fields; + + // Display merge pane if item types match + if (this._leftpane.ref.itemTypeID == this._rightpane.ref.itemTypeID) { + this._leftpane.objectbox.visibleFields = fields; + this._rightpane.objectbox.visibleFields = fields; + + this._leftpane.objectbox.clickable = false; + this._rightpane.objectbox.clickable = false; + this._leftpane.objectbox.clickableFields = diffFields; + this._rightpane.objectbox.clickableFields = diffFields; + + var mergeItem = new Zotero.Item(false, this._leftpane.ref.itemTypeID); + this._mergepane.ref = mergeItem; + this._mergepane.objectbox.visibleFields = fields; + } + // Otherwise only allow clicking on item types + else { + this._leftpane.objectbox.clickableFields = ['itemType']; + this._rightpane.objectbox.clickableFields = ['itemType']; + } + */ + + + this._mergepane.objectbox.editable = true; + + + /* + + No need to refresh if not comparing fields + + this._leftpane.objectbox.refresh(); + this._rightpane.objectbox.refresh(); + */ + ]]> + </body> + </method> + + + <method name="_id"> + <parameter name="id"/> + <body> + <![CDATA[ + return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0]; + ]]> + </body> + </method> + </implementation> + + <content> + <xul:hbox id="merge-group" flex="1"> + <xul:zoteromergepane id="leftpane" flex="1"/> + <xul:zoteromergepane id="rightpane" flex="1"/> + <xul:zoteromergepane id="mergepane" flex="1"/> + </xul:hbox> + </content> + </binding> + + + <binding id="merge-pane"> + <resources> + <stylesheet src="chrome://zotero/skin/bindings/merge.css"/> + </resources> + + <implementation> + <constructor> + <![CDATA[ + this.parent = document.getBindingParent(this.parentNode); + ]]> + </constructor> + + <property name="type" onget="return this.parent.type" readonly="true"/> + <property name="caption" onget="return this._id('caption')" readonly="true"/> + <property name="objectbox" onget="return this._id('objectbox')" readonly="true"/> + + <field name="_deleted"/> + <property name="deleted"> + <setter> + <![CDATA[ + this._deleted = !!val; + + var placeholder = this._id('object-placeholder'); + if (placeholder) { + placeholder.hidden = !!val; + } + else { + this._id('objectbox').hidden = !!true; + } + var deleteBox = this._id('delete-box'); + deleteBox.hidden = !val; + ]]> + </setter> + </property> + + <property name="ref" onget="return this._deleted ? 'deleted' : this.objectbox.ref;"> + <setter> + <![CDATA[ + if (val == 'deleted') { + this.deleted = true; + return; + } + + this.deleted = false; + + // Replace XUL placeholder with XUL object box of given type + var elementName; + switch (this.type) { + case 'item': + elementName = 'zoteroitembox'; + break; + + case 'note': + elementName = 'zoteronoteeditor'; + break; + + default: + throw ("Object type '" + this.type + + "' not supported in <zoteromergepane>.ref"); + } + + var objbox = document.createElement(elementName); + + if (this._id('object-placeholder')) { + var placeholder = this._id('object-placeholder'); + placeholder.parentNode.replaceChild(objbox, placeholder); + } + else { + var oldObjBox = this._id('objectbox'); + oldObjBox.parentNode.replaceChild(objbox, oldObjBox); + } + + objbox.setAttribute("id", "objectbox"); + objbox.setAttribute("flex", "1"); + + if (this.getAttribute('id') == 'mergepane') { + objbox.mode = 'mergeedit'; + } + else { + objbox.mode = 'merge'; + objbox.clickHandler = this.chooseObj; + } + + // Type-specific settings + switch (this.type) { + case 'note': + objbox.buttonCaption = 'Choose this version'; + break; + } + + objbox.ref = val; + ]]> + </setter> + </property> + + <field name="original"/> <!-- original object --> + <field name="parent"/> + + <method name="chooseObj"> + <parameter name="obj"/> + <body> + <![CDATA[ + var pane = Zotero.getAncestorByTagName(obj, 'zoteromergepane'); + var mergegroup = document.getBindingParent(pane); + var mergepane = mergegroup.mergepane; + + if (pane.getAttribute('id') == 'leftpane') { + var position = 'left'; + var otherPane = mergegroup.rightpane; + } + else { + var position = 'right'; + var otherPane = mergegroup.leftpane; + } + + pane.removeAttribute("selected"); + otherPane.removeAttribute("selected"); + pane.setAttribute("selected", "true"); + + if (pane.ref == 'deleted') { + mergepane.deleted = true; + } + else { + mergepane.ref = pane.original; + } + ]]> + </body> + </method> + + <!-- Unused --> + <method name="chooseField"> + <parameter name="row"/> + <body> + <![CDATA[ + // If used, has to be updated to handle original item + + var fieldName = row.firstChild.getAttribute('fieldname'); + // TODO: creator/date + var value = row.lastChild.firstChild.nodeValue + + var mergegroup = document.getBindingParent(this.parentNode).parent; + var mergepane = mergegroup.mergepane; + var pane = document.getBindingParent(this.parentNode); + + if (pane.getAttribute('id') == 'leftpane') { + var position = 'left'; + var otherPane = mergegroup.rightpane; + } + else { + var position = 'right'; + var otherPane = mergegroup.leftpane; + } + + // Changing item type + if (fieldName == 'itemType') { + fieldName = 'itemTypeID'; + value = row.lastChild.getAttribute('itemTypeID'); + + if (!mergepane.ref) { + mergepane.ref = new Zotero.Item(false, value); + } + else { + mergepane.objectbox.changeTypeTo(value, true); + } + + pane.objectbox.clickableFields = []; + pane.objectbox.clickable = true; + var fieldIDs = Zotero.ItemFields.getItemTypeFields(value); + var fieldNames = ['itemType']; + for each(var field in fieldIDs) { + fieldNames.push(Zotero.ItemFields.getName(field)); + } + otherPane.objectbox.clickableFields = fieldNames; + otherPane.objectbox.clickable = false; + pane.objectbox.refresh(); + otherPane.objectbox.refresh(); + } + // Changing another field + else { + mergepane.ref.setField(fieldName, value); + } + + mergepane.objectbox.refresh(); + ]]> + </body> + </method> + + <method name="_id"> + <parameter name="id"/> + <body> + <![CDATA[ + if (!document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id).length) { + return false; + } + return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0]; + ]]> + </body> + </method> + </implementation> + + <content> + <xul:groupbox id="merge-pane" flex="1"> + <xul:caption id="caption"/> + <xul:box id="object-placeholder"/> + <xul:hbox id="delete-box" hidden="true" flex="1" + onclick="document.getBindingParent(this).chooseObj(this)"> + <xul:label value="Deleted"/> <!-- TODO: localize --> + </xul:hbox> + </xul:groupbox> + </content> + </binding> +</bindings> diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml @@ -20,6 +20,7 @@ ***** END LICENSE BLOCK ***** --> + <bindings xmlns="http://www.mozilla.org/xbl" xmlns:xbl="http://www.mozilla.org/xbl" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> @@ -30,69 +31,209 @@ </resources> <implementation> - <field name="itemRef">null</field> - <property name="item" onget="return this.itemRef;"> + <!-- + Public properties + --> + <field name="editable">false</field> + <field name="saveOnEdit">false</field> + <field name="displayTags">false</field> + <field name="displayRelated">false</field> + <field name="displayButton">false</field> + + <field name="parentClickHandler"/> + <field name="keyDownHandler"/> + <field name="commandHandler"/> + <field name="clickHandler"/> + + <field name="buttonCaption"/> + + <!-- Modes are predefined settings groups for particular tasks --> + <field name="_mode">"view"</field> + <property name="mode" onget="return this._mode;"> + <setter> + <![CDATA[ + // Duplicate default property settings here + this.editable = false; + this.saveOnEdit = false; + this.displayTags = false; + this.displayRelated = false; + this.displayButton = false; + + switch (val) { + case 'view': + break; + + case 'edit': + this.editable = true; + this.saveOnEdit = true; + this.parentClickHandler = this.selectParent; + this.keyDownHandler = this.handleKeyDown; + this.commandHandler = this.save; + this.displayTags = true; + this.displayRelated = true; + break; + + case 'merge': + this.displayButton = true; + break; + + case 'mergeedit': + this.editable = true; + this.keyDownHandler = this.handleKeyDown; + break; + + default: + throw ("Invalid mode '" + val + "' in noteeditor.xml"); + } + + this._mode = val; + document.getAnonymousNodes(this)[0].setAttribute('mode', val); + ]]> + </setter> + </property> + + <field name="_parent"/> + <property name="parent" onget="return this._parent;"> <setter> <![CDATA[ - this.itemRef = val; + this._parent = val; - var citeLabel = this.id('citeLabel'); - if(citeLabel.firstChild) + var citeLabel = this._id('citeLabel'); + if (citeLabel.firstChild) { citeLabel.removeChild(citeLabel.firstChild); + } - if(this.item && !this.getAttribute('notitle')=='1') - { - citeLabel.appendChild(document.createTextNode(this.item.getDisplayTitle(true))); + if (this.parent && !this.getAttribute('notitle') == '1') { + citeLabel.appendChild(document.createTextNode(this.parent.getDisplayTitle(true))); } ]]> </setter> </property> - <field name="noteRef">null</field> - <property name="note" onget="return this.noteRef;"> + + <field name="_item"/> + <property name="item" onget="return this._item;"> <setter> <![CDATA[ - var scrollPos = this.id('noteField').inputField.scrollTop; - - this.noteRef = val; + this._item = val; - if(this.note.getSource()) - this.item = Zotero.Items.get(this.note.getSource()); + var parent = this.item.getSource(); + if (parent) { + this.parent = Zotero.Items.get(parent); + } - this.id('noteField').value = this.note.getNote(); - this.id('links').item = this.note; + this._id('links').item = this.item; - this.id('noteField').inputField.scrollTop = scrollPos; + this.refresh(); ]]> </setter> </property> - <field name="collectionRef">null</field> - <property name="collection" onget="return this.collectionRef;" onset="this.collectionRef = val;"/> - <property name="value" onget="return this.id('noteField').value;" onset="this.id('noteField').value = val;"/> + + <property name="note" + onget="Zotero.debug('Getting note with .note deprecated -- use .item in zoteronoteeditor'); return this._item" + onset="Zotero.debug('Setting note with .note deprecated -- use .item in zoteronoteeditor'); this.item = val"/> + <property name="ref" onget="return this._item" onset="this.item = val"/> + + <field name="collection"/> + + <property name="noteField" onget="return this._id('noteField')" readonly="true"/> + <property name="value" onget="return this._id('noteField').value;" onset="this._id('noteField').value = val;"/> + + <method name="refresh"> + <body> + <![CDATA[ + Zotero.debug('Refreshing note editor'); + + var parentbox = this._id('citeLabel'); + var textbox = this._id('noteField'); + var button = this._id('goButton'); + var button = this._id('goButton'); + + if (this.editable) { + textbox.removeAttribute('readonly'); + } + else { + textbox.setAttribute('readonly', 'true'); + } + + var scrollPos = textbox.inputField.scrollTop; + if (this.item) { + textbox.value = this.item.getNote(); + } + textbox.inputField.scrollTop = scrollPos; + + this._id('linksbox').hidden = !(this.displayTags && this.displayRelated); + + if (this.parentClickHandler) { + parentbox.setAttribute('onclick', + 'document.getBindingParent(this).clickHandler(this)'); + } + else { + parentbox.removeAttribute('onclick'); + } + + if (this.keyDownHandler) { + textbox.setAttribute('onkeydown', + 'document.getBindingParent(this).handleKeyDown(event)'); + } + else { + textbox.removeAttribute('onkeydown'); + } + + if (this.commandHandler) { + textbox.setAttribute('oncommand', + 'document.getBindingParent(this).commandHandler()'); + } + else { + textbox.removeAttribute('oncommand'); + } + + if (this.displayButton) { + button.label = this.buttonCaption; + button.hidden = false; + button.setAttribute('oncommand', + 'document.getBindingParent(this).clickHandler(this)'); + } + else { + button.hidden = true; + } + ]]> + </body> + </method> + <method name="save"> <body> <![CDATA[ - var noteField = this.id('noteField'); - if(this.note) //Update note - { - this.note.updateNote(noteField.value); + var noteField = this._id('noteField'); + + // Update note + if (this.item) { + this.item.setNote(noteField.value); + if (this.saveOnEdit) { + this.item.save(); + } + return; } - else //Create new note - { - if(this.item) - var noteID = Zotero.Notes.add(noteField.value,this.item.getID()); //attached to an item - else - { - //independent note - var noteID = Zotero.Notes.add(noteField.value); - if(this.collection) - this.collection.addItem(noteID); + + // Create new note + var item = new Zotero.Item(false, 'note'); + item.setNote(noteField.value); + if (this.parent) { + item.setSource(this.parent.id); + } + if (this.saveOnEdit) { + var id = item.save(); + + if (this.parent && this.collection) { + this.collection.addItem(id); } - this.note = Zotero.Items.get(noteID); } + + this.item = Zotero.Items.get(id); ]]> </body> </method> + <!-- Used to insert a tab manually --> <method name="handleKeyDown"> <parameter name="event"/> <body> @@ -110,7 +251,7 @@ if (controller && controller.isCommandEnabled(command)) { controller = controller.QueryInterface(Components.interfaces.nsICommandController); var params = Components.classes["@mozilla.org/embedcomp/command-params;1"] - .createInstance(Components.interfaces.nsICommandParams); + .createInstance(Components.interfaces.nsICommandParams); params.setStringValue("state_data", "\t"); controller.doCommandWithParams(command, params); } @@ -133,7 +274,7 @@ <method name="focus"> <body> <![CDATA[ - this.id('noteField').focus(); + this._id('noteField').focus(); ]]> </body> </method> @@ -141,7 +282,7 @@ <method name="selectParent"> <body> <![CDATA[ - if (!this.item.getID()) { + if (!this.item.id) { return; } @@ -173,12 +314,37 @@ } zp.clearQuicksearch(); - zp.selectItem(this.item.getID()); + zp.selectItem(this.item.id); ]]> </body> </method> - <method name="id"> + <method name="disableUndo"> + <body> + <![CDATA[ + this.noteField.editor.enableUndo(true); + ]]> + </body> + </method> + + <method name="enableUndo"> + <body> + <![CDATA[ + this.noteField.editor.enableUndo(false); + ]]> + </body> + </method> + + <method name="clearUndo"> + <body> + <![CDATA[ + this.disableUndo(); + this.enableUndo(); + ]]> + </body> + </method> + + <method name="_id"> <parameter name="id"/> <body> <![CDATA[ @@ -190,15 +356,17 @@ <content> <xul:vbox xbl:inherits="flex"> - <xul:label id="citeLabel" onclick="document.getBindingParent(this).selectParent()"/> - <xul:textbox id="noteField" multiline="true" type="timed" timeout="1000" flex="1" onkeydown="document.getBindingParent(this).handleKeyDown(event)" oncommand="document.getBindingParent(this).save();"/> - <xul:hbox> + <xul:label id="citeLabel"/> + <xul:textbox id="noteField" multiline="true" type="timed" timeout="1000" flex="1"/> + <xul:hbox id="linksbox" hidden="true"> <xul:linksbox id="links" flex="1"/> </xul:hbox> + <xul:button id="goButton" hidden="true"/> </xul:vbox> </content> </binding> + <binding id="links-box"> <implementation> <field name="itemRef"/> @@ -271,8 +439,8 @@ </implementation> <content> <xul:vbox xbl:inherits="flex"> - <xul:label id="seeAlsoLabel" class="zotero-clicky" crop="end" onclick="this.parentNode.parentNode.seeAlsoClick();"/> - <xul:label id="tagsLabel" class="zotero-clicky" crop="end" onclick="this.parentNode.parentNode.tagsClick();"/> + <xul:label id="seeAlsoLabel" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).seeAlsoClick();"/> + <xul:label id="tagsLabel" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).tagsClick();"/> <xul:popupset> <xul:popup id="seeAlsoPopup" width="300" onpopupshowing="this.firstChild.reload();"> <xul:seealsobox id="seeAlso" flex="1"/> diff --git a/chrome/content/zotero/browser.js b/chrome/content/zotero/browser.js @@ -268,11 +268,25 @@ var Zotero_Browser = new function() { this.tabbrowser.addEventListener("resize", function(e) { Zotero_Browser.resize(e) }, false); // Resize on text zoom changes - document.getElementById('cmd_textZoomReduce').addEventListener("command", + + // Fx2 + var reduce = document.getElementById('cmd_textZoomReduce'); + if (reduce) { + var enlarge = document.getElementById('cmd_textZoomEnlarge'); + var reset = document.getElementById('cmd_textZoomReset'); + } + // Fx3 + else { + var reduce = document.getElementById('cmd_fullZoomReduce'); + var enlarge = document.getElementById('cmd_fullZoomEnlarge'); + var reset = document.getElementById('cmd_fullZoomReset'); + } + + reduce.addEventListener("command", function(e) { Zotero_Browser.resize(e) }, false); - document.getElementById('cmd_textZoomEnlarge').addEventListener("command", + enlarge.addEventListener("command", function(e) { Zotero_Browser.resize(e) }, false); - document.getElementById('cmd_textZoomReset').addEventListener("command", + reset.addEventListener("command", function(e) { Zotero_Browser.resize(e) }, false); } diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js @@ -244,7 +244,8 @@ var Zotero_File_Interface = new function() { * collections */ function _importCollectionDone(obj, collection) { - collection.changeParent(_importCollection.getID()); + collection.parent = _importCollection.id; + collection.save(); } /* diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js @@ -20,67 +20,29 @@ ***** END LICENSE BLOCK ***** */ -var ZoteroItemPane = new function() -{ - var _dynamicFields; - var _creatorTypeMenu; - var _beforeRow; +var ZoteroItemPane = new function() { + var _itemBeingEdited; + var _notesList; var _linksBox; var _notesLabel; - var _creatorCount; - var _lastPane; var _loaded; - var _itemBeingEdited; - var _activeScrollbox; - - var _addCreatorRow; - + var _tabs; var _lastTabIndex; var _tabDirection; - var _tabIndexMinCreators = 10; - var _tabIndexMaxCreators = 0; - var _tabIndexMinFields = 1000; - var _tabIndexMaxInfoFields = 0; var _tabIndexMaxTagsFields = 0; - const _defaultFirstName = - '(' + Zotero.getString('pane.item.defaultFirstName') + ')'; - const _defaultLastName = - '(' + Zotero.getString('pane.item.defaultLastName') + ')'; - const _defaultFullName = - '(' + Zotero.getString('pane.item.defaultFullName') + ')'; - this.onLoad = onLoad; this.viewItem = viewItem; - this.scrollToTop = scrollToTop; - this.ensureElementIsVisible = ensureElementIsVisible; this.loadPane = loadPane; - this.changeTypeTo = changeTypeTo; - this.onViewClick = onViewClick; - this.onOpenURLClick = onOpenURLClick; - this.addCreatorRow = addCreatorRow; - this.switchCreatorMode = switchCreatorMode; - this.toggleAbstractExpand = toggleAbstractExpand; - this.disableButton = disableButton; - this.createValueElement = createValueElement; - this.removeCreator = removeCreator; - this.showEditor = showEditor; - this.handleKeyPress = handleKeyPress; - this.handleCreatorAutoCompleteSelect = handleCreatorAutoCompleteSelect; - this.hideEditor = hideEditor; - this.textTransform = textTransform; - this.getCreatorFields = getCreatorFields; - this.modifyCreator = modifyCreator; this.removeNote = removeNote; this.addNote = addNote; this.removeAttachment = removeAttachment; this.addAttachmentFromDialog = addAttachmentFromDialog; this.addAttachmentFromPage = addAttachmentFromPage; - this.focusFirstField = focusFirstField; function onLoad() @@ -92,46 +54,36 @@ var ZoteroItemPane = new function() _tabs = document.getElementById('zotero-view-tabs'); // Not in item pane, so skip the introductions - if (!_tabs) - { + if (!_tabs) { return; } - _dynamicFields = document.getElementById('zotero-editpane-dynamic-fields'); - _itemTypeMenu = document.getElementById('zotero-editpane-type-menu'); - _creatorTypeMenu = document.getElementById('zotero-creator-type-menu'); + _itemBox = document.getElementById('zotero-editpane-item-box'); _notesList = document.getElementById('zotero-editpane-dynamic-notes'); _notesLabel = document.getElementById('zotero-editpane-notes-label'); _attachmentsList = document.getElementById('zotero-editpane-dynamic-attachments'); _attachmentsLabel = document.getElementById('zotero-editpane-attachments-label'); _tagsBox = document.getElementById('zotero-editpane-tags'); _relatedBox = document.getElementById('zotero-editpane-related'); - - var itemTypes = Zotero.ItemTypes.getTypes(); - for(var i = 0; i<itemTypes.length; i++) - if(itemTypes[i]['name'] != 'note' && itemTypes[i]['name'] != 'attachment') - _itemTypeMenu.appendItem(Zotero.getString("itemTypes."+itemTypes[i]['name']),itemTypes[i]['id']); } /* * Loads an item */ - function viewItem(thisItem) - { + function viewItem(thisItem) { //Zotero.debug('Viewing item'); // Force blur() when clicking off a textbox to another item in middle // pane, since for some reason it's not being called automatically - if (_itemBeingEdited && _itemBeingEdited!=thisItem) - { - switch (_tabs.selectedIndex) - { + if (_itemBeingEdited && _itemBeingEdited != thisItem) { + switch (_tabs.selectedIndex) { // Info case 0: - var boxes = _dynamicFields.getElementsByTagName('textbox'); + // TODO: fix + //var boxes = _itemBox.getElementsByTagName('textbox'); // When coming from another element, scroll pane to top - scrollToTop(); + //scrollToTop(); break; // Tags @@ -140,9 +92,8 @@ var ZoteroItemPane = new function() break; } - if (boxes && boxes.length==1) - { - boxes[0].inputField.blur(); + if (boxes && boxes.length == 1) { + //boxes[0].inputField.blur(); } } @@ -153,213 +104,25 @@ var ZoteroItemPane = new function() } - function loadPane(index) - { + function loadPane(index) { //Zotero.debug('Loading item pane ' + index); // Clear the tab index when switching panes - if (_lastPane!=index) - { + if (_lastPane!=index) { _lastTabIndex = null; } _lastPane = index; - if(_loaded[index]) - { + if (_loaded[index]) { return; } _loaded[index] = true; // Info pane - if(index == 0) - { - _activeScrollbox = document.getElementById('zotero-info'); - - // Enable/disable "View =>" button - testView: try - { - var viewButton = document.getElementById('zotero-go-to-url'); - - viewButton.removeAttribute('viewSnapshot'); - viewButton.removeAttribute('viewURL'); - viewButton.setAttribute('label', - Zotero.getString('pane.item.goToURL.online.label')); - viewButton.setAttribute('tooltiptext', - Zotero.getString('pane.item.goToURL.online.tooltip')); - - var spec = false, validURI = false; - - var uri = Components.classes["@mozilla.org/network/standard-url;1"]. - createInstance(Components.interfaces.nsIURI); - - // First try to find a snapshot matching the item's URL field - var snapID = _itemBeingEdited.getBestSnapshot(); - if (snapID) { - spec = Zotero.Items.get(snapID).getLocalFileURL(); - uri.spec = spec; - if (!uri.scheme || uri.scheme != 'file') { - snapID = false; - spec = false; - } - } - - // If that fails, try the URL field itself - if (!spec) { - spec = _itemBeingEdited.getField('url'); - uri.spec = spec; - if (!(uri.scheme && (uri.host || uri.scheme == 'file'))) { - spec = false; - } - } - - if (!spec) { - break testView; - } - - validURI = true; - - if (snapID) { - viewButton.setAttribute('label', - Zotero.getString('pane.item.goToURL.snapshot.label')); - viewButton.setAttribute('tooltiptext', - Zotero.getString('pane.item.goToURL.snapshot.tooltip')); - viewButton.setAttribute('viewSnapshot', snapID); - } - else { - viewButton.setAttribute('viewURL', spec); - } - } - catch (e){Zotero.debug(e);} - viewButton.setAttribute('disabled', !validURI); - - // Enable/disable "Locate =>" (OpenURL) button - switch (_itemBeingEdited.getType()) - { - // DEBUG: handle descendents of these types as well? - case Zotero.ItemTypes.getID('book'): - case Zotero.ItemTypes.getID('bookSection'): - case Zotero.ItemTypes.getID('journalArticle'): - case Zotero.ItemTypes.getID('thesis'): - var openURL = true; - break; - - default: - var openURL = false; - } - document.getElementById('zotero-openurl').setAttribute('disabled', !openURL); - - // Clear and rebuild creator type menu - while(_creatorTypeMenu.hasChildNodes()) - { - _creatorTypeMenu.removeChild(_creatorTypeMenu.firstChild); - } - - var creatorTypes = Zotero.CreatorTypes.getTypesForItemType(_itemBeingEdited.getType()); - var localized = {}; - for (var i=0; i<creatorTypes.length; i++) - { - localized[creatorTypes[i]['name']] - = Zotero.getString('creatorTypes.' + creatorTypes[i]['name']); - } - - for (var i in localized) - { - var menuitem = document.createElement("menuitem"); - menuitem.setAttribute("label", localized[i]); - menuitem.setAttribute("typeid", Zotero.CreatorTypes.getID(i)); - _creatorTypeMenu.appendChild(menuitem); - } - - - // - // Clear and rebuild metadata fields - // - while(_dynamicFields.hasChildNodes()) - _dynamicFields.removeChild(_dynamicFields.firstChild); - - for(var i = 0, len = _itemTypeMenu.firstChild.childNodes.length; i < len; i++) - if(_itemTypeMenu.firstChild.childNodes[i].value == _itemBeingEdited.getType()) - _itemTypeMenu.selectedIndex = i; - - var fieldNames = []; - var fields = Zotero.ItemFields.getItemTypeFields(_itemBeingEdited.getField("itemTypeID")); - for (var i = 0; i<fields.length; i++) { - fieldNames.push(Zotero.ItemFields.getName(fields[i])); - } - fieldNames.push("dateAdded","dateModified"); - - for(var i = 0; i<fieldNames.length; i++) - { - var editable = !_itemBeingEdited.isPrimaryField(fieldNames[i]); - var fieldID = Zotero.ItemFields.getID(fieldNames[i]) - var val = _itemBeingEdited.getField(fieldNames[i]); - - // Start tabindex at 1000 after creators - var tabindex = editable ? (i>0 ? _tabIndexMinFields + i : 1) : 0; - _tabIndexMaxInfoFields = Math.max(_tabIndexMaxInfoFields, tabindex); - - if (editable && Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { - addDateRow(fieldNames[i], _itemBeingEdited.getField(fieldNames[i], true), tabindex); - continue; - } - - var valueElement = createValueElement( - val, fieldNames[i], tabindex, !editable - ); - - var label = document.createElement("label"); - label.setAttribute('fieldname', fieldNames[i]); - - var prefix = ''; - // Add '(...)' before 'Abstract:' for collapsed abstracts - if (fieldNames[i] == 'abstractNote') { - if (val && !Zotero.Prefs.get('lastAbstractExpand')) { - prefix = '(...) '; - } - } - label.setAttribute("value", prefix + - Zotero.ItemFields.getLocalizedString(_itemBeingEdited.getType(), fieldNames[i]) + ":"); - - if (fieldNames[i] == 'url' && val) { - label.setAttribute("isButton", true); - // TODO: make getFieldValue non-private and use below instead - label.setAttribute("onclick", "ZoteroPane.loadURI(this.nextSibling.firstChild ? this.nextSibling.firstChild.nodeValue : this.nextSibling.value, event)"); - label.setAttribute("tooltiptext", Zotero.getString('pane.item.goToURL.online.tooltip')); - } - else if (fieldNames[i] == 'abstractNote') { - label.setAttribute("onclick", "if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); } else { ZoteroItemPane.toggleAbstractExpand(this); }"); - } - else { - label.setAttribute("onclick", "if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); }"); - } - - addDynamicRow(label,valueElement); - } - - //CREATORS: - _beforeRow = _dynamicFields.firstChild.nextSibling; - _creatorCount = 0; - if(_itemBeingEdited.numCreators() > 0) - { - for(var i = 0, len=_itemBeingEdited.numCreators(); i<len; i++) - { - var creator = _itemBeingEdited.getCreator(i); - addCreatorRow(creator['firstName'], creator['lastName'], creator['creatorTypeID'], creator['fieldMode']); - } - - if (_addCreatorRow) { - addCreatorRow('', '', false, Zotero.Prefs.get('lastCreatorFieldMode'), true, false); - _addCreatorRow = false; - } - } - else - { - // Add default row - addCreatorRow('', '', false, Zotero.Prefs.get('lastCreatorFieldMode'), true, true); - } - - var focusMode = 'info'; - var focusBox = _dynamicFields; + if (index == 0) { + var itembox = document.getElementById('zotero-editpane-item-box'); + itembox.mode = 'edit'; + itembox.item = _itemBeingEdited; } // Notes pane @@ -382,7 +145,7 @@ var ZoteroItemPane = new function() label.setAttribute('crop','end'); var box = document.createElement('box'); - box.setAttribute('onclick',"ZoteroPane.selectItem("+notes[i].getID()+");"); + box.setAttribute('onclick',"ZoteroPane.selectItem(" + notes[i].id + ");"); box.setAttribute('class','zotero-clicky'); box.appendChild(icon); box.appendChild(label); @@ -390,7 +153,7 @@ var ZoteroItemPane = new function() var removeButton = document.createElement('label'); removeButton.setAttribute("value","-"); removeButton.setAttribute("class","zotero-clicky"); - removeButton.setAttribute("onclick","ZoteroItemPane.removeNote("+notes[i].getID()+")"); + removeButton.setAttribute("onclick","ZoteroItemPane.removeNote(" + notes[i].id + ")"); var row = document.createElement('row'); row.appendChild(box); @@ -441,7 +204,7 @@ var ZoteroItemPane = new function() label.setAttribute('crop','end'); var box = document.createElement('box'); - box.setAttribute('onclick',"ZoteroPane.selectItem('"+attachments[i].getID()+"')"); + box.setAttribute('onclick',"ZoteroPane.selectItem('" + attachments[i].id + "')"); box.setAttribute('class','zotero-clicky'); box.appendChild(icon); box.appendChild(label); @@ -449,7 +212,7 @@ var ZoteroItemPane = new function() var removeButton = document.createElement('label'); removeButton.setAttribute("value","-"); removeButton.setAttribute("class","zotero-clicky"); - removeButton.setAttribute("onclick","ZoteroItemPane.removeAttachment("+attachments[i].getID()+")"); + removeButton.setAttribute("onclick","ZoteroItemPane.removeAttachment(" + attachments[i].id + ")"); var row = document.createElement('row'); row.appendChild(box); @@ -466,7 +229,6 @@ var ZoteroItemPane = new function() // Tags pane else if(index == 3) { - _activeScrollbox = document.getElementById('zotero-editpane-tags').getScrollBox(); var focusMode = 'tags'; var focusBox = _tagsBox; _tagsBox.item = _itemBeingEdited; @@ -487,1271 +249,96 @@ var ZoteroItemPane = new function() } - function scrollToTop() { - if (!_activeScrollbox) { - return; - } - var sbo = _activeScrollbox.boxObject; - sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject); - sbo.scrollTo(0,0); - } - - - function ensureElementIsVisible(elem) { - if (!_activeScrollbox) { - return; - } - var sbo = _activeScrollbox.boxObject; - sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject); - sbo.ensureElementIsVisible(elem); - } - - - function changeTypeTo(itemTypeID, menu) { - if (itemTypeID == _itemBeingEdited.getType()) { - return; - } - - var fieldsToDelete = _itemBeingEdited.getFieldsNotInType(itemTypeID, true); - - // Generate list of localized field names for display in pop-up - if (fieldsToDelete) { - var fieldNames = ""; - for (var i=0; i<fieldsToDelete.length; i++) { - fieldNames += "\n - " + - Zotero.ItemFields.getLocalizedString(_itemBeingEdited.getType(), fieldsToDelete[i]); - } - - var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); - } - - if (!fieldsToDelete || - promptService.confirm(null, - Zotero.getString('pane.item.changeType.title'), - Zotero.getString('pane.item.changeType.text') + "\n" + fieldNames)) { - _itemBeingEdited.setType(itemTypeID); - _itemBeingEdited.save(); - loadPane(0); - } - // Revert the menu (which changes before the pop-up) - else { - menu.value = _itemBeingEdited.getType(); - } - } - - function onViewClick(button, event) { - if (button.getAttribute('viewURL')) { - ZoteroPane.loadURI(button.getAttribute('viewURL'), event); - } - else if (button.getAttribute('viewSnapshot')) { - ZoteroPane.viewAttachment(button.getAttribute('viewSnapshot'), event); - } - } - - function onOpenURLClick(event) + function removeNote(id) { - var url = Zotero.OpenURL.resolve(_itemBeingEdited); - if (url) - { - ZoteroPane.loadURI(url, event); - } + var note = Zotero.Items.get(id); + if(note) + if(confirm(Zotero.getString('pane.item.notes.delete.confirm'))) + note.erase(); } - function addDynamicRow(label, value, beforeElement) + function addNote() { - var row = document.createElement("row"); - row.appendChild(label); - row.appendChild(value); - if(beforeElement) - _dynamicFields.insertBefore(row, _beforeRow); - else - _dynamicFields.appendChild(row); + ZoteroPane.openNoteWindow(null, null, _itemBeingEdited.id); } - function addCreatorRow(firstName, lastName, typeID, singleField, unsaved, defaultRow) + function _noteToTitle(text) { - // Disable the "+" button on previous rows - var elems = _dynamicFields.getElementsByAttribute('value', '+'); - if (elems.length){ - ZoteroItemPane.disableButton(elems[elems.length-1]); - } - - if (singleField) - { - if (!lastName) - { - lastName = _defaultFullName; - } - } - else - { - if (!firstName) - { - firstName = _defaultFirstName; - } - if (!lastName) - { - lastName = _defaultLastName; - } - } - - // Use the first entry in the drop-down for the default type - if (!typeID) - { - typeID = _creatorTypeMenu.childNodes[0].getAttribute('typeid'); - } - - var label = document.createElement("toolbarbutton"); - label.setAttribute("label",Zotero.getString('creatorTypes.'+Zotero.CreatorTypes.getName(typeID))+":"); - label.setAttribute("typeid", typeID); - label.setAttribute("popup","zotero-creator-type-menu"); - label.setAttribute("fieldname",'creator-'+_creatorCount+'-typeID'); - label.className = 'zotero-clicky'; - - // getCreatorFields(), switchCreatorMode() and handleCreatorAutoCompleteSelect() - // may need need to be adjusted if this DOM structure changes - var hbox = document.createElement("hbox"); - - // Name - var firstlast = document.createElement("hbox"); - firstlast.setAttribute("flex","1"); - var tabindex = _tabIndexMinCreators + (_creatorCount * 2); - var lastNameLabel = firstlast.appendChild( - createValueElement( - lastName, - 'creator-' + _creatorCount + '-lastName', - tabindex - ) - ); - - // Comma - var comma = document.createElement('label'); - comma.setAttribute('value', ','); - comma.className = 'comma'; - firstlast.appendChild(comma); - - firstlast.appendChild( - createValueElement( - firstName, - 'creator-' + _creatorCount + '-firstName', - tabindex + 1 - ) - ); - if (singleField) - { - firstlast.lastChild.setAttribute('hidden', true); - } - _tabIndexMaxCreators = Math.max(_tabIndexMaxCreators, tabindex); - - hbox.appendChild(firstlast); - - // Single/double field toggle - var toggleButton = document.createElement('toolbarbutton'); - toggleButton.setAttribute('fieldname', 'creator-' + _creatorCount + '-singleField'); - toggleButton.className = 'zotero-clicky'; - hbox.appendChild(toggleButton); - - // Minus (-) button - var removeButton = document.createElement('label'); - removeButton.setAttribute("value","-"); - // If default first row, don't let user remove it - if (defaultRow){ - disableButton(removeButton); - } - else { - removeButton.setAttribute("class","zotero-clicky"); - removeButton.setAttribute("onclick","ZoteroItemPane.removeCreator("+_creatorCount+", this.parentNode.parentNode)"); - } - hbox.appendChild(removeButton); + var MAX_LENGTH = 100; - // Plus (+) button - var addButton = document.createElement('label'); - addButton.setAttribute("value","+"); - addButton.setAttribute("class","zotero-clicky"); - // If row isn't saved, don't let user add more - if (unsaved) - { - disableButton(addButton); - } - else + var t = text.substring(0, MAX_LENGTH); + var ln = t.indexOf("\n"); + if (ln>-1 && ln<MAX_LENGTH) { - _enablePlusButton(addButton, typeID, singleField); + t = t.substring(0, ln); } - hbox.appendChild(addButton); - - _creatorCount++; - addDynamicRow(label, hbox, true); - - // Set single/double field toggle mode - if (singleField) + if(t == "") { - switchCreatorMode(hbox.parentNode, true, true); + return Zotero.getString('pane.item.notes.untitled'); } else { - switchCreatorMode(hbox.parentNode, false, true); - } - - // Focus new rows - if (unsaved && !defaultRow){ - lastNameLabel.click(); + return t; } } - - /** - * Add a date row with a label editor and a ymd indicator to show date parsing - */ - function addDateRow(field, value, tabindex) + function _updateNoteCount() { - var label = document.createElement("label"); - label.setAttribute("value", Zotero.getString("itemFields." + field) + ':'); - label.setAttribute("fieldname", field); - label.setAttribute("onclick", "this.nextSibling.firstChild.blur()"); - - var hbox = document.createElement("hbox"); - var elem = createValueElement(Zotero.Date.multipartToStr(value), field, tabindex); - - // y-m-d status indicator - var datebox = document.createElement('hbox'); - datebox.className = 'zotero-date-field-status'; - var year = document.createElement('label'); - var month = document.createElement('label'); - var day = document.createElement('label'); - year.setAttribute('value', Zotero.getString('date.abbreviation.year')); - month.setAttribute('value', Zotero.getString('date.abbreviation.month')); - day.setAttribute('value', Zotero.getString('date.abbreviation.day')); - - // Display the date parts we have and hide the others - var sqldate = Zotero.Date.multipartToSQL(value); - year.setAttribute('hidden', !Zotero.Date.sqlHasYear(sqldate)); - month.setAttribute('hidden', !Zotero.Date.sqlHasMonth(sqldate)); - day.setAttribute('hidden', !Zotero.Date.sqlHasDay(sqldate)); - - datebox.appendChild(year); - datebox.appendChild(month); - datebox.appendChild(day); + var c = _notesList.childNodes.length; - var hbox = document.createElement('hbox'); - hbox.setAttribute('flex', 1); - hbox.appendChild(elem); - hbox.appendChild(datebox); + var str = 'pane.item.notes.count.'; + switch (c){ + case 0: + str += 'zero'; + break; + case 1: + str += 'singular'; + break; + default: + str += 'plural'; + break; + } - addDynamicRow(label, hbox); + _notesLabel.value = Zotero.getString(str, [c]); } - - function switchCreatorMode(row, singleField, initial) + function _updateAttachmentCount() { - // Change if button position changes - // row->hbox->label->label->toolbarbutton - var button = row.lastChild.lastChild.previousSibling.previousSibling; - var hbox = button.previousSibling; - var lastName = hbox.firstChild; - var comma = hbox.firstChild.nextSibling; - var firstName = hbox.lastChild; - - // Switch to single-field mode - if (singleField) - { - button.setAttribute('image', 'chrome://zotero/skin/textfield-dual.png'); - button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.two')); - lastName.setAttribute('singleField', 'true'); - button.setAttribute('onclick', "ZoteroItemPane.switchCreatorMode(this.parentNode.parentNode, false)"); - lastName.setAttribute('flex', '1'); - - // Remove firstname field from tabindex - var tab = parseInt(firstName.getAttribute('ztabindex')); - firstName.setAttribute('ztabindex', -1); - if (_tabIndexMaxCreators==tab) - { - _tabIndexMaxCreators--; - } - - // Hide first name field and prepend to last name field - firstName.setAttribute('hidden', true); - comma.setAttribute('hidden', true); - - if (!initial){ - var first = _getFieldValue(firstName); - if (first && first != _defaultFirstName) - { - var last = _getFieldValue(lastName); - _setFieldValue(lastName, first + ' ' + last); - } - } - - if (_getFieldValue(lastName) == _defaultLastName) - { - _setFieldValue(lastName, _defaultFullName); - } - } - // Switch to two-field mode - else - { - button.setAttribute('image', 'chrome://zotero/skin/textfield-single.png'); - button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.one')); - lastName.setAttribute('singleField', 'false'); - button.setAttribute('onclick', "ZoteroItemPane.switchCreatorMode(this.parentNode.parentNode, true)"); - lastName.setAttribute('flex', '0'); - - // Add firstname field to tabindex - var tab = parseInt(lastName.getAttribute('ztabindex')); - firstName.setAttribute('ztabindex', tab + 1); - if (_tabIndexMaxCreators==tab) - { - _tabIndexMaxCreators++; - } - - if (!initial){ - // Move all but last word to first name field and show it - var last = _getFieldValue(lastName); - if (last && last != _defaultFullName) - { - var lastNameRE = /(.*?)[ ]*([^ ]+[ ]*)$/; - var parts = lastNameRE.exec(last); - if (parts[2] && parts[2] != last) - { - _setFieldValue(lastName, parts[2]); - _setFieldValue(firstName, parts[1]); - } - } - } - - if (!_getFieldValue(firstName)) - { - _setFieldValue(firstName, _defaultFirstName); - } - - if (_getFieldValue(lastName) == _defaultFullName) - { - _setFieldValue(lastName, _defaultLastName); - } - - firstName.setAttribute('hidden', false); - comma.setAttribute('hidden', false); - } - - // Save the last-used field mode - Zotero.Prefs.set('lastCreatorFieldMode', singleField); + var c = _attachmentsList.childNodes.length; - if (!initial) - { - var [, index, field] = button.getAttribute('fieldname').split('-'); - - var otherFields = getCreatorFields(row); // row - modifyCreator(index, field, !!singleField, otherFields); + var str = 'pane.item.attachments.count.'; + switch (c){ + case 0: + str += 'zero'; + break; + case 1: + str += 'singular'; + break; + default: + str += 'plural'; + break; } - } - - - function toggleAbstractExpand(label) { - var cur = Zotero.Prefs.get('lastAbstractExpand'); - Zotero.Prefs.set('lastAbstractExpand', !cur); - var ab = label.nextSibling; - var valueText = _itemBeingEdited.getField('abstractNote'); - var tabindex = ab.getAttribute('ztabindex'); - var elem = createValueElement(valueText, 'abstractNote', tabindex); - ab.parentNode.replaceChild(elem, ab); - - var text = Zotero.ItemFields.getLocalizedString(_itemBeingEdited.getType(), 'abstractNote') + ':'; - // Add '(...)' before "Abstract:" for collapsed abstracts - if (valueText && cur) { - text = '(...) ' + text; - } - label.setAttribute('value', text); + _attachmentsLabel.value = Zotero.getString(str, [c]); } - - function disableButton(button) + function removeAttachment(id) { - button.setAttribute('disabled', true); - button.setAttribute('onclick', false); + var attachment = Zotero.Items.get(id); + if(attachment) + if(confirm(Zotero.getString('pane.item.attachments.delete.confirm'))) + attachment.erase(); } - function _enablePlusButton(button, creatorTypeID, fieldMode) + function addAttachmentFromDialog(link) { - button.setAttribute('disabled', false); - button.setAttribute("onclick", - "ZoteroItemPane.disableButton(this); ZoteroItemPane.addCreatorRow('', '', " + (creatorTypeID ? creatorTypeID : 'false') + ", " + fieldMode + ", true);"); + ZoteroPane.addAttachmentFromDialog(link, _itemBeingEdited.id); } - function createValueElement(valueText, fieldName, tabindex, noedit) + function addAttachmentFromPage(link) { - var fieldID = Zotero.ItemFields.getID(fieldName); - - // If an abstract, check last expand state - var abstractAsVbox = (fieldName == 'abstractNote') && - Zotero.Prefs.get('lastAbstractExpand'); - - if (fieldName == 'extra' || abstractAsVbox) { - var valueElement = document.createElement("vbox"); - } - else - { - var valueElement = document.createElement("label"); - } - - valueElement.setAttribute('fieldname',fieldName); - - if (!noedit){ - valueElement.setAttribute('flex', 1); - valueElement.setAttribute('ztabindex', tabindex); - valueElement.setAttribute('onclick', '/* Skip right-click on Windows */ if (event.button) { return; } ZoteroItemPane.showEditor(this)'); - valueElement.className = 'zotero-clicky'; - } - - switch (fieldName) { - case 'tag': - _tabIndexMaxTagsFields = Math.max(_tabIndexMaxTagsFields, tabindex); - break; - - // Convert dates from UTC - case 'dateAdded': - case 'dateModified': - case 'accessDate': - if (valueText){ - var date = Zotero.Date.sqlToDate(valueText, true); - valueText = date ? date.toLocaleString() : ''; - - // Don't show time for access date if none - if (fieldName == 'accessDate') { - valueText = valueText.replace('00:00:00 ', ''); - } - } - break; - } - - if (fieldID) { - // Display the SQL date as a tooltip for date fields - if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { - valueElement.setAttribute('tooltiptext', - Zotero.Date.multipartToSQL(_itemBeingEdited.getField(fieldName, true))); - } - - // Display a context menu for certain fields - if (fieldName == 'seriesTitle' || fieldName == 'shortTitle' || - Zotero.ItemFields.isFieldOfBase(fieldID, 'title') || - Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle')) { - valueElement.setAttribute('contextmenu', 'zotero-field-menu'); - } - } - - - if (fieldName.indexOf('firstName')!=-1){ - valueElement.setAttribute('flex', '1'); - } - - var firstSpace; - if(typeof valueText == 'string') - firstSpace = valueText.indexOf(" "); - - // To support newlines in 'extra' fields, we use multiple - // <description> elements inside a vbox - if (fieldName == 'extra' || abstractAsVbox) { - var lines = valueText.split("\n"); - for (var i = 0; i < lines.length; i++) { - var descriptionNode = document.createElement("description"); - var linetext = document.createTextNode(lines[i]); - descriptionNode.appendChild(linetext); - valueElement.appendChild(descriptionNode); - } - } - // 29 == arbitrary length at which to chop uninterrupted text - else if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29 - || (fieldName && - (fieldName.substr(0, 7) == 'creator') || fieldName == 'abstractNote')) { - if (fieldName == 'abstractNote') { - valueText = valueText.replace(/[\t\n]/g, ' '); - } - valueElement.setAttribute('crop', 'end'); - valueElement.setAttribute('value',valueText); - } - else - { - // Wrap to multiple lines - valueElement.appendChild(document.createTextNode(valueText)); - } - - return valueElement; - } - - function removeCreator(index, labelToDelete) - { - // If unsaved row, just remove element - if (!_itemBeingEdited.hasCreatorAt(index)){ - labelToDelete.parentNode.removeChild(labelToDelete); - - // Enable the "+" button on the previous row - var elems = _dynamicFields.getElementsByAttribute('value', '+'); - var button = elems[elems.length-1]; - var creatorFields = getCreatorFields(Zotero.getAncestorByTagName(button, 'row')); - _enablePlusButton(button, creatorFields.typeID, creatorFields.singleField); - - _creatorCount--; - return; - } - _itemBeingEdited.removeCreator(index); - _itemBeingEdited.save(); - loadPane(0); - } - - function showEditor(elem) - { - // Blur any active fields - if (_dynamicFields) { - _dynamicFields.focus(); - } - - Zotero.debug('Showing editor'); - - var fieldName = elem.getAttribute('fieldname'); - var tabindex = elem.getAttribute('ztabindex'); - - var [field, creatorIndex, creatorField] = fieldName.split('-'); - if (field == 'creator') - { - var c = _itemBeingEdited.getCreator(creatorIndex); - var value = c ? c[creatorField] : ''; - var itemID = _itemBeingEdited.getID(); - } - else if (fieldName=='tag') - { - var tagID = elem.parentNode.getAttribute('id').split('-')[1]; - var value = tagID ? Zotero.Tags.getName(tagID) : ''; - var itemID = Zotero.getAncestorByTagName(elem, 'tagsbox').item.getID(); - } - else - { - var value = _itemBeingEdited.getField(fieldName); - var itemID = _itemBeingEdited.getID(); - - // Access date needs to be converted from UTC - if (fieldName=='accessDate' && value!='') - { - var localDate = Zotero.Date.sqlToDate(value, true); - var value = Zotero.Date.dateToSQL(localDate); - } - } - - var t = document.createElement("textbox"); - t.setAttribute('value',value); - t.setAttribute('fieldname', fieldName); - t.setAttribute('ztabindex', tabindex); - t.setAttribute('flex','1'); - - if (creatorField=='lastName') - { - t.setAttribute('singleField', elem.getAttribute('singleField')); - } - - if (['title', 'abstractNote', 'extra'].indexOf(fieldName) != -1) { - t.setAttribute('multiline', true); - t.setAttribute('rows', 8); - } - else - { - var autoCompleteFields = [ - 'creator', - 'journalAbbreviation', - 'seriesTitle', - 'seriesText', - 'repository', - 'callNumber', - 'archiveLocation', - 'language', - 'rights', - 'tag' - ]; - - // Add the type-specific versions of these base fields - var baseACFields = ['publisher', 'publicationTitle', 'type', - 'medium', 'place']; - autoCompleteFields = autoCompleteFields.concat(baseACFields); - - for (var i=0; i<baseACFields.length; i++) { - var add = Zotero.ItemFields.getTypeFieldsFromBase(baseACFields[i], true) - autoCompleteFields = autoCompleteFields.concat(add); - } - - // Add auto-complete for certain fields - if (autoCompleteFields.indexOf(field) != -1) { - t.setAttribute('type', 'autocomplete'); - t.setAttribute('autocompletesearch', 'zotero'); - var suffix = itemID ? itemID : ''; - if (field=='creator') { - suffix = (elem.getAttribute('singleField')=='true' - ? '1' : '0') + '-' + suffix; - } - t.setAttribute('autocompletesearchparam', fieldName + '/' + suffix); - t.setAttribute('ontextentered', - 'ZoteroItemPane.handleCreatorAutoCompleteSelect(this)'); - } - } - var box = elem.parentNode; - box.replaceChild(t,elem); - - t.select(); - - t.setAttribute('onblur',"ZoteroItemPane.hideEditor(this, true)"); - t.setAttribute('onkeypress',"return ZoteroItemPane.handleKeyPress(event)"); - - _tabDirection = false; - _lastTabIndex = tabindex; - - return t; - } - - - /* - * Save a multiple-field selection for the creator autocomplete - * (e.g. "Shakespeare, William") - */ - function handleCreatorAutoCompleteSelect(textbox) - { - var comment = Zotero.Utilities.AutoComplete.getResultComment(textbox); - if (!comment) - { - return; - } - - var [creatorID, numFields] = comment.split('-'); - - // If result uses two fields, save both - if (numFields==2) - { - // Manually clear autocomplete controller's reference to - // textbox to prevent error next time around - textbox.mController.input = null; - - var [field, creatorIndex, creatorField] = - textbox.getAttribute('fieldname').split('-'); - - var creator = Zotero.Creators.get(creatorID); - - var otherField = creatorField=='lastName' ? 'firstName' : 'lastName'; - - // Update this textbox - textbox.setAttribute('value', creator[creatorField]); - textbox.value = creator[creatorField]; - - // Update the other label - if (otherField=='firstName'){ - var label = textbox.nextSibling.nextSibling; - } - else if (otherField=='lastName'){ - var label = textbox.previousSibling.previousSibling; - } - - if (label.firstChild){ - label.firstChild.nodeValue = creator[otherField]; - } - else { - label.value = creator[otherField]; - } - - var row = textbox.parentNode.parentNode.parentNode; - var otherFields = ZoteroItemPane.getCreatorFields(row); - otherFields[otherField] = creator[otherField]; - - ZoteroItemPane.modifyCreator(creatorIndex, creatorField, - creator[creatorField], otherFields); - } - - // Otherwise let the autocomplete popup handle matters - } - - function handleKeyPress(event){ - var target = event.target; - var focused = document.commandDispatcher.focusedElement; - - switch (event.keyCode) - { - case event.DOM_VK_RETURN: - // Prevent blur on containing textbox - event.preventDefault(); - - var fieldname = target.getAttribute('fieldname'); - // Use shift-enter as the save action for the larger fields - if ((fieldname == 'abstractNote' || fieldname == 'extra') - && !event.shiftKey) - { - break; - } - else if (fieldname == 'tag') - { - // If last tag row, create new one - var row = target.parentNode.parentNode; - if (row == row.parentNode.lastChild) - { - _tabDirection = 1; - var lastTag = true; - } - } - // Shift-enter adds new creator row - else if (fieldname.indexOf('creator-') == 0 && event.shiftKey) { - // Value hasn't changed - if (target.getAttribute('value') == target.value) { - Zotero.debug("Value hasn't changed"); - // If + button is disabled, just focus next creator row - if (Zotero.getAncestorByTagName(target, 'row').lastChild.lastChild.disabled) { - _focusNextField('info', _dynamicFields, _lastTabIndex, false); - } - else { - ZoteroItemPane.addCreatorRow('', '', false, Zotero.Prefs.get('lastCreatorFieldMode'), true, false); - } - } - // Value has changed - else { - _tabDirection = 1; - _addCreatorRow = true; - focused.blur(); - } - return false; - } - focused.blur(); - - // Return focus to items pane - if (!lastTag) { - var tree = document.getElementById('zotero-items-tree'); - if (tree) { - tree.focus(); - } - } - - return false; - - case event.DOM_VK_ESCAPE: - // Reset field to original value - target.value = target.getAttribute('value'); - - var tagsbox = Zotero.getAncestorByTagName(focused, 'tagsbox'); - - focused.blur(); - - if (tagsbox) { - tagsbox.closePopup(); - } - - // Return focus to items pane - var tree = document.getElementById('zotero-items-tree'); - if (tree) { - tree.focus(); - } - - return false; - - case event.DOM_VK_TAB: - _tabDirection = event.shiftKey ? -1 : 1; - // Blur the old manually -- not sure why this is necessary, - // but it prevents an immediate blur() on the next tag - focused.blur(); - return false; - } - - return true; - } - - function hideEditor(t, saveChanges) - { - Zotero.debug('Hiding editor'); - var textbox = Zotero.getAncestorByTagName(t, 'textbox'); - if (!textbox){ - Zotero.debug('Textbox not found in hideEditor'); - return; - } - - var fieldName = textbox.getAttribute('fieldname'); - var tabindex = textbox.getAttribute('ztabindex'); - - var value = t.value; - - var elem; - var [field, creatorIndex, creatorField] = fieldName.split('-'); - - // Creator fields - if (field == 'creator') - { - var row = textbox.parentNode.parentNode.parentNode; - - var otherFields = getCreatorFields(row); - - if (saveChanges){ - modifyCreator(creatorIndex, creatorField, value, otherFields); - } - - var val = _itemBeingEdited.getCreator(creatorIndex)[creatorField]; - - if (!val){ - // Reset to '(first)'/'(last)'/'(name)' - if (creatorField=='lastName') - { - val = otherFields['singleField'] - ? _defaultFullName : _defaultLastName; - } - else if (creatorField=='firstName') - { - val = _defaultFirstName; - } - } - - elem = createValueElement(val, fieldName, tabindex); - - // Reset creator mode settings - if (otherFields['singleField']) - { - switchCreatorMode(row, true, true); - } - else - { - switchCreatorMode(row, false, true); - } - } - - // Tags - else if (fieldName=='tag') - { - var tagsbox = Zotero.getAncestorByTagName(textbox, 'tagsbox'); - if (!tagsbox) - { - Zotero.debug('Tagsbox not found', 1); - return; - } - - var row = textbox.parentNode; - var rows = row.parentNode; - - // Tag id encoded as 'tag-1234' - var id = row.getAttribute('id').split('-')[1]; - - if (saveChanges) - { - if (id) - { - if (value) - { - // If trying to replace with another existing tag - // (which causes a delete of the row), - // clear the tab direction so we don't advance - // when the notifier kicks in - var existing = Zotero.Tags.getID(value, 0); - if (existing && id != existing) - { - _tabDirection = false; - } - var changed = tagsbox.replace(id, value); - if (changed) - { - return; - } - } - else - { - tagsbox.remove(id); - return; - } - } - // New tag - else - { - // If this is an existing automatic tag, it's going to be - // deleted and the number of rows will stay the same, - // so we have to compensate - var existingTypes = Zotero.Tags.getTypes(value); - if (existingTypes && existingTypes.indexOf(1) != -1) { - _lastTabIndex--; - } - var id = tagsbox.add(value); - } - } - - if (id) - { - elem = createValueElement(value, 'tag', tabindex); - } - else - { - // Just remove the row - // - // If there's an open popup, this throws NODE CANNOT BE FOUND - try { - var row = rows.removeChild(row); - } - catch (e) {} - tagsbox.fixPopup(); - tagsbox.closePopup(); - - _tabDirection = false; - return; - } - - var focusMode = 'tags'; - var focusBox = tagsbox; - } - - // Fields - else - { - // Access date needs to be parsed and converted to UTC - if (fieldName=='accessDate' && value!='') - { - if (Zotero.Date.isSQLDate(value) || Zotero.Date.isSQLDateTime(value)) { - var localDate = Zotero.Date.sqlToDate(value); - value = Zotero.Date.dateToSQL(localDate, true); - } - else { - var d = Zotero.Date.strToDate(value); - value = null; - if (d.year && d.month != undefined && d.day) { - d = new Date(d.year, d.month, d.day); - value = Zotero.Date.dateToSQL(d, true); - } - } - } - - if (saveChanges) { - _modifyField(fieldName,value); - } - - elem = createValueElement(_itemBeingEdited.getField(fieldName), fieldName, tabindex); - } - - var box = textbox.parentNode; - box.replaceChild(elem,textbox); - - if (_tabDirection) - { - if (!focusMode) - { - var focusMode = 'info'; - var focusBox = _dynamicFields; - } - _focusNextField(focusMode, focusBox, _lastTabIndex, _tabDirection==-1); - } - } - - function _modifyField(field, value) - { - _itemBeingEdited.setField(field,value); - return _itemBeingEdited.save(); - } - - - function _getFieldValue(field) - { - return field.firstChild - ? field.firstChild.nodeValue : field.value; - } - - function _setFieldValue(field, value) - { - if (field.firstChild) - { - field.firstChild.nodeValue = value; - } - else - { - field.value = value; - } - } - - - // TODO: work with textboxes too - function textTransform(label, mode) { - var val = _getFieldValue(label); - switch (mode) { - case 'lower': - var newVal = val.toLowerCase(); - break; - case 'title': - var utils = new Zotero.Utilities(); - var newVal = utils.capitalizeTitle(val.toLowerCase(), true); - break; - default: - throw ("Invalid transform mode '" + mode + "' in ZoteroItemPane.textTransform()"); - } - _setFieldValue(label, newVal); - _modifyField(label.getAttribute('fieldname'), newVal); - } - - - function getCreatorFields(row){ - var typeID = row.getElementsByTagName('toolbarbutton')[0].getAttribute('typeid'); - var label1 = row.getElementsByTagName('hbox')[0].firstChild.firstChild; - var label2 = label1.parentNode.lastChild; - - return { - lastName: label1.firstChild ? label1.firstChild.nodeValue - : label1.value, - firstName: label2.firstChild ? label2.firstChild.nodeValue - : label2.value, - typeID: typeID, - singleField: label1.getAttribute('singleField') == 'true' - } - } - - function modifyCreator(index, field, value, otherFields) - { - if (otherFields){ - var firstName = otherFields.firstName; - var lastName = otherFields.lastName; - var typeID = otherFields.typeID; - var singleField = otherFields.singleField; - - // Ignore '(first)'/'(last)' or '(name)' - if (singleField || firstName == _defaultFirstName){ - firstName = ''; - } - - if (lastName==_defaultFullName || lastName == _defaultLastName){ - lastName = ''; - } - } - else { - var creator = _itemBeingEdited.getCreator(index); - var firstName = creator['firstName']; - var lastName = creator['lastName']; - var typeID = creator['creatorTypeID']; - var singleField = creator['singleField']; - } - - // Don't save empty creators - if (!_itemBeingEdited.hasCreatorAt(index) && !firstName && !lastName){ - return; - } - - switch (field){ - case 'firstName': - firstName = value; - break; - case 'lastName': - lastName = value; - break; - case 'typeID': - typeID = value; - break; - case 'singleField': - singleField = value; - break; - } - - _itemBeingEdited.setCreator(index, firstName, lastName, typeID, singleField); - _itemBeingEdited.save(); - } - - - function removeNote(id) - { - var note = Zotero.Items.get(id); - if(note) - if(confirm(Zotero.getString('pane.item.notes.delete.confirm'))) - note.erase(); - } - - function addNote() - { - ZoteroPane.openNoteWindow(null, null, _itemBeingEdited.getID()); - } - - function _noteToTitle(text) - { - var MAX_LENGTH = 100; - - var t = text.substring(0, MAX_LENGTH); - var ln = t.indexOf("\n"); - if (ln>-1 && ln<MAX_LENGTH) - { - t = t.substring(0, ln); - } - - if(t == "") - { - return Zotero.getString('pane.item.notes.untitled'); - } - else - { - return t; - } - } - - function _updateNoteCount() - { - var c = _notesList.childNodes.length; - - var str = 'pane.item.notes.count.'; - switch (c){ - case 0: - str += 'zero'; - break; - case 1: - str += 'singular'; - break; - default: - str += 'plural'; - break; - } - - _notesLabel.value = Zotero.getString(str, [c]); - } - - function _updateAttachmentCount() - { - var c = _attachmentsList.childNodes.length; - - var str = 'pane.item.attachments.count.'; - switch (c){ - case 0: - str += 'zero'; - break; - case 1: - str += 'singular'; - break; - default: - str += 'plural'; - break; - } - - _attachmentsLabel.value = Zotero.getString(str, [c]); - } - - function removeAttachment(id) - { - var attachment = Zotero.Items.get(id); - if(attachment) - if(confirm(Zotero.getString('pane.item.attachments.delete.confirm'))) - attachment.erase(); - } - - function addAttachmentFromDialog(link) - { - ZoteroPane.addAttachmentFromDialog(link, _itemBeingEdited.getID()); - } - - function addAttachmentFromPage(link) - { - ZoteroPane.addAttachmentFromPage(link, _itemBeingEdited.getID()); - } - - - function focusFirstField(mode) { - switch (mode) { - case 'info': - _focusNextField('info', _dynamicFields, 0, false); - break; - } - } - - - /* - * Advance the field focus forward or backward - * - * Note: We're basically replicating the built-in tabindex functionality, - * which doesn't work well with the weird label/textbox stuff we're doing. - * (The textbox being tabbed away from is deleted before the blur() - * completes, so it doesn't know where it's supposed to go next.) - */ - function _focusNextField(mode, box, tabindex, back){ - tabindex = parseInt(tabindex); - if (back) - { - if (mode=='info') - { - switch (tabindex) - { - case 1: - //Zotero.debug('At beginning'); - document.getElementById('zotero-editpane-type-menu').focus(); - return false; - - case _tabIndexMinCreators: - var nextIndex = 1; - break; - - case _tabIndexMinFields: - var nextIndex = _tabIndexMaxCreators; - break; - - default: - var nextIndex = tabindex - 1; - } - } - else if (mode=='tags') - { - switch (tabindex) - { - case 1: - return false; - - default: - var nextIndex = tabindex - 1; - } - } - } - else - { - if (mode=='info') - { - switch (tabindex) - { - case 1: - var nextIndex = _tabIndexMinCreators; - break; - - case _tabIndexMaxCreators: - var nextIndex = _tabIndexMinFields; - break; - - case _tabIndexMaxInfoFields: - //Zotero.debug('At end'); - return false; - - default: - var nextIndex = tabindex + 1; - } - } - else if (mode=='tags') - { - switch (tabindex) - { - case _tabIndexMaxTagsFields: - // In tags box, keep going to create new row - var nextIndex = tabindex + 1; - break; - - default: - var nextIndex = tabindex + 1; - } - } - } - - Zotero.debug('Looking for tabindex ' + nextIndex, 4); - switch (mode) - { - case 'info': - var next = box.getElementsByAttribute('ztabindex', nextIndex); - if (!next[0]) - { - //Zotero.debug("Next field not found"); - return _focusNextField(mode, box, nextIndex, back); - } - break; - - // Tags pane - case 'tags': - var next = document.getAnonymousNodes(box)[0]. - getElementsByAttribute('ztabindex', nextIndex); - if (!next[0]){ - next[0] = box.addDynamicRow(); - } - break; - } - - next[0].click(); - ensureElementIsVisible(next[0]); - return true; + ZoteroPane.addAttachmentFromPage(link, _itemBeingEdited.id); } } diff --git a/chrome/content/zotero/itemPane.xul b/chrome/content/zotero/itemPane.xul @@ -28,53 +28,11 @@ <overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - <script src="itemPane.js"/> - <deck id="zotero-view-item" flex="1" onselect="if (this.selectedIndex!==''){ ZoteroItemPane.loadPane(this.selectedIndex) }"> - <scrollbox id="zotero-info" flex="1" orient="vertical" style="overflow:auto"> - <popupset> - <popup id="zotero-creator-type-menu" position="after_start" - oncommand="var otherFields = ZoteroItemPane.getCreatorFields(document.popupNode.parentNode); - var typeID = event.explicitOriginalTarget.getAttribute('typeid'); - document.popupNode.setAttribute('label', - Zotero.getString('creatorTypes.' + Zotero.CreatorTypes.getName(typeID)) + ':'); - document.popupNode.setAttribute('typeid', typeID); - ZoteroItemPane.modifyCreator(document.popupNode.getAttribute('fieldname').split('-')[1], - 'typeID', typeID, otherFields)"/> - <popup id="zotero-field-menu"> - <menu label="&zotero.item.textTransform;"> - <menupopup> - <menuitem label="&zotero.item.textTransform.lowercase;" class="menuitem-non-iconic" - oncommand="ZoteroItemPane.textTransform(document.popupNode, 'lower')"/> - <menuitem label="&zotero.item.textTransform.titlecase;" class="menuitem-non-iconic" - oncommand="ZoteroItemPane.textTransform(document.popupNode, 'title')"/> - </menupopup> - </menu> - </popup> - </popupset> - <hbox id="zotero-editpane-go-buttons" align="center"> - <button id="zotero-go-to-url" - onfocus="ZoteroItemPane.ensureElementIsVisible(this)" - oncommand="ZoteroItemPane.onViewClick(this, event)" disabled="false"/> - <button id="zotero-openurl" label="&zotero.toolbar.openURL.label;" - tooltiptext="&zotero.toolbar.openURL.tooltip;" - onfocus="ZoteroItemPane.ensureElementIsVisible(this)" - oncommand="ZoteroItemPane.onOpenURLClick(event);"/> - </hbox> - <hbox align="center"> - <menulist id="zotero-editpane-type-menu" oncommand="ZoteroItemPane.changeTypeTo(this.value, this)" flex="1" - onfocus="ZoteroItemPane.ensureElementIsVisible(this)" - onkeypress="if (event.keyCode == event.DOM_VK_TAB){ if (!event.shiftKey) { ZoteroItemPane.focusFirstField('info'); event.preventDefault(); } }"> - <menupopup/> - </menulist> - </hbox> - <grid flex="1"> - <columns> - <column/> - <column flex="1"/> - </columns> - <rows id="zotero-editpane-dynamic-fields" flex="1"/> - </grid> - </scrollbox> + <script src="itemPane.js"/> + + <deck id="zotero-view-item" flex="1" onselect="if (this.selectedIndex !== '') { ZoteroItemPane.loadPane(this.selectedIndex); }"> + <zoteroitembox id="zotero-editpane-item-box" flex="1"/> + <vbox flex="1"> <hbox align="center"> <label id="zotero-editpane-notes-label"/> diff --git a/chrome/content/zotero/merge.js b/chrome/content/zotero/merge.js @@ -0,0 +1,214 @@ +var Zotero_Merge_Window = new function () { + this.init = init; + this.onBack = onBack; + this.onNext = onNext; + this.onFinish = onFinish; + this.onCancel = onCancel; + + var _wizard = null; + var _wizardPage = null; + var _mergeGroup = null; + var _numObjects = null; + + var _initialized = false; + var _io = null; + var _objects = null; + var _merged = []; + var _pos = -1; + + function init() { + _wizard = document.getElementsByTagName('wizard')[0]; + _wizardPage = document.getElementsByTagName('wizardpage')[0]; + _mergeGroup = document.getElementsByTagName('zoteromergegroup')[0]; + + if (screen.width > 1000) { + _wizard.setAttribute('zoterowidescreen', 'true'); + } + + // Set font size from pref + Zotero.setFontSize(_wizardPage); + + // TODO: localize + _wizard.getButton('cancel').setAttribute('label', "Cancel Sync") + + _io = window.arguments[0]; + _objects = _io.dataIn.objects; + if (!_objects.length) { + // TODO: handle no objects + return; + } + + var firstObj = _objects[0][0] == 'deleted' ? _objects[0][1] : _objects[0][0]; + + if (firstObj instanceof Zotero.Item) { + if (firstObj.isNote()) { + _mergeGroup.type = 'note'; + } + else { + _mergeGroup.type = 'item'; + } + } + else { + throw ("Invalid merge object type '" + firstObj.constructor.name + + "' in Zotero_Merge_Window.init()"); + } + + _mergeGroup.leftCaption = _io.dataIn.captions[0]; + _mergeGroup.rightCaption = _io.dataIn.captions[1]; + _mergeGroup.mergeCaption = _io.dataIn.captions[2]; + + _numObjects = document.getElementById('zotero-merge-num-objects'); + document.getElementById('zotero-merge-total-objects').value = _objects.length; + + this.onNext(); + } + + + function onBack() { + _pos--; + + if (_pos == 0) { + _wizard.canRewind = false; + } + + _merged[_pos + 1] = _getCurrentMergeObject(); + + _numObjects.value = _pos + 1; + + _mergeGroup.left = _objects[_pos][0]; + _mergeGroup.right = _objects[_pos][1]; + + // Restore previously merged object into merge pane + _mergeGroup.merge = _merged[_pos].ref; + _mergeGroup.leftpane.removeAttribute("selected"); + _mergeGroup.rightpane.removeAttribute("selected"); + + _updateChangedCreators(); + + if (Zotero.isMac) { + _wizard.getButton("next").setAttribute("hidden", "false"); + _wizard.getButton("finish").setAttribute("hidden", "true"); + } + else { + var buttons = document.getAnonymousElementByAttribute(_wizard, "anonid", "Buttons"); + var deck = document.getAnonymousElementByAttribute(buttons, "anonid", "WizardButtonDeck"); + deck.selectedIndex = 1; + } + } + + + function onNext() { + if (_pos + 1 == _objects.length) { + return true; + } + + _pos++; + + if (_pos == 0) { + _wizard.canRewind = false; + } + else { + _wizard.canRewind = true; + + // Save merged object to return array + _merged[_pos - 1] = _getCurrentMergeObject(); + } + + // Adjust counter + _numObjects.value = _pos + 1; + + _mergeGroup.left = _objects[_pos][0]; + _mergeGroup.right = _objects[_pos][1]; + + // Restore previously merged object into merge pane + if (_merged[_pos]) { + _mergeGroup.merge = _merged[_pos].ref; + _mergeGroup.leftpane.removeAttribute("selected"); + _mergeGroup.rightpane.removeAttribute("selected"); + } + + _updateChangedCreators(); + + // On Windows the buttons don't move when one is hidden + if ((_pos + 1) != _objects.length) { + if (Zotero.isMac) { + _wizard.getButton("next").setAttribute("hidden", "false"); + _wizard.getButton("finish").setAttribute("hidden", "true"); + } + else { + var buttons = document.getAnonymousElementByAttribute(_wizard, "anonid", "Buttons"); + var deck = document.getAnonymousElementByAttribute(buttons, "anonid", "WizardButtonDeck"); + deck.selectedIndex = 1; + } + } + // Last object + else { + if (Zotero.isMac) { + _wizard.getButton("next").setAttribute("hidden", "true"); + _wizard.getButton("finish").setAttribute("hidden", "false"); + } + // Windows uses a deck to switch between the Next and Finish buttons + // TODO: check Linux + else { + var buttons = document.getAnonymousElementByAttribute(_wizard, "anonid", "Buttons"); + var deck = document.getAnonymousElementByAttribute(buttons, "anonid", "WizardButtonDeck"); + deck.selectedIndex = 0; + } + } + + return false; + } + + + function onFinish() { + _merged[_pos] = _getCurrentMergeObject(); + + _io.dataOut = _merged; + return true; + } + + + function onCancel() { + // if already merged, ask + } + + + function _getCurrentMergeObject() { + var id = _mergeGroup.merge == 'deleted' ? + (_mergeGroup.left == 'deleted' + ? _mergeGroup.right.id : _mergeGroup.left.id) + : _mergeGroup.merge.id; + + return { + id: id, + ref: _mergeGroup.merge, + left: _mergeGroup.left, + right: _mergeGroup.right + }; + } + + + // Hack to support creator reconciliation via item view + function _updateChangedCreators() { + if (_mergeGroup.type == 'item' && _io.dataIn.changedCreators) { + var originalCreators = _mergeGroup.rightpane.original.getCreators(); + var clonedCreators = _mergeGroup.rightpane.ref.getCreators(); + var refresh = false; + for (var i in originalCreators) { + if (_io.dataIn.changedCreators[originalCreators[i].ref.id]) { + var changedCreator = _io.dataIn.changedCreators[originalCreators[i].ref.id]; + _mergeGroup.rightpane.original.setCreator( + i, changedCreator, originalCreators[i].creatorTypeID + ); + clonedCreators[i].ref = changedCreator; + refresh = true; + } + } + + if (refresh) { + _mergeGroup.rightpane.objectbox.refresh(); + _mergeGroup.mergepane.objectbox.refresh(); + } + } + } +} diff --git a/chrome/content/zotero/merge.xul b/chrome/content/zotero/merge.xul @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<!-- + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +--> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://zotero/skin/zotero.css" type="text/css"?> +<?xml-stylesheet href="chrome://zotero/skin/merge.css" type="text/css"?> + +<!-- <!DOCTYPE window SYSTEM "chrome://zotero/locale/merge.dtd"> --> + +<wizard + id="zotero-merge-window" + orient="vertical" + title="" + onwizardfinish="return Zotero_Merge_Window.onFinish()" + onwizardcancel="return Zotero_Merge_Window.onCancel()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="include.js"/> + <script src="merge.js"/> + + <wizardpage onpageshow="Zotero_Merge_Window.init()" + onpagerewound="Zotero_Merge_Window.onBack(); return false" + onpageadvanced="return Zotero_Merge_Window.onNext()" + label="Conflict Resolution"> + <zoteromergegroup flex="1"/> + <separator class="thin"/> + <hbox id="zotero-step-count"> + <label id="zotero-merge-num-objects"/> + <label value="of"/> + <label id="zotero-merge-total-objects"/> + </hbox> + </wizardpage> +</wizard> diff --git a/chrome/content/zotero/note.js b/chrome/content/zotero/note.js @@ -23,15 +23,15 @@ var noteEditor; var notifierUnregisterID; -function onLoad() -{ - noteEditor = document.getElementById('note-editor'); +function onLoad() { + noteEditor = document.getElementById('zotero-note-editor'); + noteEditor.mode = 'edit'; noteEditor.focus(); // Set font size from pref Zotero.setFontSize(noteEditor); - var params = new Array(); + var params = []; var b = document.location.href.substr(document.location.href.indexOf('?')+1).split('&'); for(var i = 0; i < b.length; i++) { @@ -39,30 +39,33 @@ function onLoad() params[b[i].substr(0,mid)] = b[i].substr(mid+1); } - var itemID = params['id']; - var collectionID = params['coll']; - var parentItemID = params['p']; + var itemID = params.id; + var collectionID = params.coll; + var parentItemID = params.p; if (itemID) { var ref = Zotero.Items.get(itemID); // Make sure Undo doesn't wipe out the note - if (!noteEditor.note || noteEditor.note.getID() != ref.getID()) { - noteEditor.id('noteField').editor.enableUndo(false); + if (!noteEditor.item || noteEditor.item.id != ref.id) { + noteEditor.disableUndo(); } - noteEditor.note = ref; - noteEditor.id('noteField').editor.enableUndo(true); + noteEditor.item = ref; + noteEditor.enableUndo(); document.title = ref.getNoteTitle(); } - else if (parentItemID) { - var ref = Zotero.Items.get(parentItemID); - noteEditor.item = ref; - } - else - { - if(collectionID && collectionID != '' && collectionID != 'undefined') - noteEditor.collection = Zotero.Collections.get(collectionID); + else { + if (parentItemID) { + var ref = Zotero.Items.get(parentItemID); + noteEditor.parent = ref; + } + else { + if (collectionID && collectionID != '' && collectionID != 'undefined') { + noteEditor.collection = Zotero.Collections.get(collectionID); + } + } + noteEditor.refresh(); } notifierUnregisterID = Zotero.Notifier.registerObserver(NotifyCallback, 'item'); @@ -79,20 +82,19 @@ function onUnload() var NotifyCallback = { notify: function(action, type, ids){ // DEBUG: why does this reset without checking the modified ids? - if (noteEditor.note) { - noteEditor.note = noteEditor.note; + if (noteEditor.item) { + noteEditor.item = noteEditor.item; // If the document title hasn't yet been set, reset undo so // undoing to empty isn't possible var noteTitle = noteEditor.note.getNoteTitle(); if (!document.title && noteTitle != '') { - noteEditor.id('noteField').editor.enableUndo(false); - noteEditor.id('noteField').editor.enableUndo(true); + noteEditor.clearUndo(); document.title = noteTitle; } // Update the window name (used for focusing) in case this is a new note - window.name = 'zotero-note-' + noteEditor.note.getID(); + window.name = 'zotero-note-' + noteEditor.item.id; } } } diff --git a/chrome/content/zotero/note.xul b/chrome/content/zotero/note.xul @@ -22,5 +22,5 @@ </keyset> <command id="cmd_close" oncommand="window.close();"/> - <noteeditor id="note-editor" flex="1"/> + <zoteronoteeditor id="zotero-note-editor" flex="1"/> </window> \ No newline at end of file diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js @@ -504,7 +504,7 @@ var ZoteroPane = new function() return false; } - var item = new Zotero.Item(typeID); + var item = new Zotero.Item(false, typeID); for (var i in data) { @@ -514,13 +514,13 @@ var ZoteroPane = new function() item.save(); if (this.itemsView && this.itemsView._itemGroup.isCollection()) { - this.itemsView._itemGroup.ref.addItem(item.getID()); + this.itemsView._itemGroup.ref.addItem(item.id); } //set to Info tab document.getElementById('zotero-view-item').selectedIndex = 0; - this.selectItem(item.getID()); + this.selectItem(item.id); return item; } @@ -548,7 +548,10 @@ var ZoteroPane = new function() newName.value = untitled; } - Zotero.Collections.add(newName.value, parent); + var collection = new Zotero.Collection; + collection.name = newName.value; + collection.parent = parent; + collection.save(); } function newSearch() @@ -737,7 +740,7 @@ var ZoteroPane = new function() Zotero.Prefs.set('lastViewedFolder', 'L'); } if (itemgroup.isCollection()) { - Zotero.Prefs.set('lastViewedFolder', 'C' + itemgroup.ref.getID()); + Zotero.Prefs.set('lastViewedFolder', 'C' + itemgroup.ref.id); } else if (itemgroup.isSearch()) { Zotero.Prefs.set('lastViewedFolder', 'S' + itemgroup.ref.id); @@ -760,22 +763,23 @@ var ZoteroPane = new function() { var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex); - if(item.isNote()) + if(item.ref.isNote()) { var noteEditor = document.getElementById('zotero-note-editor'); + noteEditor.mode = 'edit'; // 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.note || noteEditor.note.getID() != item.ref.getID()) { - noteEditor.id('noteField').editor.enableUndo(false); + if (!noteEditor.item || noteEditor.item.id != item.ref.id) { + noteEditor.disableUndo(); } - noteEditor.item = null; - noteEditor.note = item.ref; + noteEditor.parent = null; + noteEditor.item = item.ref; - noteEditor.id('noteField').editor.enableUndo(true); + noteEditor.enableUndo(); - document.getElementById('zotero-view-note-button').setAttribute('noteID',item.ref.getID()); + document.getElementById('zotero-view-note-button').setAttribute('noteID',item.ref.id); if(item.ref.getSource()) { document.getElementById('zotero-view-note-button').setAttribute('sourceID',item.ref.getSource()); @@ -786,7 +790,7 @@ var ZoteroPane = new function() } document.getElementById('zotero-item-pane-content').selectedIndex = 2; } - else if(item.isAttachment()) + else if(item.ref.isAttachment()) { // DEBUG: this is annoying -- we really want to use an abstracted // version of createValueElement() from itemPane.js @@ -904,7 +908,7 @@ var ZoteroPane = new function() document.getElementById('zotero-attachment-view').setAttribute('label', str); // Display page count - var pages = Zotero.Fulltext.getPages(item.ref.getID()); + var pages = Zotero.Fulltext.getPages(item.ref.id); var pages = pages ? pages.total : null; var pagesRow = document.getElementById('zotero-attachment-pages'); if (pages) { @@ -919,8 +923,9 @@ var ZoteroPane = new function() this.updateItemIndexedState(); var noteEditor = document.getElementById('zotero-attachment-note-editor'); - noteEditor.item = null; - noteEditor.note = item.ref; + noteEditor.mode = 'edit'; + noteEditor.parent = null; + noteEditor.item = item.ref; document.getElementById('zotero-item-pane-content').selectedIndex = 3; } @@ -956,7 +961,7 @@ var ZoteroPane = new function() var reindexButton = document.getElementById('zotero-attachment-reindex'); var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex); - var status = Zotero.Fulltext.getIndexedState(item.ref.getID()); + var status = Zotero.Fulltext.getIndexedState(item.ref.id); var str = 'fulltext.indexState.'; switch (status) { case Zotero.Fulltext.INDEX_STATE_UNAVAILABLE: @@ -980,7 +985,7 @@ var ZoteroPane = new function() var str = Zotero.getString('pane.items.menu.reindexItem'); reindexButton.setAttribute('tooltiptext', str); - if (Zotero.Fulltext.canReindex(item.ref.getID())) { + if (Zotero.Fulltext.canReindex(item.ref.id)) { reindexButton.setAttribute('hidden', false); } else { @@ -999,7 +1004,7 @@ var ZoteroPane = new function() if (!items[i].isAttachment()) { continue; } - var itemID = items[i].getID(); + var itemID = items[i].id; Zotero.Fulltext.indexItems(itemID, true); } this.updateItemIndexedState(); @@ -1007,11 +1012,12 @@ var ZoteroPane = new function() function duplicateSelectedItem() { - var newItemID = this.getSelectedItems()[0].clone(); + var newItem = this.getSelectedItems()[0].clone(); + var newItemID = newItem.save() var newItem = Zotero.Items.get(newItemID); if (this.itemsView._itemGroup.isCollection()) { - this.itemsView._itemGroup.ref.addItem(newItem.getID()); + this.itemsView._itemGroup.ref.addItem(newItem.id); this.selectItem(newItemID); } } @@ -1089,30 +1095,29 @@ var ZoteroPane = new function() function editSelectedCollection() { if (this.collectionsView.selection.count > 0) { - var collection = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex); + var row = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex); - if(collection.isCollection()) - { + if (row.isCollection()) { var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService); - var newName = { value: collection.getName() }; + var newName = { value: row.getName() }; var result = promptService.prompt(window, "", Zotero.getString('pane.collections.rename'), newName, "", {}); - if (result && newName.value) - { - collection.ref.rename(newName.value); + if (result && newName.value) { + row.ref.name = newName.value; + row.ref.save(); } } - else - { + else { var s = new Zotero.Search(); - s.load(collection.ref['id']); - var io = {dataIn: {search: s, name: collection.getName()}, dataOut: null}; + s.load(row.ref.id); + var io = {dataIn: {search: s, name: row.getName()}, dataOut: null}; window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io); - if(io.dataOut) + if (io.dataOut) { this.onCollectionSelected(); //reload itemsView + } } } } @@ -1243,7 +1248,7 @@ var ZoteroPane = new function() && this.collectionsView.selection.currentIndex != -1) { var collection = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex); if (collection && collection.isCollection()) { - return asID ? collection.ref.getID() : collection.ref; + return asID ? collection.ref.id : collection.ref; } } // If the Zotero pane hasn't yet been opened, use the lastViewedFolder pref @@ -1253,7 +1258,7 @@ var ZoteroPane = new function() if (matches && matches[1] == 'C') { var col = Zotero.Collections.get(matches[2]); if (col) { - return asID ? col.getID() : col; + return asID ? col.id : col; } } } @@ -1470,7 +1475,7 @@ var ZoteroPane = new function() else { var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex).ref; - var itemID = item.getID(); + var itemID = item.id; menu.setAttribute('itemID', itemID); // Show in Library @@ -1493,7 +1498,7 @@ var ZoteroPane = new function() if (item.isAttachment()) { hide.push(m.duplicateItem); // If not linked URL, show reindex line - if (Zotero.Fulltext.canReindex(item.getID())) { + if (Zotero.Fulltext.canReindex(item.id)) { show.push(m.sep4, m.reindexItem); } else { @@ -1757,7 +1762,12 @@ var ZoteroPane = new function() } catch (e){} - var itemID = Zotero.Notes.add(text, parent); + var item = new Zotero.Item(false, 'note'); + item.setNote(text); + if (parent) { + item.setSource(parent); + } + var itemID = item.save(); if (this.itemsView && this.itemsView._itemGroup.isCollection()) { this.itemsView._itemGroup.ref.addItem(itemID); @@ -1772,7 +1782,7 @@ var ZoteroPane = new function() // TODO: _text_ var c = this.getSelectedCollection(); if (c) { - this.openNoteWindow(null, c.getID()); + this.openNoteWindow(null, c.id); } else { this.openNoteWindow(); @@ -1796,10 +1806,13 @@ var ZoteroPane = new function() var items = this.getSelectedItems(); if (this.itemsView.selection.count == 1 && items[0] && items[0].isNote()) { var note = items[0].getNote() - items[0].updateNote(note == '' ? text : note + "\n\n" + text); + + items[0].setNote(note == '' ? text : note + "\n\n" + text); + items[0].save(); + var noteElem = document.getElementById('zotero-note-editor') noteElem.focus(); - noteElem.id('noteField').inputField.editor. + noteElem.noteField.inputField.editor. selectionController.scrollSelectionIntoView(1, 1, true); @@ -1892,17 +1905,17 @@ var ZoteroPane = new function() var item = this.newItem(Zotero.ItemTypes.getID('webpage'), data); // Automatically save snapshot if pref set - if (item.getID() && Zotero.Prefs.get('automaticSnapshots')) + if (item.id && Zotero.Prefs.get('automaticSnapshots')) { var f = function() { // We set |noParent|, since child items don't belong to collections - ZoteroPane.addAttachmentFromPage(false, item.getID(), true); + ZoteroPane.addAttachmentFromPage(false, item.id, true); } // Give progress window time to appear setTimeout(f, 300); } - return item.getID(); + return item.id; } @@ -1930,7 +1943,7 @@ var ZoteroPane = new function() progressWin.startCloseTimer(); if (this.itemsView && this.itemsView._itemGroup.isCollection()) { - var parentCollectionID = this.itemsView._itemGroup.ref.getID(); + var parentCollectionID = this.itemsView._itemGroup.ref.id; } } @@ -2026,7 +2039,7 @@ var ZoteroPane = new function() } } else { - this.showAttachmentNotFoundDialog(attachment.getID()) + this.showAttachmentNotFoundDialog(attachment.id) } } } diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul @@ -82,7 +82,7 @@ <popup id="zotero-collectionmenu" onpopupshowing="ZoteroPane.buildCollectionContextMenu();"> <menuitem label="&zotero.toolbar.newCollection.label;" oncommand="ZoteroPane.newCollection()"/> <menuitem label="&zotero.toolbar.newSavedSearch.label;" oncommand="ZoteroPane.newSearch()"/> - <menuitem label="&zotero.toolbar.newSubcollection.label;" oncommand="ZoteroPane.newCollection(ZoteroPane.getSelectedCollection().getID())"/> + <menuitem label="&zotero.toolbar.newSubcollection.label;" oncommand="ZoteroPane.newCollection(ZoteroPane.getSelectedCollection().id)"/> <menuseparator/> <menuitem oncommand="ZoteroPane.editSelectedCollection();"/> <menuitem oncommand="ZoteroPane.deleteSelectedCollection();"/> @@ -125,6 +125,10 @@ <menuitem id="zotero-tb-actions-export" label="&zotero.toolbar.export.label;" oncommand="Zotero_File_Interface.exportFile();"/> <menuseparator id="zotero-tb-actions-utilities-separator"/> <menuitem id="zotero-tb-actions-timeline" label="&zotero.toolbar.timeline.label;" oncommand="Zotero_Timeline_Interface.loadTimeline()"/> + <menuseparator id="zotero-tb-actions-sync-separator"/> + <menuitem label="Clear Server Data" oncommand="Zotero.Sync.Server.clear()"/> + <menuitem label="Reset Server Lock" oncommand="Zotero.Sync.Server.resetServer()"/> + <menuitem label="Reset Client" oncommand="Zotero.Sync.Server.resetClient()"/> <menuseparator id="zotero-tb-actions-separator"/> <menuitem id="zotero-tb-actions-prefs" label="&zotero.toolbar.preferences.label;" oncommand="window.openDialog('chrome://zotero/content/preferences/preferences.xul', 'zotero-prefs', 'chrome,titlebar,toolbar,' + Zotero.Prefs.get('browser.preferences.instantApply', true) ? 'dialog=no' : 'modal')"/> @@ -283,6 +287,8 @@ <vbox id="zotero-item-pane" persist="width"> <toolbar align="right"> + <toolbarbutton tooltiptext="Sync with Zotero Server" image="chrome://zotero/skin/arrow_refresh.png" oncommand="Zotero.Sync.Server.sync()"/> + <toolbarseparator/> <toolbarbutton id="zotero-tb-fullscreen" tooltiptext="&zotero.toolbar.fullscreen.tooltip;" oncommand="ZoteroPane.fullScreen();"/> <toolbarbutton class="tabs-closebutton" oncommand="ZoteroPane.toggleDisplay()"/> </toolbar> @@ -303,7 +309,7 @@ <deck id="zotero-view-item" flex="1"/> <!-- Note info pane --> <vbox id="zotero-view-note" flex="1"> - <noteeditor id="zotero-note-editor" flex="1"/> + <zoteronoteeditor id="zotero-note-editor" flex="1"/> <button id="zotero-view-note-button" label="&zotero.notes.separate;" oncommand="ZoteroPane.openNoteWindow(this.getAttribute('noteID')); if(this.hasAttribute('sourceID')) ZoteroPane.selectItem(this.getAttribute('sourceID'));"/> </vbox> <!-- Attachment info pane --> @@ -324,7 +330,7 @@ <toolbarbutton id="zotero-attachment-reindex" oncommand="ZoteroPane.reindexItem()"/> </hbox> - <noteeditor id="zotero-attachment-note-editor" notitle="1" flex="1"/> + <zoteronoteeditor id="zotero-attachment-note-editor" notitle="1" flex="1"/> </vbox> </deck> </groupbox> diff --git a/chrome/content/zotero/preferences/preferences.xul b/chrome/content/zotero/preferences/preferences.xul @@ -153,6 +153,37 @@ To add a new preference: </prefpane> + <!-- localize --> + <prefpane id="zotero-prefpane-sync" + label="Sync" + onpaneload="document.getElementById('sync-password').value = Zotero.Sync.Server.password;" + image="chrome://zotero/skin/prefs-sync.png"> + <preferences> + <preference id="pref-sync-username" name="extensions.zotero.sync.server.username" type="string"/> + </preferences> + + <grid> + <columns> + <columns/> + <columns/> + </columns> + + <rows> + <row> + <label value="Username:"/> + <textbox preference="pref-sync-username" + onchange="Zotero.Prefs.set('sync.server.username', this.value); var pass = document.getElementById('sync-password'); if (pass.value) { Zotero.Sync.Server.password = pass.value; }"/> + </row> + <row> + <label value="Password:"/> + <textbox id="sync-password" type="password" + onchange="Zotero.Sync.Server.password = this.value"/> + </row> + </rows> + </grid> + </prefpane> + + <prefpane id="zotero-prefpane-search" label="&zotero.preferences.prefpane.search;" onpaneload="updateIndexStats()" diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js @@ -51,10 +51,11 @@ Zotero.Attachments = new function(){ try { // Create a new attachment - var attachmentItem = new Zotero.Item('attachment'); + var attachmentItem = new Zotero.Item(false, 'attachment'); attachmentItem.setField('title', title); - attachmentItem.save(); - var itemID = attachmentItem.getID(); + attachmentItem.setSource(sourceItemID); + attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_FILE; + var itemID = attachmentItem.save(); // Create directory for attachment files within storage directory var destDir = this.createDirectoryForItem(itemID); @@ -69,8 +70,9 @@ Zotero.Attachments = new function(){ var mimeType = Zotero.MIME.getMIMETypeFromFile(newFile); - _addToDB(newFile, null, null, this.LINK_MODE_IMPORTED_FILE, - mimeType, null, sourceItemID, itemID); + attachmentItem.attachmentMIMEType = mimeType; + attachmentItem.attachmentPath = this.getPath(newFile, this.LINK_MODE_IMPORTED_FILE); + attachmentItem.save(); Zotero.DB.commitTransaction(); @@ -124,14 +126,18 @@ Zotero.Attachments = new function(){ try { // Create a new attachment - var attachmentItem = new Zotero.Item('attachment'); + var attachmentItem = new Zotero.Item(false, 'attachment'); attachmentItem.setField('title', title); attachmentItem.setField('url', url); + attachmentItem.setSource(sourceItemID); + attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_URL; + attachmentItem.attachmentMIMEType = mimeType; + attachmentItem.attachmentCharset = charset; + // DEBUG: this should probably insert access date too so as to // create a proper item, but at the moment this is only called by // translate.js, which sets the metadata fields itself - attachmentItem.save(); - var itemID = attachmentItem.getID(); + var itemID = attachmentItem.save(); var storageDir = Zotero.getStorageDirectory(); file.parent.copyTo(storageDir, itemID); @@ -143,8 +149,9 @@ Zotero.Attachments = new function(){ newFile.append(itemID); newFile.append(file.leafName); - _addToDB(newFile, url, null, this.LINK_MODE_IMPORTED_URL, mimeType, - charsetID, sourceItemID, itemID); + attachmentItem.path = this.getPath(newFile, this.LINK_MODE_IMPORTED_URL); + attachmentItem.save(); + Zotero.DB.commitTransaction(); // Determine charset and build fulltext index @@ -245,11 +252,13 @@ Zotero.Attachments = new function(){ try { // Create a new attachment - var attachmentItem = new Zotero.Item('attachment'); + var attachmentItem = new Zotero.Item(false, 'attachment'); attachmentItem.setField('title', title); attachmentItem.setField('url', url); attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); - // Don't send a Notifier event on the incomplete item + attachmentItem.setSource(sourceItemID); + attachmentItem.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL; + attachmentItem.attachmentMIMEType = mimeType; var itemID = attachmentItem.save(); // Add to collections @@ -271,18 +280,21 @@ Zotero.Attachments = new function(){ wbp.progressListener = new Zotero.WebProgressFinishListener(function(){ try { + var attachmentItem = Zotero.Items.get(itemID); + var str = Zotero.File.getSample(file); if (mimeType == 'application/pdf' && Zotero.MIME.sniffForMIMEType(str) != 'application/pdf') { Zotero.debug("Downloaded PDF did not have MIME type " + "'application/pdf' in Attachments.importFromURL()", 2); - var item = Zotero.Items.get(itemID); - item.erase(); + attachmentItem.erase(); return; } - _addToDB(file, url, title, Zotero.Attachments.LINK_MODE_IMPORTED_URL, - mimeType, null, sourceItemID, itemID); + attachmentItem.attachmentPath = Zotero.Attachments.getPath( + file, Zotero.Attachments.LINK_MODE_IMPORTED_URL, itemID + ); + attachmentItem.save(); Zotero.Notifier.trigger('add', 'item', itemID); @@ -300,8 +312,7 @@ Zotero.Attachments = new function(){ } catch (e) { // Clean up - var item = Zotero.Items.get(itemID); - item.erase(); + attachmentItem.erase(); throw (e); } @@ -387,8 +398,15 @@ Zotero.Attachments = new function(){ var mimeType = obj.channel.contentType; if (mimeType) { - var sql = "UPDATE itemAttachments SET mimeType=? WHERE itemID=?"; - Zotero.DB.query(sql, [mimeType, itemID]); + var disabled = Zotero.Notifier.disable(); + + var item = Zotero.Items.get(itemID); + item.attachmentMIMEType = mimeType; + item.save(); + + if (disabled) { + Zotero.Notifier.enable(); + } } Zotero.Notifier.trigger('add', 'item', itemID); @@ -466,11 +484,16 @@ Zotero.Attachments = new function(){ try { // Create a new attachment - var attachmentItem = new Zotero.Item('attachment'); + var attachmentItem = new Zotero.Item(false, 'attachment'); attachmentItem.setField('title', title); attachmentItem.setField('url', url); attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); + attachmentItem.setSource(sourceItemID); + attachmentItem.attachmentLinkMode = Zotero.Attachments.LINK_MODE_IMPORTED_URL; + attachmentItem.attachmentCharset = charsetID; + attachmentItem.attachmentMIMEType = mimeType; var itemID = attachmentItem.save(); + attachmentItem = Zotero.Items.get(itemID); // Create a new folder for this item in the storage directory var destDir = this.createDirectoryForItem(itemID); @@ -513,8 +536,9 @@ Zotero.Attachments = new function(){ wpdDOMSaver.init(file.path, document); wpdDOMSaver.saveHTMLDocument(); - _addToDB(file, url, title, Zotero.Attachments.LINK_MODE_IMPORTED_URL, - mimeType, charsetID, sourceItemID, itemID); + var path = this.getPath(file, Zotero.Attachments.LINK_MODE_IMPORTED_URL, itemID); + attachmentItem.attachmentPath = path; + attachmentItem.save(); } else { Zotero.debug('Saving with saveURI()'); @@ -529,8 +553,9 @@ Zotero.Attachments = new function(){ var nsIURL = ioService.newURI(url, null, null); wbp.progressListener = new Zotero.WebProgressFinishListener(function () { try { - _addToDB(file, url, title, Zotero.Attachments.LINK_MODE_IMPORTED_URL, - mimeType, charsetID, sourceItemID, itemID); + var path = this.getPath(file, Zotero.Attachments.LINK_MODE_IMPORTED_URL, itemID); + attachmentItem.attachmentPath = path; + attachmentItem.save(); Zotero.Notifier.trigger('add', 'item', itemID); @@ -640,7 +665,7 @@ Zotero.Attachments = new function(){ try { // Create a new attachment - var attachmentItem = new Zotero.Item('attachment'); + var attachmentItem = new Zotero.Item(false, 'attachment'); attachmentItem.setField('title', title); attachmentItem.setField('url', url); attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); @@ -879,10 +904,32 @@ Zotero.Attachments = new function(){ /* * Gets a relative descriptor for imported attachments and a persistent * descriptor for files outside the storage directory + * + * @param int missingItemID Item id to use if file is missing to + * generate suitable path */ - function getPath(file, linkMode) { - if (!file.exists()) { - throw ('Zotero.Attachments.getPath() cannot be called on non-existent file'); + function getPath(file, linkMode, missingItemID) { + var exists = file.exists(); + // TODO: can we get the itemID from the path? + if (!missingItemID && !exists) { + throw ('Zotero.Attachments.getPath() cannot be called on non-existent file without missingItemID'); + } + + // If imported file doesn't exist, create one temporarily so we can get + // the relative path (which doesn't work on non-existent files) + if (!exists && (linkMode == self.LINK_MODE_IMPORTED_URL || + linkMode == self.LINK_MODE_IMPORTED_FILE)) { + var missingFile = self.createDirectoryForItem(missingItemID); + missingFile.append(file.leafName); + missingFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644); + + var descriptor = Zotero.Attachments.getPath(missingFile, linkMode); + + var parentDir = missingFile.parent; + missingFile.remove(null); + parentDir.remove(null); + + return descriptor; } file.QueryInterface(Components.interfaces.nsILocalFile); @@ -943,85 +990,34 @@ Zotero.Attachments = new function(){ /** * Create a new item of type 'attachment' and add to the itemAttachments table * - * Passing an itemID causes it to skip new item creation and use the specified - * item instead -- used when importing files (since we have to know - * the itemID before copying in a file and don't want to update the DB before - * the file is saved) - * * Returns the itemID of the new attachment **/ - function _addToDB(file, url, title, linkMode, mimeType, charsetID, sourceItemID, itemID){ + function _addToDB(file, url, title, linkMode, mimeType, charsetID, sourceItemID) { Zotero.DB.beginTransaction(); - if (sourceItemID){ - var sourceItem = Zotero.Items.get(sourceItemID); - if (!sourceItem){ - Zotero.DB.commitTransaction(); - throw ("Cannot set attachment source to invalid item " + sourceItemID); - } - if (sourceItem.isAttachment()){ - Zotero.DB.commitTransaction(); - throw ("Cannot set attachment source to another file (" + sourceItemID + ")"); - } - } - - // If an itemID is provided, use that - if (itemID){ - var attachmentItem = Zotero.Items.get(itemID); - if (!attachmentItem.isAttachment()){ - throw ("Item " + itemID + " is not a valid attachment in _addToDB()"); - } - } - // Otherwise create a new attachment - else { - var attachmentItem = new Zotero.Item('attachment'); - attachmentItem.setField('title', title); - if (linkMode==self.LINK_MODE_IMPORTED_URL - || linkMode==self.LINK_MODE_LINKED_URL){ - attachmentItem.setField('url', url); - attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); - } - attachmentItem.save(); + var attachmentItem = new Zotero.Item(false, 'attachment'); + attachmentItem.setField('title', title); + if (linkMode == self.LINK_MODE_IMPORTED_URL + || linkMode == self.LINK_MODE_LINKED_URL) { + attachmentItem.setField('url', url); + attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP"); } + // Get path if (file) { - if (file.exists()) { - var path = getPath(file, linkMode); - } - // If file doesn't exist, create one temporarily so we can get the - // relative path (since getPath() doesn't work on non-existent files) - else if (linkMode == self.LINK_MODE_IMPORTED_URL || - linkMode == self.LINK_MODE_IMPORTED_FILE) { - var missingFile = self.createDirectoryForItem(attachmentItem.getID()); - missingFile.append(file.leafName); - missingFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644); - var path = getPath(missingFile, linkMode); - var parentDir = missingFile.parent; - missingFile.remove(null); - parentDir.remove(null); - } + var path = Zotero.Attachments.getPath(file, linkMode, attachmentItem.id); + attachmentItem.attachmentPath = path; } - var sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, " - + "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)"; - var bindParams = [ - attachmentItem.getID(), - sourceItemID ? {int:sourceItemID} : null, - {int:linkMode}, - mimeType ? {string:mimeType} : null, - charsetID ? {int:charsetID} : null, - path ? {string:path} : null - ]; - Zotero.DB.query(sql, bindParams); - - if (sourceItemID){ - sourceItem.incrementAttachmentCount(); - Zotero.Notifier.trigger('modify', 'item', sourceItemID); - } + attachmentItem.setSource(sourceItemID); + attachmentItem.attachmentLinkMode = linkMode; + attachmentItem.attachmentMIMEType = mimeType; + attachmentItem.attachmentCharset = charsetID; + attachmentItem.save(); Zotero.DB.commitTransaction(); - return attachmentItem.getID(); + return attachmentItem.id; } @@ -1050,10 +1046,15 @@ Zotero.Attachments = new function(){ Zotero.File.addCharsetListener(browser, new function(){ return function(charset, id){ var charsetID = Zotero.CharacterSets.getID(charset); - if (charsetID){ - var sql = "UPDATE itemAttachments SET charsetID=" + charsetID - + " WHERE itemID=" + itemID; - Zotero.DB.query(sql); + + var disabled = Zotero.Notifier.disable(); + + var item = Zotero.Items.get(itemID); + item.attachmentCharset = charsetID; + item.save(); + + if (disabled) { + Zotero.Notifier.enable(); } // Chain fulltext indexer inside the charset callback, diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js @@ -1822,7 +1822,7 @@ Zotero.CSL.Item = function(item) { // don't return URL or accessed information for journal articles if a // pages field exists - var itemType = Zotero.ItemTypes.getName(this.zoteroItem.getType()); + var itemType = Zotero.ItemTypes.getName(this.zoteroItem.itemTypeID); if(!Zotero.Prefs.get("export.citePaperJournalArticleURL") && ["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 && this.zoteroItem.getField("pages")) { @@ -2140,7 +2140,7 @@ Zotero.CSL.Item._fallbackTypeMap = { * Determines whether this item is of a given type */ Zotero.CSL.Item.prototype.isType = function(type) { - var zoteroType = Zotero.ItemTypes.getName(this.zoteroItem.getType()); + var zoteroType = Zotero.ItemTypes.getName(this.zoteroItem.itemTypeID); return (Zotero.CSL.Item._optionalTypeMap[zoteroType] && Zotero.CSL.Item._optionalTypeMap[zoteroType] == type) @@ -2153,7 +2153,7 @@ Zotero.CSL.Item.prototype.isType = function(type) { Zotero.CSL.Item.prototype._separateNames = function() { this._names = []; - var authorID = Zotero.CreatorTypes.getPrimaryIDForType(this.zoteroItem.getType()); + var authorID = Zotero.CreatorTypes.getPrimaryIDForType(this.zoteroItem.itemTypeID); var creators = this.zoteroItem.getCreators(); for each(var creator in creators) { diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js @@ -125,7 +125,7 @@ Zotero.CollectionTreeView.prototype.reload = function() for (var i=0; i<this.rowCount; i++) { if (this.isContainer(i) && this.isContainerOpen(i)) { - openCollections.push(this._getItemAtRow(i).ref.getID()); + openCollections.push(this._getItemAtRow(i).ref.id); } } @@ -152,6 +152,11 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids) return; } + if (!this._collectionRowMap) { + Zotero.debug("Collection row map didn't exist in collectionTreeView.notify()"); + return; + } + this.selection.selectEventsSuppressed = true; var savedSelection = this.saveSelection(); @@ -225,7 +230,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids) { case 'collection': var collection = Zotero.Collections.get(ids); - var collectionID = collection.getID(); + var collectionID = collection.id; // Open container if creating subcollection var parentID = collection.getParent(); if (parentID) { @@ -379,7 +384,7 @@ Zotero.CollectionTreeView.prototype.toggleOpenState = function(row) } else { - var newRows = Zotero.getCollections(this._getItemAtRow(row).ref.getID()); //Get children + var newRows = Zotero.getCollections(this._getItemAtRow(row).ref.id); //Get children for(var i = 0; i < newRows.length; i++) { @@ -516,7 +521,7 @@ Zotero.CollectionTreeView.prototype.saveSelection = function() return 'L'; } else if (this._getItemAtRow(i).isCollection()) { - return 'C' + this._getItemAtRow(i).ref.getID(); + return 'C' + this._getItemAtRow(i).ref.id; } else if (this._getItemAtRow(i).isSearch()) { return 'S' + this._getItemAtRow(i).ref.id; @@ -568,7 +573,7 @@ Zotero.CollectionTreeView.prototype._refreshHashMap = function() this._searchRowMap = []; for(var i=0; i < this.rowCount; i++){ if (this.isCollection(i)){ - this._collectionRowMap[this._getItemAtRow(i).ref.getID()] = i; + this._collectionRowMap[this._getItemAtRow(i).ref.id] = i; } else if (this.isSearch(i)){ this._searchRowMap[this._getItemAtRow(i).ref.id] = i; @@ -680,8 +685,8 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient) return true; } else if (dataType == 'zotero/collection' - && data.data != rowCollection.getID() - && !Zotero.Collections.get(data.data).hasDescendent('collection', rowCollection.getID())) { + && data.data != rowCollection.id + && !Zotero.Collections.get(data.data).hasDescendent('collection', rowCollection.id)) { return true;//collections cannot be dropped on themselves, nor in their children } } @@ -705,9 +710,10 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient) { var targetCollectionID; if(this._getItemAtRow(row).isCollection()) - targetCollectionID = this._getItemAtRow(row).ref.getID(); + targetCollectionID = this._getItemAtRow(row).ref.id; var droppedCollection = Zotero.Collections.get(data.data); - droppedCollection.changeParent(targetCollectionID); + droppedCollection.parent = targetCollectionID; + droppedCollection.save(); } else if (dataType == 'zotero/item') { var ids = data.data.split(','); @@ -730,7 +736,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient) } else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') { if (this._getItemAtRow(row).isCollection()) { - var parentCollectionID = this._getItemAtRow(row).ref.getID(); + var parentCollectionID = this._getItemAtRow(row).ref.id; } else { var parentCollectionID = false; @@ -804,7 +810,7 @@ Zotero.CollectionTreeView.prototype.onDragStart = function(evt,transferData,acti transferData.data=new TransferData(); //attach ID - transferData.data.addDataForFlavour("zotero/collection",this._getItemAtRow(this.selection.currentIndex).ref.getID()); + transferData.data.addDataForFlavour("zotero/collection",this._getItemAtRow(this.selection.currentIndex).ref.id); } /* @@ -881,14 +887,18 @@ Zotero.ItemGroup.prototype.isSearch = function() Zotero.ItemGroup.prototype.getName = function() { - if(this.isCollection()) - return this.ref.getName(); - else if(this.isLibrary()) + if (this.isCollection()) { + return this.ref.name; + } + else if (this.isLibrary()) { return Zotero.getString('pane.collections.library'); - else if(this.isSearch()) - return this.ref['name']; - else + } + else if (this.isSearch()) { + return this.ref.name; + } + else { return ""; + } } Zotero.ItemGroup.prototype.getChildItems = function() @@ -927,7 +937,7 @@ Zotero.ItemGroup.prototype.getSearchObject = function() { } else if (this.isCollection()) { s.addCondition('noChildren', 'true'); - s.addCondition('collectionID', 'is', this.ref.getID()); + s.addCondition('collectionID', 'is', this.ref.id); if (Zotero.Prefs.get('recursiveCollections')) { s.addCondition('recursive', 'true'); } diff --git a/chrome/content/zotero/xpcom/data/cachedTypes.js b/chrome/content/zotero/xpcom/data/cachedTypes.js @@ -0,0 +1,273 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +/* + * Base function for retrieving ids and names of static types stored in the DB + * (e.g. creatorType, fileType, charset, itemType) + * + * Extend using the following code within a child constructor: + * + * Zotero.CachedTypes.apply(this, arguments); + * this.constructor.prototype = new Zotero.CachedTypes(); + * + * And the following properties: + * + * this._typeDesc = 'c'; + * this._idCol = ''; + * this._nameCol = ''; + * this._table = ''; + * this._ignoreCase = false; + * + */ +Zotero.CachedTypes = function() { + var _types = []; + var _typesLoaded; + var self = this; + + // Override these variables in child classes + this._typeDesc = ''; + this._idCol = ''; + this._nameCol = ''; + this._table = ''; + this._ignoreCase = false; + + this.getName = getName; + this.getID = getID; + this.getTypes = getTypes; + + function getName(idOrName) { + if (!_typesLoaded) { + _load(); + } + + if (this._ignoreCase) { + idOrName = idOrName + ''; + idOrName = idOrName.toLowerCase(); + } + + if (!_types['_' + idOrName]) { + Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1); + return ''; + } + + return _types['_' + idOrName]['name']; + } + + + function getID(idOrName) { + if (!_typesLoaded) { + _load(); + } + + if (this._ignoreCase) { + idOrName = idOrName + ''; + idOrName = idOrName.toLowerCase(); + } + + if (!_types['_' + idOrName]) { + Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1); + return false; + } + + return _types['_' + idOrName]['id']; + } + + + function getTypes(where) { + return Zotero.DB.query('SELECT ' + this._idCol + ' AS id, ' + + this._nameCol + ' AS name FROM ' + this._table + + (where ? ' ' + where : '') + ' ORDER BY ' + this._nameCol); + } + + + function _load() { + var types = self.getTypes(); + + for (var i in types) { + // Store as both id and name for access by either + var typeData = { + id: types[i]['id'], + name: types[i]['name'] + } + _types['_' + types[i]['id']] = typeData; + if (self._ignoreCase) { + _types['_' + types[i]['name'].toLowerCase()] = _types['_' + types[i]['id']]; + } + else { + _types['_' + types[i]['name']] = _types['_' + types[i]['id']]; + } + } + + _typesLoaded = true; + } +} + + +Zotero.CreatorTypes = new function() { + Zotero.CachedTypes.apply(this, arguments); + this.constructor.prototype = new Zotero.CachedTypes(); + + this.getTypesForItemType = getTypesForItemType; + this.isValidForItemType = isValidForItemType; + this.getPrimaryIDForType = getPrimaryIDForType; + + this._typeDesc = 'creator type'; + this._idCol = 'creatorTypeID'; + this._nameCol = 'creatorType'; + this._table = 'creatorTypes'; + + function getTypesForItemType(itemTypeID) { + var sql = "SELECT creatorTypeID AS id, creatorType AS name " + + "FROM itemTypeCreatorTypes NATURAL JOIN creatorTypes " + // DEBUG: sort needs to be on localized strings in itemPane.js + // (though still put primary field at top) + + "WHERE itemTypeID=? ORDER BY primaryField=1 DESC, name"; + return Zotero.DB.query(sql, itemTypeID); + } + + + function isValidForItemType(creatorTypeID, itemTypeID) { + var sql = "SELECT COUNT(*) FROM itemTypeCreatorTypes " + + "WHERE itemTypeID=? AND creatorTypeID=?"; + return !!Zotero.DB.valueQuery(sql, [itemTypeID, creatorTypeID]); + } + + + function getPrimaryIDForType(itemTypeID) { + var sql = "SELECT creatorTypeID FROM itemTypeCreatorTypes " + + "WHERE itemTypeID=? AND primaryField=1"; + return Zotero.DB.valueQuery(sql, itemTypeID); + } +} + + +Zotero.ItemTypes = new function() { + Zotero.CachedTypes.apply(this, arguments); + this.constructor.prototype = new Zotero.CachedTypes(); + + this.getPrimaryTypes = getPrimaryTypes; + this.getSecondaryTypes = getSecondaryTypes; + this.getHiddenTypes = getHiddenTypes; + this.getLocalizedString = getLocalizedString; + this.getImageSrc = getImageSrc; + + this._typeDesc = 'item type'; + this._idCol = 'itemTypeID'; + this._nameCol = 'typeName'; + this._table = 'itemTypes'; + + function getPrimaryTypes() { + return this.getTypes('WHERE display=2'); + } + + function getSecondaryTypes() { + return this.getTypes('WHERE display=1'); + } + + function getHiddenTypes() { + return this.getTypes('WHERE display=0'); + } + + function getLocalizedString(typeIDOrName) { + var typeName = this.getName(typeIDOrName); + return Zotero.getString("itemTypes." + typeName); + } + + function getImageSrc(itemType) { + // DEBUG: only have icons for some types so far + switch (itemType) { + case 'attachment-file': + case 'attachment-link': + case 'attachment-snapshot': + case 'attachment-web-link': + case 'attachment-pdf': + case 'artwork': + case 'audioRecording': + case 'blogPost': + case 'book': + case 'bookSection': + case 'computerProgram': + case 'conferencePaper': + case 'email': + case 'film': + case 'forumPost': + case 'interview': + case 'journalArticle': + case 'letter': + case 'magazineArticle': + case 'manuscript': + case 'map': + case 'newspaperArticle': + case 'note': + case 'podcast': + case 'radioBroadcast': + case 'report': + case 'thesis': + case 'tvBroadcast': + case 'videoRecording': + case 'webpage': + return "chrome://zotero/skin/treeitem-" + itemType + ".png"; + } + + return "chrome://zotero/skin/treeitem.png"; + } +} + + +Zotero.FileTypes = new function() { + Zotero.CachedTypes.apply(this, arguments); + this.constructor.prototype = new Zotero.CachedTypes(); + + this._typeDesc = 'file type'; + this._idCol = 'fileTypeID'; + this._nameCol = 'fileType'; + this._table = 'fileTypes'; + + this.getIDFromMIMEType = getIDFromMIMEType; + + function getIDFromMIMEType(mimeType) { + var sql = "SELECT fileTypeID FROM fileTypeMIMETypes " + + "WHERE ? LIKE mimeType || '%'"; + + return Zotero.DB.valueQuery(sql, [mimeType]); + } +} + + +Zotero.CharacterSets = new function() { + Zotero.CachedTypes.apply(this, arguments); + this.constructor.prototype = new Zotero.CachedTypes(); + + this._typeDesc = 'character set'; + this._idCol = 'charsetID'; + this._nameCol = 'charset'; + this._table = 'charsets'; + this._ignoreCase = true; + + this.getAll = getAll; + + function getAll() { + return this.getTypes(); + } +} + diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js @@ -0,0 +1,930 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +Zotero.Collection = function(collectionID) { + this._collectionID = collectionID ? collectionID : null; + this._init(); +} + +Zotero.Collection.prototype._init = function (collectionID) { + // Public members for access by public methods -- do not access directly + this._name = null; + this._parent = null; + this._dateModified = null; + this._key = null; + + this._hasChildCollections = false; + this._childCollections = []; + this._childCollectionsLoaded = false; + + this._hasChildItems = false; + this._childItems = []; + this._childItemsLoaded = false; + + this._previousData = false; +} + + +Zotero.Collection.prototype.__defineGetter__('id', function () { return this._collectionID; }); + +Zotero.Collection.prototype.__defineSetter__('collectionID', function (val) { this._set('collectionID', val); }); +Zotero.Collection.prototype.__defineGetter__('name', function () { return this._get('name'); }); +Zotero.Collection.prototype.__defineSetter__('name', function (val) { this._set('name', val); }); +Zotero.Collection.prototype.__defineGetter__('parent', function () { return this._get('parent'); }); +Zotero.Collection.prototype.__defineSetter__('parent', function (val) { this._set('parent', val); }); +Zotero.Collection.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); }); +Zotero.Collection.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); }); +Zotero.Collection.prototype.__defineGetter__('key', function () { return this._get('key'); }); +Zotero.Collection.prototype.__defineSetter__('key', function (val) { this._set('key', val); }); + +Zotero.Collection.prototype.__defineSetter__('childCollections', function (arr) { this._setChildCollections(arr); }); +Zotero.Collection.prototype.__defineSetter__('childItems', function (arr) { this._setChildItems(arr); }); + + +Zotero.Collection.prototype._get = function (field) { + if (this.id && !this._loaded) { + this.load(); + } + return this['_' + field]; +} + + +Zotero.Collection.prototype._set = function (field, val) { + switch (field) { + case 'id': // set using constructor + //case 'collectionID': // set using constructor + throw ("Invalid field '" + field + "' in Zotero.Collection.set()"); + } + + if (this.id) { + if (!this._loaded) { + this.load(); + } + } + else { + this._loaded = true; + } + + if (this['_' + field] != val) { + this._prepFieldChange(field); + + switch (field) { + default: + this['_' + field] = val; + } + } +} + +Zotero.Collection.prototype.getID = function() { + Zotero.debug('Collection.getID() deprecated -- use Collection.id'); + return this.id; +} + +Zotero.Collection.prototype.getName = function() { + Zotero.debug('Collection.getName() deprecated -- use Collection.name'); + return this.name; +} + +Zotero.Collection.prototype.getParent = function() { + Zotero.debug('Collection.getParent() deprecated -- use Collection.parent'); + return this.parent; +} + + +/* + * Build collection from database + */ +Zotero.Collection.prototype.load = function() { + // Should be same as query in Zotero.Collections, just with collectionID + var sql = "SELECT C.*, " + + "(SELECT COUNT(*) FROM collections WHERE " + + "parentCollectionID=C.collectionID)!=0 AS hasChildCollections, " + + "(SELECT COUNT(*) FROM collectionItems WHERE " + + "collectionID=C.collectionID)!=0 AS hasChildItems " + + "FROM collections C WHERE collectionID=?"; + + var data = Zotero.DB.rowQuery(sql, this.id); + + this._init(); + this._loaded = true; + + if (!data) { + return; + } + + this.loadFromRow(data); +} + + +/* + * Populate collection data from a database row + */ +Zotero.Collection.prototype.loadFromRow = function(row) { + this._loaded = true; + this._changed = false; + this._previousData = false; + + this._collectionID = row.collectionID; + this._name = row.collectionName; + this._parent = row.parentCollectionID; + this._dateModified = row.dateModified; + this._key = row.key; + this._hasChildCollections = row.hasChildCollections; + this._hasChildItems = row.hasChildItems; + this._loadChildItems(); +} + + +Zotero.Collection.prototype.isEmpty = function() { + return !(parseInt(this._hasChildCollections)) && !(parseInt(this._hasChildItems)); +} + +Zotero.Collection.prototype.hasChildCollections = function() { + return !!(parseInt(this._hasChildCollections)); +} + +Zotero.Collection.prototype.hasChildItems = function() { + return !!(parseInt(this._hasChildItems)); +} + + +/** + * Check if collection exists in the database + * + * @return bool TRUE if the collection exists, FALSE if not + */ +Zotero.Collection.prototype.exists = function() { + if (!this.id) { + throw ('collectionID not set in Zotero.Collection.exists()'); + } + + var sql = "SELECT COUNT(*) FROM collections WHERE collectionID=?"; + return !!Zotero.DB.valueQuery(sql, this.id); +} + + +/** + * Returns subcollections of this collection + * + * @param bool asIDs Return as collectionIDs + * @return array Array of Zotero.Collection instances + * or collectionIDs, or FALSE if none + */ +Zotero.Collection.prototype.getChildCollections = function (asIDs) { + if (!this._childCollectionsLoaded) { + this._loadChildCollections(); + } + + if (this._childCollections.length == 0) { + return false; + } + + // Return collectionIDs + if (asIDs) { + var ids = []; + for each(var col in this._childCollections) { + ids.push(col.id); + } + return ids; + } + + // Return Zotero.Collection objects + var objs = []; + for each(var col in this._childCollections) { + objs.push(col); + } + return objs; +} + + +/** + * Returns child items of this collection + * + * @param bool asIDs Return as itemIDs + * @return array Array of Zotero.Item instances or itemIDs, + * or FALSE if none + */ +Zotero.Collection.prototype.getChildItems = function (asIDs) { + if (!this._childItemsLoaded) { + this._loadChildItems(); + } + + if (this._childItems.length == 0) { + return false; + } + + // Return itemIDs + if (asIDs) { + var ids = []; + for each(var item in this._childItems) { + ids.push(item.id); + } + return ids; + } + + // Return Zotero.Item objects + var objs = []; + for each(var item in this._childItems) { + objs.push(item); + } + return objs; +} + + +Zotero.Collection.prototype.save = function () { + if (!this.name) { + throw ('Collection name is empty in Zotero.Collection.save()'); + } + + if (!this._changed) { + Zotero.debug("Collection " + this.id + " has not changed"); + return false; + } + + if (this._changed.parent && this.parent) { + if (!Zotero.Collections.get(this.parent)) { + throw ('Cannot set parent of collection ' + this.id + + ' to invalid parent ' + this.parent); + } + + if (this.parent == this.id) { + throw ('Cannot move collection into itself!'); + } + + if (this.hasDescendent('collection', this.parent)) { + throw ('Cannot move collection into one of its own descendents!', 2); + } + } + + + Zotero.DB.beginTransaction(); + + // ID change + if (this._changed['collectionID']) { + var oldID = this._previousData.primary.collectionID; + var params = [this.id, oldID]; + + Zotero.debug("Changing collectionID " + oldID + " to " + this.id); + + var row = Zotero.DB.rowQuery("SELECT * FROM collections WHERE collectionID=?", oldID); + // Add a new row so we can update the old rows despite FK checks + // Use temp key due to UNIQUE constraint on key column + Zotero.DB.query("INSERT INTO collections VALUES (?, ?, ?, ?, ?)", + [this.id, row.collectionName, row.parentCollectionID, + row.dateModified, 'TEMPKEY']); + + Zotero.DB.query("UPDATE collectionItems SET collectionID=? WHERE collectionID=?", params); + Zotero.DB.query("UPDATE collections SET parentCollectionID=? WHERE parentCollectionID=?", params); + + Zotero.DB.query("DELETE FROM collections WHERE collectionID=?", oldID); + Zotero.DB.query("UPDATE collections SET key=? WHERE collectionID=?", [row.key, this.id]); + + Zotero.Collections.unload(oldID); + Zotero.Notifier.trigger('id-change', 'collection', oldID + '-' + this.id); + + // update caches + } + + var isNew = !this.id || !this.exists(); + + try { + // how to know if date modified changed (in server code too?) + + var collectionID = this.id ? this.id : Zotero.ID.get('collections'); + + Zotero.debug("Saving collection " + this.id); + + var key = this.key ? this.key : this._generateKey(); + + var columns = [ + 'collectionID', 'collectionName', 'parentCollectionID', + 'dateModified', 'key' + ]; + var placeholders = ['?', '?', '?', '?', '?']; + var sqlValues = [ + collectionID ? { int: collectionID } : null, + { string: this.name }, + this.parent ? { int: this.parent } : null, + // If date modified hasn't changed, use current timestamp + this._changed.dateModified ? + this.dateModified : Zotero.DB.transactionDateTime, + key + ]; + + var sql = "REPLACE INTO collections (" + columns.join(', ') + ") VALUES (" + + placeholders.join(', ') + ")"; + var insertID = Zotero.DB.query(sql, sqlValues); + if (!collectionID) { + collectionID = insertID; + } + + // Subcollections + if (this._changed.childCollections) { + var removed = []; + var newids = []; + var currentIDs = this.getChildCollections(true); + if (!currentIDs) { + currentIDs = []; + } + + if (this._previousData.childCollections) { + for each(var id in this._previousData.childCollections) { + if (currentIDs.indexOf(id) == -1) { + removed.push(id); + } + } + } + for each(var id in currentIDs) { + if (this._previousData.childCollections && + this._previousData.childCollections.indexOf(id) != -1) { + continue; + } + newids.push(id); + } + + if (removed.length) { + var sql = "UPDATE collections SET parentCollectionID=NULL " + + "WHERE collectionID IN (" + + removed.map(function () '?').join() + + ")"; + Zotero.DB.query(sql, removed); + } + + if (newids.length) { + var sql = "UPDATE collections SET parentCollectionID=? " + + "WHERE collectionID IN (" + + newids.map(function () '?').join() + + ")"; + Zotero.DB.query(sql, [collectionID].concat(newids)); + } + + // TODO: notifier + } + + // Child items + if (this._changed.childItems) { + var removed = []; + var newids = []; + var currentIDs = this.getChildItems(true); + if (!currentIDs) { + currentIDs = []; + } + + if (this._previousData.childItems) { + for each(var id in this._previousData.childItems) { + if (currentIDs.indexOf(id) == -1) { + removed.push(id); + } + } + } + for each(var id in currentIDs) { + if (this._previousData.childItems && + this._previousData.childItems.indexOf(id) != -1) { + continue; + } + newids.push(id); + } + + if (removed.length) { + var sql = "DELETE FROM collectionItems WHERE collectionID=? " + + "AND itemID IN (" + + removed.map(function () '?').join() + + ")"; + Zotero.DB.query(sql, [collectionID].concat(removed)); + } + + if (newids.length) { + var sql = "SELECT IFNULL(MAX(orderIndex)+1, 0) " + + "FROM collectionItems WHERE collectionID=?" + var orderStatement = Zotero.DB.getStatement(sql); + + var sql = "INSERT INTO collectionItems " + + "(collectionID, itemID, orderIndex) VALUES (?,?,?)"; + var insertStatement = Zotero.DB.getStatement(sql); + + for each(var itemID in newids) { + orderStatement.bindInt32Parameter(0, collectionID); + try { + if (orderStatement.executeStep()) { + var orderIndex = orderStatement.getInt32(0); + } + } + catch (e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + + orderStatement.reset(); + + insertStatement.bindInt32Parameter(0, collectionID); + insertStatement.bindInt32Parameter(1, itemID); + insertStatement.bindInt32Parameter(2, + orderIndex ? orderIndex : 0); + + try { + insertStatement.execute(); + } + catch (e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + } + } + + //Zotero.Notifier.trigger('add', 'collection-item', this.id + '-' + itemID); + } + + Zotero.DB.commitTransaction(); + } + catch (e) { + Zotero.DB.rollbackTransaction(); + throw (e); + } + + // If successful, set values in object + if (!this.id) { + this._collectionID = collectionID; + } + + if (!this.key) { + this._key = key; + } + + Zotero.Collections.reloadAll(); + + if (isNew) { + Zotero.Notifier.trigger('add', 'collection', this.id); + } + else { + Zotero.Notifier.trigger('modify', 'collection', this.id, this._previousData); + } + + if (this._changed.parent) { + var notifyIDs = [this.id]; + if (this._previousData.parent) { + notifyIDs.push(this._previousData.parent); + } + if (this.parent) { + notifyIDs.push(this.parent); + } + //Zotero.Notifier.trigger('move', 'collection', notifyIDs, notifierData); + } + + return this.id; +} + + +/** +* Add an item to the collection +**/ +Zotero.Collection.prototype.addItem = function(itemID) { + var current = this.getChildItems(true); + if (current && current.indexOf(itemID) != -1) { + Zotero.debug("Item " + itemID + " already a child of collection " + + this.id + " in Zotero.Collection.addItem()"); + return false; + } + + Zotero.DB.beginTransaction(); + + if (!Zotero.Items.get(itemID)) { + Zotero.DB.rollbackTransaction(); + throw(itemID + ' is not a valid item id'); + } + + var sql = "SELECT IFNULL(MAX(orderIndex)+1, 0) " + + "FROM collectionItems WHERE collectionID=?"; + var nextOrderIndex = Zotero.DB.valueQuery(sql, this.id); + + sql = "INSERT OR IGNORE INTO collectionItems VALUES (?,?,?)"; + Zotero.DB.query(sql, [this.id, itemID, nextOrderIndex]); + + sql = "UPDATE collections SET dateModified=? WHERE collectionID=?"; + Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]); + + Zotero.DB.commitTransaction(); + + Zotero.Collections.reload(this.id); + + Zotero.Notifier.trigger('add', 'collection-item', this.id + '-' + itemID); + + return true; +} + + +/** + * Add multiple items to the collection in batch + */ +Zotero.Collection.prototype.addItems = function(itemIDs) { + if (!itemIDs || !itemIDs.length) { + return; + } + + Zotero.DB.beginTransaction(); + for (var i=0; i<itemIDs.length; i++) { + this.addItem(itemIDs[i]); + } + Zotero.DB.commitTransaction(); +} + + +/** +* Remove an item from the collection (does not delete item from library) +**/ +Zotero.Collection.prototype.removeItem = function(itemID) { + var index = this.getChildItems(true).indexOf(itemID); + if (index == -1) { + Zotero.debug("Item " + itemID + " not a child of collection " + + this.id + " in Zotero.Collection.removeItem()"); + return false; + } + + Zotero.DB.beginTransaction(); + + var sql = "DELETE FROM collectionItems WHERE collectionID=? AND itemID=?"; + Zotero.DB.query(sql, [this.id, itemID]); + + sql = "UPDATE collections SET dateModified=? WHERE collectionID=?"; + Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]) + + Zotero.DB.commitTransaction(); + + Zotero.Collections.reload(this.id); + + Zotero.Notifier.trigger('remove', 'collection-item', this.id + '-' + itemID); + + return true; +} + + +/** + * Remove multiple items from the collection in batch + * (does not delete item from library) + */ +Zotero.Collection.prototype.removeItems = function(itemIDs) { + if (!itemIDs || !itemIDs.length) { + return; + } + + Zotero.DB.beginTransaction(); + for (var i=0; i<itemIDs.length; i++) { + this.removeItem(itemIDs[i]); + } + Zotero.DB.commitTransaction(); +} + + +/** +* Check if an item belongs to the collection +**/ +Zotero.Collection.prototype.hasItem = function(itemID) { + if (!this._childItemsLoaded) { + this._loadChildItems(); + } + + for each(var item in this._childItems) { + if (item.id == itemID) { + return true; + } + } + return false; +} + + +Zotero.Collection.prototype.hasDescendent = function(type, id) { + var descendents = this.getDescendents(); + for (var i=0, len=descendents.length; i<len; i++) { + if (descendents[i].type == type && descendents[i].id == id) { + return true; + } + } + return false; +} + + +/** +* Deletes collection and all descendent collections (and optionally items) +**/ +Zotero.Collection.prototype.erase = function(deleteItems) { + Zotero.DB.beginTransaction(); + + var descendents = this.getDescendents(); + var collections = [this.id]; + var items = []; + var notifierData = {}; + notifierData[this.id] = { old: this.serialize() }; + + for(var i=0, len=descendents.length; i<len; i++) { + // Descendent collections + if (descendents[i].type == 'collection') { + collections.push(descendents[i].id); + var c = Zotero.Collections.get(descendents[i].id); + if (c) { + notifierData[c.id] = { old: c.serialize() }; + } + } + // Descendent items + else { + if (deleteItems) { + // Delete items from DB + Zotero.Items.get(descendents[i].id).erase(); + } + } + } + + var placeholders = collections.map(function () '?').join(); + + // Remove item associations for all descendent collections + Zotero.DB.query('DELETE FROM collectionItems WHERE collectionID IN ' + + '(' + placeholders + ')', collections); + + // Remove parent definitions first for FK check + Zotero.DB.query('UPDATE collections SET parentCollectionID=NULL ' + + 'WHERE parentCollectionID IN (' + placeholders + ')', collections); + + // And delete all descendent collections + Zotero.DB.query('DELETE FROM collections WHERE collectionID IN ' + + '(' + placeholders + ')', collections); + + Zotero.DB.commitTransaction(); + + // Clear deleted collection from internal memory + Zotero.Collections.unload(collections); + + Zotero.Collections.reloadAll(); + + Zotero.Notifier.trigger('delete', 'collection', collections, notifierData); +} + + +Zotero.Collection.prototype.isCollection = function() { + return true; +} + + +Zotero.Collection.prototype.toArray = function() { + Zotero.debug('Collection.toArray() is deprecated -- use Collection.serialize()'); + return this.serialize(); +} + + +Zotero.Collection.prototype.serialize = function(nested) { + var obj = { + primary: { + collectionID: this.id, + dateModified: this.dateModified, + key: this.key + }, + name: this.name, + parent: this.parent, + childCollections: this.getChildCollections(true), + childItems: this.getChildItems(true), + descendents: this.getDescendents(nested) + }; + return obj; +} + + +/** + * Returns an array of descendent collections and items + * (rows of 'id', 'type' ('item' or 'collection'), 'parent', and, + * if collection, 'name' and the nesting 'level') + * + * @param bool recursive Descend into subcollections + * @param bool nested Return multidimensional array with 'children' + * nodes instead of flat array + * @param string type 'item', 'collection', or FALSE for both + */ +Zotero.Collection.prototype.getChildren = function(recursive, nested, type, level) { + var toReturn = []; + + if (!level) { + level = 1; + } + + // 0 == collection + // 1 == item + var children = Zotero.DB.query('SELECT collectionID AS id, ' + + "0 AS type, collectionName AS collectionName " + + 'FROM collections WHERE parentCollectionID=?1' + + ' UNION SELECT itemID AS id, 1 AS type, NULL AS collectionName ' + + 'FROM collectionItems WHERE collectionID=?1', this.id); + + if (type) { + switch (type) { + case 'item': + case 'collection': + break; + default: + throw ("Invalid type '" + type + "' in Collection.getChildren()"); + } + } + + for(var i=0, len=children.length; i<len; i++) { + // This seems to not work without parseInt() even though + // typeof children[i]['type'] == 'number' and + // children[i]['type'] === parseInt(children[i]['type']), + // which sure seems like a bug to me + switch (parseInt(children[i].type)) { + case 0: + if (!type || type=='collection') { + toReturn.push({ + id: children[i].id, + name: children[i].collectionName, + type: 'collection', + level: level, + parent: this.id + }); + } + + if (recursive) { + var descendents = + Zotero.Collections.get(children[i].id). + getChildren(true, nested, type, level+1); + + if (nested) { + toReturn[toReturn.length-1].children = descendents; + } + else { + for (var j=0, len2=descendents.length; j<len2; j++) { + toReturn.push(descendents[j]); + } + } + } + break; + + case 1: + if (!type || type=='item') { + toReturn.push({ + id: children[i].id, + type: 'item', + parent: this.id + }); + } + break; + } + } + + return toReturn; +} + + +/** + * Alias for the recursive mode of getChildren() + */ +Zotero.Collection.prototype.getDescendents = function(nested, type, level) { + return this.getChildren(true, nested, type); +} + + +Zotero.Collection.prototype._prepFieldChange = function (field) { + if (!this._changed) { + this._changed = {}; + } + this._changed[field] = true; + + // Save a copy of the data before changing + // TODO: only save previous data if collection exists + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } +} + + +Zotero.Collection.prototype._setChildCollections = function (collectionIDs) { + this._setChildren('collection', collectionIDs); +} + + +Zotero.Collection.prototype._setChildItems = function (itemIDs) { + this._setChildren('item', itemIDs); +} + + +Zotero.Collection.prototype._setChildren = function (type, ids) { + if (type != 'collection' && type != 'item') { + throw ("Invalid type '" + type + "' in Zotero.Collection._setChildren()"); + } + + var Type = type.charAt(0).toUpperCase() + type.substr(1); + var Types = Type + 's'; // 'Items' + var types = type + 's'; // 'items' + + if (!this['_child' + Types + 'Loaded']) { + this['_loadChild' + Types](); + } + + if (ids.constructor.name != 'Array') { + throw (type + 'IDs must be an array in Zotero.Collection._setChildren()'); + } + + var currentIDs = this['getChild' + Types](true); + if (!currentIDs) { + currentIDs = []; + } + var oldIDs = []; // children being kept + var newIDs = []; // new children + + if (ids.length == 0) { + if (this['_child' + Types].length == 0) { + Zotero.debug('No child ' + types + ' added', 4); + return false; + } + } + else { + for (var i in ids) { + var id = parseInt(ids[i]); + if (isNaN(id)) { + throw ("Invalid " + type + "ID '" + ids[i] + + "' in Zotero.Collection._setChildren()"); + } + + if (currentIDs.indexOf(id) != -1) { + Zotero.debug(Type + " " + ids[i] + + " is already a child of collection " + this.id); + oldIDs.push(id); + continue; + } + + newIDs.push(id); + } + } + + // Mark as changed if new or removed ids + if (newIDs.length > 0 || oldIDs.length != this['_child' + Types].length) { + this._prepFieldChange('child' + Types); + } + else { + Zotero.debug('Child ' + types + ' not changed', 4); + return false; + } + + newIDs = oldIDs.concat(newIDs); + + this['_child' + Types] = []; + // Items.get() can take an array + if (type == 'item') { + this._childItems = Zotero.Items.get(newIDs); + } + else { + for (var id in newIDs) { + this['_child' + Types].push(Zotero[Types].get(id)); + } + } + + return true; +} + + +Zotero.Collection.prototype._loadChildCollections = function () { + var sql = "SELECT collectionID FROM collections WHERE parentCollectionID=?"; + var ids = Zotero.DB.columnQuery(sql, this.id); + + this._childCollections = []; + + if (ids) { + for each(var id in ids) { + this._childCollections.push(Zotero.Collections.get(id)); + } + } + + this._childCollectionsLoaded = true; +} + +Zotero.Collection.prototype._loadChildItems = function() { + var sql = "SELECT itemID FROM collectionItems WHERE collectionID=?"; + var ids = Zotero.DB.columnQuery(sql, this.id); + + this._childItems = []; + + if (ids) { + for each(var id in ids) { + this._childItems.push(Zotero.Items.get(id)); + } + } + + this._childItemsLoaded = true; +} + + +Zotero.Collection.prototype._generateKey = function () { + return Zotero.ID.getKey(); +} diff --git a/chrome/content/zotero/xpcom/data/collections.js b/chrome/content/zotero/xpcom/data/collections.js @@ -0,0 +1,184 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +/* + * Primary interface for accessing Zotero collection + */ +Zotero.Collections = new function() { + var _collections = {}; + var _collectionsLoaded = false; + + this.get = get; + this.add = add; + this.getUpdated = getUpdated; + this.getCollectionsContainingItems = getCollectionsContainingItems; + this.reload = reload; + this.reloadAll = reloadAll; + this.erase = erase; + this.unload = unload; + + /* + * Returns a Zotero.Collection object for a collectionID + */ + function get(id) { + if (!_collectionsLoaded) { + this.reloadAll(); + } + return (typeof _collections[id]!='undefined') ? _collections[id] : false; + } + + + /** + * Add new collection to DB and return Collection object + * + * _name_ is non-empty string + * _parent_ is optional collectionID -- creates root collection by default + * + * Returns true on success; false on error + **/ + function add(name, parent) { + var col = new Zotero.Collection; + col.name = name; + col.parent = parent; + var id = col.save(); + return this.get(id); + } + + + function getUpdated(date) { + var sql = "SELECT collectionID FROM collections"; + if (date) { + sql += " WHERE dateModified>?"; + return Zotero.DB.columnQuery(sql, Zotero.Date.dateToSQL(date, true)); + } + return Zotero.DB.columnQuery(sql); + } + + + function getCollectionsContainingItems(itemIDs, asIDs) { + var sql = "SELECT collectionID FROM collections WHERE "; + var sqlParams = []; + for each(var id in itemIDs) { + sql += "collectionID IN (SELECT collectionID FROM collectionItems " + + "WHERE itemID=?) AND " + sqlParams.push(id); + } + sql = sql.substring(0, sql.length - 5); + var collectionIDs = Zotero.DB.columnQuery(sql, sqlParams); + + if (asIDs) { + return collectionIDs; + } + + return Zotero.Collections.get(collectionIDs); + } + + + function reload(id) { + if (!_collectionsLoaded) { + this.reloadAll(); + return; + } + + if (!_collections[id]) { + _collections[id] = new Zotero.Collection(id); + } + _collections[id].load(); + } + + + /** + * Loads collection data from DB and adds to internal cache + **/ + function reloadAll() { + Zotero.debug('Loading all collections'); + + // This should be the same as the query in Zotero.Collection.load(), + // just without a specific collectionID + var sql = "SELECT C.*, " + + "(SELECT COUNT(*) FROM collections WHERE " + + "parentCollectionID=C.collectionID)!=0 AS hasChildCollections, " + + "(SELECT COUNT(*) FROM collectionItems WHERE " + + "collectionID=C.collectionID)!=0 AS hasChildItems " + + "FROM collections C"; + var result = Zotero.DB.query(sql); + + var collectionIDs = []; + + if (result) { + for (var i=0; i<result.length; i++) { + var collectionID = result[i].collectionID; + collectionIDs.push(collectionID); + + // If collection doesn't exist, create new object and stuff in array + if (!_collections[collectionID]) { + _collections[collectionID] = new Zotero.Collection; + } + _collections[collectionID].loadFromRow(result[i]); + } + } + + // Remove old collections that no longer exist + for each(var c in _collections) { + if (collectionIDs.indexOf(c.id) == -1) { + this.unload(c.id); + } + } + + _collectionsLoaded = true; + } + + + function erase(ids) { + ids = Zotero.flattenArguments(ids); + + Zotero.DB.beginTransaction(); + for each(var id in ids) { + var collection = this.get(id); + if (collection) { + collection.erase(); + } + collection = undefined; + } + + this.unload(ids); + + Zotero.DB.commitTransaction(); + } + + + /** + * Clear collection from internal cache (used by Zotero.Collection.erase()) + * + * Can be passed ids as individual parameters or as an array of ids, or both + **/ + function unload() { + var ids = Zotero.flattenArguments(arguments); + + for(var i=0; i<ids.length; i++) { + delete _collections[ids[i]]; + } + } + +} + diff --git a/chrome/content/zotero/xpcom/data/creator.js b/chrome/content/zotero/xpcom/data/creator.js @@ -0,0 +1,451 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +Zotero.Creator = function (creatorID) { + this._creatorID = creatorID ? creatorID : null; + this._init(); +} + + +Zotero.Creator.prototype._init = function () { + this._firstName = null; + this._lastName = null; + this._fieldMode = null; + this._birthYear = null; + this._key = null; + this._dateModified = null; + + this._creatorDataID = null; + this._loaded = false; + this._changed = false; + this._previousData = false; +} + + +Zotero.Creator.prototype.__defineGetter__('id', function () { return this._creatorID; }); +Zotero.Creator.prototype.__defineGetter__('creatorDataID', function () { return this._get('creatorDataID'); }); + +Zotero.Creator.prototype.__defineSetter__('creatorID', function (val) { this._set('creatorID', val); }); +Zotero.Creator.prototype.__defineGetter__('firstName', function () { return this._get('firstName'); }); +Zotero.Creator.prototype.__defineSetter__('firstName', function (val) { this._set('firstName', val); }); +Zotero.Creator.prototype.__defineGetter__('lastName', function () { return this._get('lastName'); }); +Zotero.Creator.prototype.__defineSetter__('lastName', function (val) { this._set('lastName', val); }); +Zotero.Creator.prototype.__defineGetter__('fieldMode', function () { return this._get('fieldMode'); }); +Zotero.Creator.prototype.__defineSetter__('fieldMode', function (val) { this._set('fieldMode', val); }); +Zotero.Creator.prototype.__defineGetter__('birthYear', function () { return this._get('birthYear'); }); +Zotero.Creator.prototype.__defineSetter__('birthYear', function (val) { this._set('birthYear', val); }); +Zotero.Creator.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); }); +Zotero.Creator.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); }); +Zotero.Creator.prototype.__defineGetter__('key', function () { return this._get('key'); }); +Zotero.Creator.prototype.__defineSetter__('key', function (val) { this._set('key', val); }); + +// Block properties that can't be set this way +Zotero.Creator.prototype.__defineSetter__('id', function () { this._set('id', val); }); +Zotero.Creator.prototype.__defineSetter__('creatorDataID', function () { this._set('creatorDataID', val); }); + + +Zotero.Creator.prototype._get = function (field) { + if (this.id && !this._loaded) { + this.load(); + } + return this['_' + field]; +} + + +Zotero.Creator.prototype._set = function (field, val) { + switch (field) { + case 'id': // set using constructor + //case 'creatorID': // set using constructor + case 'creatorDataID': + throw ("Invalid field '" + field + "' in Zotero.Creator.set()"); + } + + if (this.id) { + if (!this._loaded) { + this.load(); + } + } + else { + this._loaded = true; + } + + this._checkValue(field, val); + + if (this['_' + field] != val) { + if (!this._changed) { + this._changed = {}; + } + this._changed[field] = true; + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } + + this['_' + field] = val; + } +} + + +Zotero.Creator.prototype.setFields = function(fields) { + for (var field in fields) { + this[field] = fields[field]; + } +} + + +/** + * Check if creator exists in the database + * + * @return bool TRUE if the creator exists, FALSE if not + */ +Zotero.Creator.prototype.exists = function() { + if (!this.id) { + throw ('creatorID not set in Zotero.Creator.exists()'); + } + + var sql = "SELECT COUNT(*) FROM creators WHERE creatorID=?"; + return !!Zotero.DB.valueQuery(sql, this.id); +} + + +Zotero.Creator.prototype.hasChanged = function () { + return this._changed; +} + + +Zotero.Creator.prototype.save = function () { + if (!this.firstName && !this.lastName) { + throw ('First and last name are empty in Zotero.Creator.save()'); + } + + if (!this.hasChanged()) { + Zotero.debug("Creator " + this.id + " has not changed"); + return false; + } + + if (this.fieldMode == 1 && this.firstName) { + throw ("First name ('" + this.firstName + "') must be empty in single-field mode in Zotero.Creator.save()"); + } + + Zotero.DB.beginTransaction(); + + // ID change + if (this._changed['creatorID']) { + var oldID = this._previousData.primary.creatorID; + var params = [this.id, oldID]; + + Zotero.debug("Changing creatorID " + oldID + " to " + this.id); + + var row = Zotero.DB.rowQuery("SELECT * FROM creators WHERE creatorID=?", oldID); + // Add a new row so we can update the old rows despite FK checks + // Use temp key due to UNIQUE constraint on key column + Zotero.DB.query("INSERT INTO creators VALUES (?, ?, ?, ?)", + [this.id, row.creatorDataID, row.dateModified, 'TEMPKEY']); + + Zotero.DB.query("UPDATE itemCreators SET creatorID=? WHERE creatorID=?", params); + + Zotero.DB.query("DELETE FROM creators WHERE creatorID=?", oldID); + Zotero.DB.query("UPDATE creators SET key=? WHERE creatorID=?", [row.key, this.id]); + + Zotero.Creators.unload(oldID); + Zotero.Notifier.trigger('id-change', 'creator', oldID + '-' + this.id); + + // update caches + } + + var isNew = !this.id || !this.exists(); + + try { + // how to know if date modified changed (in server code too?) + + var creatorID = this.id ? this.id : Zotero.ID.get('creators'); + + Zotero.debug("Saving creator " + this.id); + + var key = this.key ? this.key : this._generateKey(); + + // If this was the only creator with the previous data, + // see if we can reuse or remove the old data row + if (this.creatorDataID) { + var count = Zotero.Creators.countCreatorsWithData(this.creatorDataID); + if (count == 1) { + var newCreatorDataID = Zotero.Creators.getDataID(this); + // Data hasn't changed + if (this.creatorDataID == newCreatorDataID) { + var creatorDataID = this.creatorDataID; + } + // Existing data row with the new data -- switch to that + // and delete old row + else if (newCreatorDataID) { + var creatorDataID = newCreatorDataID; + Zotero.Creators.deleteData(this.creatorDataID); + } + // Update current data row with new data + else { + Zotero.Creators.updateData(this.creatorDataID, this); + var creatorDataID = this.creatorDataID; + } + } + } + + if (!creatorDataID) { + var creatorDataID = Zotero.Creators.getDataID(this, true); + } + + var columns = ['creatorID', 'creatorDataID', 'dateModified', 'key']; + var placeholders = ['?', '?', '?', '?']; + var sqlValues = [ + creatorID ? { int: creatorID } : null, + { int: creatorDataID }, + // If date modified hasn't changed, use current timestamp + this._changed.dateModified ? + this.dateModified : Zotero.DB.transactionDateTime, + key + ]; + + var sql = "REPLACE INTO creators (" + columns.join(', ') + ") VALUES (" + + placeholders.join(', ') + ")"; + var insertID = Zotero.DB.query(sql, sqlValues); + if (!creatorID) { + creatorID = insertID; + } + + this.updateLinkedItems(creatorID); + + Zotero.DB.commitTransaction(); + } + catch (e) { + Zotero.DB.rollbackTransaction(); + throw (e); + } + + // If successful, set values in object + if (!this.id) { + this._creatorID = creatorID; + } + if (!this.key) { + this._key = key; + } + if (!this.creatorDataID) { + this._creatorDataID = creatorDataID; + } + + Zotero.Creators.reload(this.id); + + if (isNew) { + Zotero.Notifier.trigger('add', 'creator', this.id); + } + else { + Zotero.Notifier.trigger('modify', 'creator', this.id, this._previousData); + } + + return this.id; +} + + +Zotero.Creator.prototype.countLinkedItems = function() { + var sql = "SELECT COUNT(*) FROM itemCreators WHERE creatorID=?"; + return Zotero.DB.valueQuery(sql, this.id); +} + + +Zotero.Creator.prototype.getLinkedItems = function () { + var sql = "SELECT itemID FROM itemCreators WHERE creatorID=?"; + return Zotero.DB.columnQuery(sql, this.id); +} + + +Zotero.Creator.prototype.updateLinkedItems = function () { + Zotero.DB.beginTransaction(); + + var sql = "SELECT itemID FROM itemCreators WHERE creatorID=?"; + var changedItemIDs = Zotero.DB.columnQuery(sql, this.id); + + if (!changedItemIDs) { + Zotero.DB.commitTransaction(); + return; + } + + var notifierData = {}; + for each(var id in changedItemIDs) { + var item = Zotero.Items.get(id); + if (item) { + notifierData[item.id] = { old: item.serialize() }; + } + } + + sql = "UPDATE items SET dateModified=? WHERE itemID IN " + + "(SELECT itemID FROM itemCreators WHERE creatorID=?)"; + Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]); + + Zotero.Items.reload(changedItemIDs); + + Zotero.DB.commitTransaction(); + + Zotero.Notifier.trigger('modify', 'item', changedItemIDs, notifierData); +} + + +Zotero.Creator.prototype.equals = function (creator) { + return (creator.firstName == this.firstName) && + (creator.lastName == this.lastName) && + (creator.fieldMode == this.fieldMode) && + (creator.shortName == this.shortName) && + (creator.birthYear == this.birthYear); +} + + +Zotero.Creator.prototype.serialize = function () { + var obj = {}; + + obj.primary = {}; + obj.primary.creatorID = this.id; + obj.primary.dateModified = this.dateModified; + obj.primary.key = this.key; + + obj.fields = {}; + if (this.fieldMode == 1) { + obj.fields.name = this.lastName; + } + else { + obj.fields.firstName = this.firstName; + obj.fields.lastName = this.lastName; + } + obj.fields.fieldMode = this.fieldMode; + obj.fields.shortName = this.shortName; + obj.fields.birthYear = this.birthYear; + + return obj; +} + + +/** + * Remove creator from all linked items + * + * Creators.erase() should be used instead of this + * + * Actual deletion of creator occurs in Zotero.Creators.purge(), + * which is called by Creators.erase() + */ +Zotero.Creator.prototype.erase = function () { + if (!this.id) { + return false; + } + + Zotero.debug("Deleting creator " + this.id); + + var changedItems = []; + var changedItemsNotifierData = {}; + + Zotero.DB.beginTransaction(); + + var toSave = {}; + + var linkedItemIDs = this.getLinkedItems(); + for each(var itemID in linkedItemIDs) { + var item = Zotero.Items.get(itemID) + if (!item) { + throw ('Linked item not found in Zotero.Creator.erase()'); + } + + var pos = item.getCreatorPosition(this.id); + if (!pos) { + throw ('Creator not found in linked item in Zotero.Creator.erase()'); + } + + item.removeCreator(pos); + + if (!toSave[item.id]) { + toSave[item.id] = item; + } + } + + for each(var item in toSave) { + item.save(); + } + + Zotero.DB.commitTransaction(); +} + + +// Also called from Zotero.Creators.reload() +Zotero.Creator.prototype.load = function () { + Zotero.debug("Loading data for creator " + this.id + " in Zotero.Creator.load()"); + + if (!this.id) { + throw ("creatorID not set in Zotero.Creator.load()"); + } + + var sql = "SELECT key, dateModified, creatorDataID, CD.* " + + "FROM creators C NATURAL JOIN creatorData CD WHERE creatorID=?"; + var data = Zotero.DB.rowQuery(sql, this.id); + + this._init(); + this._loaded = true; + + if (!data) { + return; + } + + for (var key in data) { + this['_' + key] = data[key]; + } +} + + +Zotero.Creator.prototype._checkValue = function (field, value) { + if (this['_' + field] === undefined) { + throw ("Invalid property " + field + " in Zotero.Creator._checkValue()"); + } + + // Data validation + switch (field) { + case 'fieldMode': + if (value !== 0 && value !== 1) { + this._invalidValueError(field, value); + } + break; + + case 'key': + var re = /^[23456789ABCDEFGHIJKMNPQRSTUVWXTZ]{8}$/ + if (!re.test(value)) { + this._invalidValueError(field, value); + } + break; + + case 'dateModified': + if (value !== '' && !Zotero.Date.isSQLDateTime(value)) { + this._invalidValueError(field, value); + } + break; + } +} + + +Zotero.Creator.prototype._generateKey = function () { + return Zotero.ID.getKey(); +} + + +Zotero.Creator.prototype._invalidValueError = function (field, value) { + throw ("Invalid '" + field + "' value '" + value + "' in Zotero.Creator._invalidValueError()"); +} diff --git a/chrome/content/zotero/xpcom/data/creators.js b/chrome/content/zotero/xpcom/data/creators.js @@ -0,0 +1,346 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +Zotero.Creators = new function() { + var _creatorsByID = {}; // Zotero.Creator objects indexed by creatorID + var _creatorDataHash = {}; // creatorDataIDs indexed by md5 hash of data + + this.get = get; + this.getUpdated = getUpdated; + this.getDataID = getDataID; + this.getCreatorsWithData = getCreatorsWithData; + this.countCreatorsWithData = countCreatorsWithData; + this.updateData = updateData; + this.deleteData = deleteData; + this.reload = reload; + this.reloadAll = reloadAll; + this.erase = erase; + this.purge = purge; + this.unload = unload; + + this.fields = ['firstName', 'lastName', 'fieldMode', 'birthYear']; + + var self = this; + + /* + * Returns a Zotero.Creator object for a given creatorID + */ + function get(creatorID) { + if (_creatorsByID[creatorID]) { + return _creatorsByID[creatorID]; + } + + var sql = 'SELECT * FROM creators WHERE creatorID=?'; + var result = Zotero.DB.rowQuery(sql, creatorID); + + if (!result) { + return false; + } + + _creatorsByID[creatorID] = new Zotero.Creator(result.creatorID); + return _creatorsByID[creatorID]; + } + + + function getUpdated(date) { + var sql = "SELECT creatorID FROM creators"; + if (date) { + sql += " WHERE dateModified>?"; + return Zotero.DB.columnQuery(sql, Zotero.Date.dateToSQL(date, true)); + } + return Zotero.DB.columnQuery(sql); + } + + + /** + * Returns the creatorDataID matching given fields + * + * @param array fields + * @param bool create If no matching creatorDataID, create one + */ + function getDataID(fields, create) { + fields = _cleanFields(fields); + + if (!fields.firstName && !fields.lastName) { + throw ("First or last name must be provided in Zotero.Creators.getDataID()"); + } + + var hash = _getHash(fields); + if (_creatorDataHash[hash]) { + return _creatorDataHash[hash]; + } + + Zotero.DB.beginTransaction(); + + var params = [ + fields.firstName, + fields.lastName, + '', + fields.fieldMode, + fields.birthYear + ]; + + var sql = "SELECT creatorDataID FROM creatorData WHERE " + + "firstName=? AND lastName=? AND shortName=? " + + "AND fieldMode=? AND birthYear=?"; + var id = Zotero.DB.valueQuery(sql, params); + + if (!id && create) { + id = Zotero.ID.get('creatorData'); + params.unshift(id); + + sql = "INSERT INTO creatorData (creatorDataID, " + + "firstName, lastName, shortName, fieldMode, birthYear) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + var insertID = Zotero.DB.query(sql, params); + if (!id) { + id = insertID; + } + } + + Zotero.DB.commitTransaction(); + + if (id) { + _creatorDataHash[hash] = id; + } + + return id; + } + + + function getCreatorsWithData(creatorDataID) { + var sql = "SELECT creatorID FROM creators WHERE creatorDataID=?"; + return Zotero.DB.columnQuery(sql, creatorDataID); + } + + + function countCreatorsWithData(creatorDataID) { + var sql = "SELECT COUNT(*) FROM creators WHERE creatorDataID=?"; + return Zotero.DB.valueQuery(sql, creatorDataID); + } + + + function updateData(creatorDataID, fields) { + fields = _cleanFields(fields); + + var sqlFields = []; + var sqlParams = []; + for (var field in fields) { + // Skip fields not specified as changeable creator fields + if (this.fields.indexOf(field) == -1) { + continue; + } + sqlFields.push(field + '=?'); + sqlParams.push(fields[field]); + } + + var sql = "UPDATE creatorData SET " + sqlFields.join(', ') + + " WHERE creatorDataID=?"; + + sqlParams.push(creatorDataID); + Zotero.DB.query(sql, sqlParams); + + _updateCachedData(creatorDataID); + } + + + function deleteData(creatorDataID) { + var sql = "DELETE FROM creatorData WHERE creatorDataID=?"; + Zotero.DB.query(sql, creatorDataID); + _updateCachedData(creatorDataID); + } + + + /* + * Reloads data for specified creators into internal array + * + * Can be passed ids as individual parameters or as an array of ids, or both + */ + function reload() { + if (!arguments[0]) { + return false; + } + + var ids = Zotero.flattenArguments(arguments); + Zotero.debug('Reloading creators ' + ids); + + for each(var id in ids) { + if (!_creatorsByID[id]) { + this.get(id); + } + else { + _creatorsByID[id].load(); + } + } + + return true; + } + + + function reloadAll() { + Zotero.debug("Reloading all creators"); + _creatorDataHash = {}; + for (var id in _creatorsByID) { + _creatorsByID[id].load(); + var realID = _creatorsByID[id].id; + if (realID != id) { + Zotero.debug("Clearing cache entry for creator " + id); + delete _creatorsByID[id]; + } + } + } + + + /** + * Remove creator(s) from all linked items and call this.purge() + * to delete creator rows + */ + function erase(ids) { + ids = Zotero.flattenArguments(ids); + + var unlock = Zotero.Notifier.begin(true); + Zotero.UnresponsiveScriptIndicator.disable(); + try { + Zotero.DB.beginTransaction(); + for each(var id in ids) { + var creator = this.get(id); + if (!creator) { + Zotero.debug('Creator ' + id + ' does not exist in Creators.erase()!', 1); + Zotero.Notifier.trigger('delete', 'creator', id); + continue; + } + creator.erase(); + creator = undefined; + } + this.purge(); + Zotero.DB.commitTransaction(); + } + catch (e) { + Zotero.DB.rollbackTransaction(); + throw (e); + } + finally { + Zotero.Notifier.commit(unlock); + Zotero.UnresponsiveScriptIndicator.enable(); + } + } + + + /* + * Delete obsolete creator/creatorData rows from database + * and clear internal array entries + */ + function purge() { + Zotero.debug("Purging creator tables"); + + // Purge unused creators + var sql = 'SELECT creatorID FROM creators WHERE creatorID NOT IN ' + + '(SELECT creatorID FROM itemCreators)'; + var toDelete = Zotero.DB.columnQuery(sql); + + if (toDelete) { + // Clear creator entries in internal array + for each(var creatorID in toDelete) { + delete _creatorsByID[creatorID]; + } + + var sql = "DELETE FROM creators WHERE creatorID NOT IN " + + "(SELECT creatorID FROM itemCreators)"; + Zotero.DB.query(sql); + } + + // Purge unused creatorData rows + var sql = 'SELECT creatorDataID FROM creatorData WHERE creatorDataID NOT IN ' + + '(SELECT creatorDataID FROM creators)'; + var toDelete = Zotero.DB.columnQuery(sql); + + if (toDelete) { + // Clear creator entries in internal array + for each(var creatorDataID in toDelete) { + _updateCachedData(creatorDataID); + } + + var sql = "DELETE FROM creatorData WHERE creatorDataID NOT IN " + + "(SELECT creatorDataID FROM creators)"; + Zotero.DB.query(sql); + } + } + + + /** + * Clear creator from internal array + * + * @param int id creatorID + */ + function unload(id) { + delete _creatorsByID[id]; + } + + + function _cleanFields(fields) { + var cleanedFields = { + firstName: '', + lastName: '', + fieldMode: 0, + birthYear: '' + }; + for (var field in fields) { + if (fields[field]) { + cleanedFields[field] = fields[field]; + } + } + return cleanedFields; + } + + + function _getHash(fields) { + var hashFields = []; + for each(var field in Zotero.Creators.fields) { + hashFields.push(fields[field]); + } + var ZU = new Zotero.Utilities; + return ZU.md5(hashFields.join('_')); + } + + + function _getDataFromID(creatorDataID) { + var sql = "SELECT * FROM creatorData WHERE creatorDataID=?"; + return Zotero.DB.rowQuery(sql, creatorDataID); + } + + + function _updateCachedData(creatorDataID) { + for (var hash in _creatorDataHash) { + if (_creatorDataHash[hash] == creatorDataID) { + delete _creatorDataHash[hash]; + } + } + + var creators = getCreatorsWithData(creatorDataID); + for each(var creatorID in creators) { + if (_creatorsByID[creatorID]) { + _creatorsByID[creatorID].load(); + } + } + } +} diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -0,0 +1,3392 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +/* + * Constructor for Item object + * + * Generally should be called through Zotero.Items rather than directly + */ +Zotero.Item = function(itemID, itemTypeOrID) { + if (!this._init) { + throw ('Zotero.Item() cannot be called statically'); + } + + // + // These members are public so that they can be accessed by public methods + // -- do not access directly + // + + this._disabled = false; + this._init(); + + if (itemID) { + if (itemID != parseInt(itemID)) { + throw ("Invalid itemID '" + itemID + "' in Zotero.Item(itemID, itemTypeOrID)"); + } + this._itemID = parseInt(itemID); + } + + if (itemTypeOrID) { + // setType initializes type-specific properties in this._itemData + this.setType(Zotero.ItemTypes.getID(itemTypeOrID)); + } +} + +Zotero.Item.prototype._init = function () { + // Primary fields + this._itemTypeID = null; + this._dateAdded = null; + this._dateModified = null; + this._key = null; + this._firstCreator = null; + this._numNotes = null; + this._numAttachments = null; + + this._creators = []; + this._itemData = null; + this._sourceItemID = null; + + this._primaryDataLoaded = false; + this._creatorsLoaded = false; + this._itemDataLoaded = false; + + this._changedPrimaryData = false; + this._changedItemData = false; + this._changedCreators = false; + this._changedNote = false; + this._changedSource = false; + this._changedAttachmentData = false; + + this._previousData = null; + + this._noteTitle = null; + this._noteText = null; + this._noteAccessTime = null; + + this._attachmentLinkMode = null; + this._attachmentMIMEType = null; + this._attachmentCharset = null; + this._attachmentPath = null; +} + + +Zotero.Item.prototype.__defineGetter__('id', function () { return this._itemID; }); +Zotero.Item.prototype.__defineGetter__('itemID', function () { return this._itemID; }); +Zotero.Item.prototype.__defineGetter__('itemTypeID', function () { return this.getField('itemTypeID'); }); +Zotero.Item.prototype.__defineGetter__('dateAdded', function () { return this.getField('dateAdded'); }); +Zotero.Item.prototype.__defineGetter__('dateModified', function () { return this.getField('dateModified'); }); +Zotero.Item.prototype.__defineGetter__('key', function () { return this.getField('key'); }); +Zotero.Item.prototype.__defineGetter__('firstCreator', function () { return this.getField('firstCreator'); }); +//Zotero.Item.prototype.__defineGetter__('numNotes', function () { return this._itemID; }); +//Zotero.Item.prototype.__defineGetter__('numAttachments', function () { return this._itemID; }); + + +/* + * Deprecated -- use id property + */ +Zotero.Item.prototype.getID = function() { + Zotero.debug('Item.getID() is deprecated -- use Item.id'); + return this._itemID; +} + +Zotero.Item.prototype.getType = function() { + Zotero.debug('Item.getType() is deprecated -- use Item.itemTypeID'); + return this.getField('itemTypeID'); +} + + +////////////////////////////////////////////////////////////////////////////// +// +// Public Zotero.Item methods +// +////////////////////////////////////////////////////////////////////////////// + +/* + * Check if the specified field is a primary field from the items table + */ +Zotero.Item.prototype.isPrimaryField = function(field) { + // Create primaryFields hash array if not yet created + if (!Zotero.Item.primaryFields) { + Zotero.Item.primaryFields = Zotero.DB.getColumnHash('items'); + Zotero.Item.primaryFields.firstCreator = true; + Zotero.Item.primaryFields.numNotes = true; + Zotero.Item.primaryFields.numAttachments = true; + } + + return !!Zotero.Item.primaryFields[field]; +} + + +/** + * Check if item exists in the database + * + * @return bool TRUE if the item exists, FALSE if not + */ +Zotero.Item.prototype.exists = function() { + if (!this.id) { + throw ('itemID not set in Zotero.Item.exists()'); + } + + var sql = "SELECT COUNT(*) FROM items WHERE itemID=?"; + return !!Zotero.DB.valueQuery(sql, this.id); +} + + +/* + * Retrieves (and loads from DB, if necessary) an itemData field value + * + * Field can be passed as fieldID or fieldName + * + * If |unformatted| is true, skip any special processing of DB value + * (e.g. multipart date field) (default false) + * + * If |includeBaseMapped| is true and field is a base field, returns value of + * type-specific field instead (e.g. 'label' for 'publisher' in 'audioRecording') + */ +Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) { + this._disabledCheck(); + + //Zotero.debug('Requesting field ' + field + ' for item ' + this.id, 4); + if (this.isPrimaryField(field)) { + var privField = '_' + field; + if (this.id && !this._primaryDataLoaded) { + this.loadPrimaryData(true); + } + //Zotero.debug('Returning ' + (this[privField] ? this[privField] : '') + ' (typeof ' + typeof this[privField] + ')'); + return this[privField]; + } + + if (this.isNote()) { + switch (Zotero.ItemFields.getName(field)) { + case 'title': + return this.getNoteTitle(); + + default: + return ''; + } + } + + if (includeBaseMapped) { + var fieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase( + this.itemTypeID, field + ); + } + + if (!fieldID) { + var fieldID = Zotero.ItemFields.getID(field); + } + + if (typeof this._itemData[fieldID] == 'undefined') { + //Zotero.debug("Field '" + field + "' doesn't exist for item type " + this._itemTypeID + " in Item.getField()"); + return ''; + } + + if (this.id && this._itemData[fieldID] === null && !this._itemDataLoaded) { + this._loadItemData(); + } + + var value = this._itemData[fieldID] ? this._itemData[fieldID] : ''; + + if (!unformatted) { + // Multipart date fields + if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { + value = Zotero.Date.multipartToStr(value); + } + } + //Zotero.debug('Returning ' + value); + return value; +} + + +Zotero.Item.prototype.getUsedFields = function(asNames) { + var sql = "SELECT fieldID FROM itemData WHERE itemID=?"; + if (asNames) { + sql = "SELECT fieldName FROM fields WHERE fieldID IN (" + sql + ")"; + } + return Zotero.DB.columnQuery(sql, this.id); +} + + + +/* + * Build object from database + */ +Zotero.Item.prototype.loadPrimaryData = function(allowFail) { + if (!this.id) { + throw ('ID not set in Zotero.Item.loadPrimaryData()'); + } + + var columns = [], join = [], where = []; + for (var field in Zotero.Item.primaryFields) { + var colSQL = null, joinSQL = null, whereSQL = null; + // If field not already set + if (this['_' + field] === null) { + // Parts should be the same as query in Zotero.Items._load, just + // without itemID clause + switch (field) { + case 'itemTypeID': + case 'dateAdded': + case 'dateModified': + case 'key': + colSQL = 'I.' + field; + break; + + case 'firstCreator': + colSQL = Zotero.Items.getFirstCreatorSQL(); + break; + + case 'numNotes': + colSQL = '(SELECT COUNT(*) FROM itemNotes ' + + 'WHERE sourceItemID=I.itemID) AS numNotes'; + break; + + case 'numAttachments': + colSQL = '(SELECT COUNT(*) FROM itemAttachments ' + + 'WHERE sourceItemID=I.itemID) AS numAttachments'; + break; + } + if (colSQL) { + columns.push(colSQL); + } + if (joinSQL) { + join.push(joinSQL); + } + if (whereSQL) { + where.push(whereSQL); + } + } + } + + var sql = 'SELECT I.itemID' + (columns.length ? ', ' + columns.join(', ') : '') + + " FROM items I " + (join.length ? join.join(' ') + ' ' : '') + + "WHERE I.itemID=?" + (where.length ? ' AND ' + where.join(' AND ') : ''); + var row = Zotero.DB.rowQuery(sql, this.id); + + if (!row) { + if (allowFail) { + this._primaryDataLoaded = true; + return false; + } + throw ("Item " + this.id + " not found in Zotero.Item.loadPrimaryData()"); + } + + this.loadFromRow(row); + + return true; +} + + +/* + * Populate basic item data from a database row + */ +Zotero.Item.prototype.loadFromRow = function(row, reload) { + if (reload) { + this._init(); + } + + // If necessary or reloading, set the type, initialize this._itemData, + // and reset _itemDataLoaded + if (reload || (!this._itemTypeID && row.itemTypeID)) { + this.setType(row.itemTypeID, true); + } + + for (var col in row) { + // Only accept primary field data through loadFromRow() + if (this.isPrimaryField(col)) { + //Zotero.debug("Setting field '" + col + "' to '" + row[col] + "' for item " + this.id); + this['_' + col] = row[col] ? row[col] : ''; + } + else { + Zotero.debug(col + ' is not a valid primary field'); + } + } + + this._primaryDataLoaded = true; +} + + +/* + * Check if any data fields have changed since last save + */ +Zotero.Item.prototype.hasChanged = function() { + return !!(this._changedPrimaryData + || this._changedCreators + || this._changedItemData + || this._changedNote + || this._changedSource + || this._changedAttachmentData); +} + + +/* + * Set or change the item's type + */ +Zotero.Item.prototype.setType = function(itemTypeID, loadIn) { + if (itemTypeID == this._itemTypeID) { + return true; + } + + // If there's an existing type + if (this._itemTypeID) { + if (loadIn) { + throw ('Cannot change type in loadIn mode in Zotero.Item.setType()'); + } + + if (this.id && !this._itemDataLoaded) { + this._loadItemData(); + } + + var copiedFields = []; + + var obsoleteFields = this.getFieldsNotInType(itemTypeID); + if (obsoleteFields) { + for each(var oldFieldID in obsoleteFields) { + // Try to get a base type for this field + var baseFieldID = + Zotero.ItemFields.getBaseIDFromTypeAndField(this.itemTypeID, oldFieldID); + + if (baseFieldID) { + var newFieldID = + Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, baseFieldID); + + // If so, save value to copy to new field + if (newFieldID) { + copiedFields.push([newFieldID, this.getField(oldFieldID)]); + } + } + + // Clear old field + /* + delete this._itemData[oldFieldID]; + if (!this._changedItemData) { + this._changedItemData = {}; + } + this._changedItemData[oldFieldID] = true; + */ + this.setField(oldFieldID, false); + } + } + + for (var fieldID in this._itemData) { + if (this._itemData[fieldID] && + (!obsoleteFields || obsoleteFields.indexOf(fieldID) == -1)) { + copiedFields.push([fieldID, this.getField(fieldID)]); + } + } + + // And reset custom creator types to the default + var creators = this.getCreators(); + if (creators) { + for (var i in creators) { + if (!Zotero.CreatorTypes.isValidForItemType(creators[i].creatorTypeID, itemTypeID)) { + // Reset to contributor (creatorTypeID 2), which exists in all + this.setCreator(i, creators[i].ref, 2); + } + } + } + } + + this._itemTypeID = itemTypeID; + + // Initialize this._itemData with type-specific fields + this._itemData = {}; + var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID); + for each(var fieldID in fields) { + this._itemData[fieldID] = null; + } + + // DEBUG: clear change item data? + + if (copiedFields) { + for each(var f in copiedFields) { + this.setField(f[0], f[1]); + } + } + + if (loadIn) { + this._itemDataLoaded = false; + } + else { + if (!this._changedPrimaryData) { + this._changedPrimaryData = {}; + } + this._changedPrimaryData['itemTypeID'] = true; + } + + return true; +} + + +/* + * Find existing fields from current type that aren't in another + * + * If _allowBaseConversion_, don't return fields that can be converted + * via base fields (e.g. label => publisher => studio) + */ +Zotero.Item.prototype.getFieldsNotInType = function (itemTypeID, allowBaseConversion) { + var fieldIDs = []; + + for (var field in this._itemData) { + if (this._itemData[field]) { + var fieldID = Zotero.ItemFields.getID(field); + if (Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) { + continue; + } + + if (allowBaseConversion) { + var baseID = Zotero.ItemFields.getBaseIDFromTypeAndField(this.itemTypeID, field); + if (baseID) { + var newFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, baseID); + if (newFieldID) { + continue; + } + } + } + + fieldIDs.push(fieldID); + } + } + /* + var sql = "SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?1 AND " + + "fieldID IN (SELECT fieldID FROM itemData WHERE itemID=?2) AND " + + "fieldID NOT IN (SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)"; + + if (allowBaseConversion) { + // Not the type-specific field for a base field in the new type + sql += " AND fieldID NOT IN (SELECT fieldID FROM baseFieldMappings " + + "WHERE itemTypeID=?1 AND baseFieldID IN " + + "(SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)) AND "; + // And not a base field with a type-specific field in the new type + sql += "fieldID NOT IN (SELECT baseFieldID FROM baseFieldMappings " + + "WHERE itemTypeID=?3) AND "; + // And not the type-specific field for a base field that has + // a type-specific field in the new type + sql += "fieldID NOT IN (SELECT fieldID FROM baseFieldMappings " + + "WHERE itemTypeID=?1 AND baseFieldID IN " + + "(SELECT baseFieldID FROM baseFieldMappings WHERE itemTypeID=?3))"; + } + + return Zotero.DB.columnQuery(sql, [this.itemTypeID, this.id, { int: itemTypeID }]); + */ + if (!fieldIDs.length) { + return false; + } + + return fieldIDs; +} + + +/** +* 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); +} + + +/** +* Determine whether the item belongs to a given collectionID +**/ +Zotero.Item.prototype.inCollection = function(collectionID) { + return !!parseInt(Zotero.DB.valueQuery("SELECT COUNT(*) " + + "FROM collectionItems WHERE collectionID=" + collectionID + " AND " + + "itemID=" + this.id)); +} + + +/* + * Set a field value, loading existing itemData first if necessary + * + * Field can be passed as fieldID or fieldName + */ +Zotero.Item.prototype.setField = function(field, value, loadIn) { + this._disabledCheck(); + + //Zotero.debug("Setting field '" + field + "' to '" + value + "' (loadIn: " + (loadIn ? 'true' : 'false') + ")"); + + if (!field) { + throw ("Field not specified in Item.setField()"); + } + + // Primary field + if (this.isPrimaryField(field)) { + switch (field) { + //case 'itemID': // necessary for id changes during sync + case 'firstCreator': + case 'numNotes': + case 'numAttachments': + throw ('Primary field ' + field + ' cannot be changed through setField()'); + } + + if (this.id) { + if (!this._primaryDataLoaded) { + this.loadPrimaryData(true); + } + } + else { + this._primaryDataLoaded = true; + } + + /* + if (!Zotero.ItemFields.validate(field, value)) { + throw("Value '" + value + "' of type " + typeof value + " does not validate for field '" + field + "' in Zotero.Item.setField()"); + } + */ + + if (loadIn) { + // allowed? + throw('Cannot set primary field through setField() in loadIn mode'); + } + + // If field value has changed + // dateModified is always marked as changed + if (this['_' + field] != value || field == 'dateModified') { + Zotero.debug("Field '" + field + "' has changed from '" + this['_' + field] + "' to '" + value + "'", 4); + + // Save a copy of the object before modifying + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } + if (field == 'itemTypeID') { + this.setType(value, loadIn); + } + else { + this['_' + field] = value; + + if (!this._changedPrimaryData) { + this._changedPrimaryData = {}; + } + this._changedPrimaryData[field] = true; + } + } + else { + Zotero.debug("Field '" + field + "' has not changed", 4); + } + return true; + } + + if (!this.itemTypeID) { + throw ('Item type must be set before setting field data'); + } + + // If existing item, load field data first unless we're already in + // the middle of a load + if (this.id) { + if (!loadIn && !this._itemDataLoaded) { + this._loadItemData(); + } + } + else { + this._itemDataLoaded = true; + } + + var fieldID = Zotero.ItemFields.getID(field); + + if (!fieldID) { + throw ('"' + field + '" is not a valid itemData field.'); + } + + if (loadIn && this.isNote() && field == 110) { // title + this._noteTitle = value; + return true; + } + + if (!Zotero.ItemFields.isValidForType(fieldID, this.itemTypeID)) { + var msg = '"' + field + "' is not a valid field for type " + this.itemTypeID; + + if (loadIn) { + Zotero.debug(msg + " -- ignoring value '" + value + "'", 2); + return false; + } + else { + throw (msg); + } + } + + if (!loadIn) { + // Save date field as multipart date + if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') && + !Zotero.Date.isMultipart(value)) { + value = Zotero.Date.strToMultipart(value); + } + // Validate access date + else if (fieldID == Zotero.ItemFields.getID('accessDate')) { + if (value && (!Zotero.Date.isSQLDate(value) && + !Zotero.Date.isSQLDateTime(value) && + value != 'CURRENT_TIMESTAMP')) { + Zotero.debug("Discarding invalid accessDate '" + value + + "' in Item.setField()"); + return false; + } + } + + // If existing value, make sure it's actually changing + if ((!this._itemData[fieldID] && !value) || + (this._itemData[fieldID] && this._itemData[fieldID]==value)) { + return false; + } + + // Save a copy of the object before modifying + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } + } + + this._itemData[fieldID] = value; + + if (!loadIn) { + if (!this._changedItemData) { + this._changedItemData = {}; + } + this._changedItemData[fieldID] = true; + } + return true; +} + + +/* + * Get the title for an item for display in the interface + * + * This is the same as the standard title field (with includeBaseMapped on) + * except for letters and interviews, which get placeholder titles in + * square braces (e.g. "[Letter to Thoreau]") + */ +Zotero.Item.prototype.getDisplayTitle = function (includeAuthorAndDate) { + var title = this.getField('title', false, true); + var itemTypeID = this.itemTypeID; + var itemTypeName = Zotero.ItemTypes.getName(itemTypeID); + + if (!title && (itemTypeID == 8 || itemTypeID == 10)) { // 'letter' and 'interview' itemTypeIDs + var creators = this.getCreators(); + var authors = []; + var participants = []; + if (creators) { + for each(var creator in creators) { + if ((itemTypeID == 8 && creator.creatorTypeID == 16) || // 'letter'/'recipient' + (itemTypeID == 10 && creator.creatorTypeID == 7)) { // 'interview'/'interviewer' + participants.push(creator); + } + else if ((itemTypeID == 8 && creator.creatorTypeID == 1) || // 'letter'/'author' + (itemTypeID == 10 && creator.creatorTypeID == 6)) { // 'interview'/'interviewee' + authors.push(creator); + } + } + } + + var strParts = []; + + if (includeAuthorAndDate) { + var names = []; + for each(author in authors) { + names.push(author.ref.lastName); + } + + // TODO: Use same logic as getFirstCreatorSQL() (including "et al.") + if (names.length) { + strParts.push(Zotero.localeJoin(names, ', ')); + } + } + + if (participants.length > 0) { + var names = []; + for each(participant in participants) { + names.push(participant.ref.lastName); + } + switch (names.length) { + case 1: + var str = 'oneParticipant'; + break; + + case 2: + var str = 'twoParticipants'; + break; + + case 3: + var str = 'threeParticipants'; + break; + + default: + var str = 'manyParticipants'; + } + strParts.push(Zotero.getString('pane.items.' + itemTypeName + '.' + str, names)); + } + else { + strParts.push(Zotero.getString('itemTypes.' + itemTypeName)); + } + + if (includeAuthorAndDate) { + var d = this.getField('date'); + if (d) { + strParts.push(d); + } + } + + title = '['; + title += Zotero.localeJoin(strParts, '; '); + title += ']'; + } + + return title; +} + + +/* + * Returns the number of creators for this item + */ +Zotero.Item.prototype.numCreators = function() { + if (this.id && !this._creatorsLoaded) { + this._loadCreators(); + } + return this._creators.length; +} + + +Zotero.Item.prototype.hasCreatorAt = function(pos) { + if (this.id && !this._creatorsLoaded) { + this._loadCreators(); + } + + return !!this._creators[pos]; +} + + +/* + * Returns an array of the creator data at the given position, or false if none + * + * Note: Creator data array is returned by reference + */ +Zotero.Item.prototype.getCreator = function(pos) { + if (this.id && !this._creatorsLoaded) { + this._loadCreators(); + } + + return this._creators[pos] ? this._creators[pos] : false; +} + + +/** + * Return the position of the given creator, or FALSE if not found + */ +Zotero.Item.prototype.getCreatorPosition = function(creatorID) { + if (this.id && !this._creatorsLoaded) { + this._loadCreators(); + } + + for (var pos in this._creators) { + if (this._creators[pos].creatorID == creatorID) { + return pos; + } + } + + return false; +} + + +/* + * Returns a multidimensional array of creators, or an empty array if none + * + * Note: Creator data array is returned by reference + */ +Zotero.Item.prototype.getCreators = function() { + if (this.id && !this._creatorsLoaded) { + this._loadCreators(); + } + + return this._creators; +} + + +/* + * Set or update the creator at the specified position + * + * |orderIndex|: the position of this creator in the item (from 0) + * |creatorTypeIDOrName|: id or type name + */ +Zotero.Item.prototype.setCreator = function(orderIndex, creator, creatorTypeIDOrName) { + if (this.id) { + if (!this._creatorsLoaded) { + this._loadCreators(); + } + } + else { + this._creatorsLoaded = true; + } + + if (!(creator instanceof Zotero.Creator)) { + throw ('Creator must be a Zotero.Creator object in Zotero.Item.setCreator()'); + } + + var creatorTypeID = Zotero.CreatorTypes.getID(creatorTypeIDOrName); + + if (!creatorTypeID) { + creatorTypeID = 1; + } + + // If creator at this position hasn't changed, cancel + if (this._creators[orderIndex] && + this._creators[orderIndex].ref.id == creator.id && + this._creators[orderIndex].creatorTypeID == creatorTypeID && + !creator.hasChanged()) { + Zotero.debug("Creator in position " + orderIndex + " hasn't changed", 4); + return false; + } + + this._creators[orderIndex] = { + ref: creator, + creatorTypeID: creatorTypeID + }; + + if (!this._changedCreators) { + this._changedCreators = {}; + } + this._changedCreators[orderIndex] = true; + return true; +} + + +/* + * Remove a creator and shift others down + */ +Zotero.Item.prototype.removeCreator = function(orderIndex) { + if (this.id && !this._creatorsLoaded) { + this._loadCreators(); + } + + if (!this._creators[orderIndex]) { + throw ('No creator exists at position ' + orderIndex + + ' in Zotero.Item.removeCreator()'); + } + + // Shift creator orderIndexes down, going to length+1 so we clear the last one + for (var i=orderIndex, max=this._creators.length+1; i<max; i++) { + var next = this._creators[i+1] ? this._creators[i+1] : false; + if (next) { + this._creators[i] = next; + } + else { + this._creators.splice(i, 1); + } + + if (!this._changedCreators) { + this._changedCreators = {}; + } + this._changedCreators[i] = true; + } + return true; +} + + +/* + * Save changes back to database + * + * Returns true on item update or itemID of new item + */ +Zotero.Item.prototype.save = function() { + if (!this.hasChanged()) { + Zotero.debug('Item ' + this.id + ' has not changed', 4); + return false; + } + + // Make sure there are no gaps in the creator indexes + var creators = this.getCreators(); + var lastPos = -1; + for (var pos in creators) { + if (pos != lastPos + 1) { + throw ("Creator index " + pos + " out of sequence in Zotero.Item.save()"); + } + lastPos++; + } + + var ZU = new Zotero.Utilities; + + Zotero.DB.beginTransaction(); + + // ID change + if (this._changedPrimaryData && this._changedPrimaryData.itemID) { + // Foreign key constraints, how lovely you would be + var oldID = this._previousData.primary.itemID; + var params = [this.id, oldID]; + + Zotero.debug("Changing itemID " + oldID + " to " + this.id); + + var row = Zotero.DB.rowQuery("SELECT * FROM items WHERE itemID=?", oldID); + // Add a new row so we can update the old rows despite FK checks + // Use temp key due to UNIQUE constraint on key column + Zotero.DB.query("INSERT INTO items VALUES (?, ?, ?, ?, ?)", + [this.id, row.itemTypeID, row.dateAdded, row.dateModified, 'TEMPKEY']); + + Zotero.DB.query("UPDATE annotations SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE collectionItems SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE highlights SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE itemCreators SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE itemAttachments SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE itemAttachments SET sourceItemID=? WHERE sourceItemID=?", params); + Zotero.DB.query("UPDATE itemData SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE itemNotes SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE itemNotes SET sourceItemID=? WHERE sourceItemID=?", params); + Zotero.DB.query("UPDATE itemSeeAlso SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE itemSeeAlso SET linkedItemID=? WHERE linkedItemID=?", params); + Zotero.DB.query("UPDATE itemTags SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE fulltextItemWords SET itemID=? WHERE itemID=?", params); + Zotero.DB.query("UPDATE fulltextItems SET itemID=? WHERE itemID=?", params); + + Zotero.DB.query("DELETE FROM items WHERE itemID=?", oldID); + Zotero.DB.query("UPDATE items SET key=? WHERE itemID=?", [row.key, this.id]); + + Zotero.Items.unload(oldID); + Zotero.Notifier.trigger('id-change', 'item', oldID + '-' + this.id); + + // update caches + } + + + var isNew = !this.id || !this.exists(); + + try { + // + // New item, insert and return id + // + if (isNew) { + Zotero.debug('Saving data for new item to database'); + + var sqlColumns = []; + var sqlValues = []; + + // + // Primary fields + // + + // If available id value, use it -- otherwise we'll use autoincrement + var itemID = this.id ? this.id : Zotero.ID.get('items'); + if (itemID) { + sqlColumns.push('itemID'); + sqlValues.push({ int: itemID }); + } + + var key = this.key ? this.key : this._generateKey(); + + sqlColumns.push('itemTypeID', 'key'); + sqlValues.push({ int: this.getField('itemTypeID') }, key); + + if (this.dateAdded) { + sqlColumns.push('dateAdded'); + sqlValues.push(this.dateAdded); + } + + if (this.dateModified) { + sqlColumns.push('dateModified'); + sqlValues.push(this.dateModified); + } + + // Begin history transaction + // No associated id yet, so we use false + //Zotero.History.begin('add-item', false); + + // + // Primary fields + // + var sql = "INSERT INTO items (" + sqlColumns.join(', ') + ') VALUES ('; + // Insert placeholders for bind parameters + for (var i=0; i<sqlValues.length; i++) { + sql += '?, '; + } + sql = sql.substring(0, sql.length-2) + ")"; + + // Save basic data to items table + var insertID = Zotero.DB.query(sql, sqlValues); + if (!itemID) { + itemID = insertID; + } + + //Zotero.History.setAssociatedID(itemID); + //Zotero.History.add('items', 'itemID', itemID); + + // + // ItemData + // + if (this._changedItemData) { + // Use manual bound parameters to speed things up + sql = "SELECT valueID FROM itemDataValues WHERE value=?"; + var valueStatement = Zotero.DB.getStatement(sql); + + sql = "INSERT INTO itemDataValues VALUES (?,?)"; + var insertValueStatement = Zotero.DB.getStatement(sql); + + sql = "INSERT INTO itemData VALUES (?,?,?)"; + var insertStatement = Zotero.DB.getStatement(sql); + + for (fieldID in this._changedItemData) { + var value = this.getField(fieldID, true); + if (!value) { + continue; + } + + if (Zotero.ItemFields.getID('accessDate') == fieldID + && this.getField(fieldID) == 'CURRENT_TIMESTAMP') { + value = Zotero.DB.transactionDateTime; + } + + var dataType = ZU.getSQLDataType(value); + + switch (dataType) { + case 32: + valueStatement.bindInt32Parameter(0, value); + break; + + case 64: + valueStatement.bindInt64Parameter(0, value); + break; + + default: + valueStatement.bindUTF8StringParameter(0, value); + } + if (valueStatement.executeStep()) { + var valueID = valueStatement.getInt32(0); + } + else { + var valueID = null; + } + + valueStatement.reset(); + + if (!valueID) { + valueID = Zotero.ID.get('itemDataValues'); + insertValueStatement.bindInt32Parameter(0, valueID); + + switch (dataType) { + case 32: + insertValueStatement. + bindInt32Parameter(1, value); + break; + + case 64: + insertValueStatement. + bindInt64Parameter(1, value); + break; + + default: + insertValueStatement. + bindUTF8StringParameter(1, value); + } + + try { + Zotero.debug(value); + Zotero.debug(valueID); + insertValueStatement.execute(); + } + catch (e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + } + + insertStatement.bindInt32Parameter(0, itemID); + insertStatement.bindInt32Parameter(1, fieldID); + insertStatement.bindInt32Parameter(2, valueID); + + try { + insertStatement.execute(); + } + catch(e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + + /* + Zotero.History.add('itemData', 'itemID-fieldID', + [itemID, fieldID]); + */ + } + } + + // + // Creators + // + if (this._changedCreators) { + for (var orderIndex in this._changedCreators) { + Zotero.debug('Adding creator in position ' + orderIndex, 4); + var creator = this.getCreator(orderIndex); + + /* + if (!creator.ref.exists()) { + throw ("Creator in position " + orderIndex + " doesn't exist"); + } + */ + + if (!creator) { + continue; + } + + if (creator.ref.hasChanged()) { + Zotero.debug("Auto-saving changed creator " + creator.ref.id); + creator.ref.save(); + } + + sql = 'INSERT INTO itemCreators VALUES (?, ?, ?, ?)'; + Zotero.DB.query(sql, + [{ int: itemID }, { int: creator.ref.id }, + { int: creator.creatorTypeID }, { int: orderIndex }]); + + /* + Zotero.History.add('itemCreators', + 'itemID-creatorID-creatorTypeID', + [this.id, creatorID, creator['creatorTypeID']]); + */ + } + } + + + // Note + if (this.isNote() || this._changedNote) { + sql = "INSERT INTO itemNotes " + + "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)"; + var parent = this.isNote() ? this.getSource() : null; + var bindParams = [ + itemID, + parent ? parent : null, + this._noteText ? this._noteText : '', + this._noteTitle ? this._noteTitle : '' + ]; + Zotero.DB.query(sql, bindParams); + } + + + // Attachment + if (this.isAttachment()) { + var sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, " + + "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)"; + var parent = this.getSource(); + var linkMode = this.attachmentLinkMode; + switch (linkMode) { + case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: + case Zotero.Attachments.LINK_MODE_IMPORTED_URL: + case Zotero.Attachments.LINK_MODE_LINKED_FILE: + case Zotero.Attachments.LINK_MODE_LINKED_URL: + break; + + default: + throw ("Invalid attachment link mode " + linkMode + " in Zotero.Item.save()"); + } + var mimeType = this.attachmentMIMEType; + var charsetID = this.attachmentCharset; + var path = this.attachmentPath; + + var bindParams = [ + itemID, + parent ? parent : null, + { int: linkMode }, + mimeType ? { string: mimeType } : null, + charsetID ? { int: charsetID } : null, + path ? { string: path } : null + ]; + Zotero.DB.query(sql, bindParams); + } + + + // Parent item + if (this._sourceItemID) { + var newSourceItem = Zotero.Items.get(this._sourceItemID); + if (!newSourceItem) { + // TODO: clear caches? + throw ("Cannot set source to invalid item " + this._sourceItemID); + } + + var newSourceItemNotifierData = {}; + newSourceItemNotifierData[newSourceItem.id] = + { old: newSourceItem.serialize() }; + + switch (Zotero.ItemTypes.getName(this.itemTypeID)) { + case 'note': + newSourceItem.incrementNoteCount(); + break; + case 'attachment': + newSourceItem.incrementAttachmentCount(); + break; + } + } + } + + // + // Existing item, update + // + else { + Zotero.debug('Updating database with new item data', 4); + + // Begin history transaction + //Zotero.History.begin('modify-item', this.id); + + // + // Primary fields + // + //Zotero.History.modify('items', 'itemID', this.id); + + + var sql = "UPDATE items SET "; + var sqlValues = []; + + var updateFields = ['itemTypeID', 'key', 'dateAdded', 'dateModified']; + for each(field in updateFields) { + if (this._changedPrimaryData && this._changedPrimaryData[field]) { + sql += field + '=?, '; + sqlValues.push(this.getField(field)); + } + else if (field == 'dateModified') { + sql += field + '=?, '; + sqlValues.push(Zotero.DB.transactionDateTime); + } + } + + sql = sql.substr(0, sql.length-2) + " WHERE itemID=?"; + sqlValues.push({ int: this.id }); + + Zotero.DB.query(sql, sqlValues); + + // + // ItemData + // + if (this._changedItemData) { + var del = []; + + sql = "SELECT valueID FROM itemDataValues WHERE value=?"; + var valueStatement = Zotero.DB.getStatement(sql); + + sql = "INSERT INTO itemDataValues VALUES (?,?)"; + var insertStatement = Zotero.DB.getStatement(sql); + + sql = "REPLACE INTO itemData VALUES (?,?,?)"; + var replaceStatement = Zotero.DB.getStatement(sql); + + for (fieldID in this._changedItemData) { + var value = this.getField(fieldID, true); + + // If field changed and is empty, mark row for deletion + if (!value) { + del.push(fieldID); + continue; + } + + /* + // Field exists + if (this._preChangeArray[Zotero.ItemFields.getName(fieldID)]) { + Zotero.History.modify('itemData', 'itemID-fieldID', + [this.id, fieldID]); + } + // Field is new + else { + Zotero.History.add('itemData', 'itemID-fieldID', + [this.id, fieldID]); + } + */ + + if (Zotero.ItemFields.getID('accessDate') == fieldID + && this.getField(fieldID) == 'CURRENT_TIMESTAMP') { + value = Zotero.DB.transactionDateTime; + } + + var dataType = ZU.getSQLDataType(value); + + switch (dataType) { + case 32: + valueStatement.bindInt32Parameter(0, value); + break; + + case 64: + valueStatement.bindInt64Parameter(0, value); + break; + + default: + valueStatement.bindUTF8StringParameter(0, value); + } + if (valueStatement.executeStep()) { + var valueID = valueStatement.getInt32(0); + } + else { + var valueID = null; + } + + valueStatement.reset(); + + // Create data row if necessary + if (!valueID) { + valueID = Zotero.ID.get('itemDataValues'); + insertStatement.bindInt32Parameter(0, valueID); + + // If this is changed, search.js also needs to + // change + switch (dataType) { + case 32: + insertStatement. + bindInt32Parameter(1, value); + break; + + case 64: + insertStatement. + bindInt64Parameter(1, value); + break; + + default: + insertStatement. + bindUTF8StringParameter(1, value); + } + + try { + insertStatement.execute(); + } + catch (e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + } + + replaceStatement.bindInt32Parameter(0, this.id); + replaceStatement.bindInt32Parameter(1, fieldID); + replaceStatement.bindInt32Parameter(2, valueID); + + try { + replaceStatement.execute(); + } + catch (e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + } + + // Delete blank fields + if (del.length) { + /* + // Add to history + for (var i in del) { + Zotero.History.remove('itemData', 'itemID-fieldID', + [this.id, del[i]]); + } + */ + + sql = 'DELETE from itemData WHERE itemID=? ' + + 'AND fieldID IN (' + + del.map(function () '?').join() + + ')'; + Zotero.DB.query(sql, [this.id].concat(del)); + } + } + + // + // Creators + // + if (this._changedCreators) { + for (var orderIndex in this._changedCreators) { + Zotero.debug('Creator ' + orderIndex + ' has changed', 4); + + var creator = this.getCreator(orderIndex); + + /* + if (!creator.ref.exists()) { + throw ("Creator in position " + orderIndex + " doesn't exist"); + } + */ + + /* + // Delete at position + Zotero.History.remove('itemCreators', 'itemID-orderIndex', + [this.id, orderIndex]); + */ + + var sql2 = 'DELETE FROM itemCreators WHERE itemID=?' + + ' AND orderIndex=?'; + Zotero.DB.query(sql2, [{ int: this.id }, { int: orderIndex }]); + + if (!creator) { + continue; + } + + if (creator.ref.hasChanged()) { + Zotero.debug("Auto-saving changed creator " + creator.ref.id); + creator.ref.save(); + } + + sql = "INSERT INTO itemCreators VALUES (?,?,?,?)"; + + sqlValues = [ + { int: this.id }, + { int: creator.ref.id }, + { int: creator.creatorTypeID }, + { int: orderIndex } + ]; + + Zotero.DB.query(sql, sqlValues); + + /* + Zotero.History.add('itemCreators', + 'itemID-creatorID-creatorTypeID', + [this.id, creatorID, creator['creatorTypeID']]); + */ + } + } + + + // Note + if (this._changedNote) { + if (this._noteText === null || this._noteTitle === null) { + throw ('Cached note values not set with this._changedNote ' + + ' set to true in Item.save()'); + } + + sql = "REPLACE INTO itemNotes " + + "(itemID, sourceItemID, note, title) VALUES (?,?,?,?)"; + var parent = this.isNote() ? this.getSource() : null; + var bindParams = [ + this.id, + parent ? parent : null, + this._noteText, + this._noteTitle + ]; + Zotero.DB.query(sql, bindParams); + } + + + // Attachment + if (this._changedAttachmentData) { + var sql = "REPLACE INTO itemAttachments (itemID, sourceItemID, linkMode, " + + "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)"; + var parent = this.getSource(); + var linkMode = this.attachmentLinkMode; + var mimeType = this.attachmentMIMEType; + var charsetID = this.attachmentCharset; + var path = this.attachmentPath; + + var bindParams = [ + this.id, + parent ? parent : null, + { int: linkMode }, + mimeType ? { string: mimeType } : null, + charsetID ? { int: charsetID } : null, + path ? { string: path } : null + ]; + Zotero.DB.query(sql, bindParams); + } + + + // Parent + if (this._changedSource) { + var type = Zotero.ItemTypes.getName(this.itemTypeID); + var Type = type[0].toUpperCase() + type.substr(1); + + if (this._sourceItemID) { + var newSourceItem = Zotero.Items.get(this._sourceItemID); + if (!newSourceItem) { + // TODO: clear caches + throw ("Cannot set source to invalid item " + this._sourceItemID); + } + } + + if (newSourceItem) { + var newSourceItemNotifierData = {}; + newSourceItemNotifierData[this._sourceItemID] = + { old: newSourceItem.serialize() }; + } + + if (this._previousData) { + var oldSourceItemID = this._previousData.sourceItemID; + if (oldSourceItemID) { + var oldSourceItem = Zotero.Items.get(oldSourceItemID); + } + if (oldSourceItem) { + var oldSourceItemNotifierData = {}; + oldSourceItemNotifierData[oldSourceItemID] = + { old: oldSourceItem.serialize() }; + } + else if (oldSourceItemID) { + var oldSourceItemNotifierData = null; + Zotero.debug("Old source item " + oldSourceItemID + + " didn't exist in setSource()", 2); + } + } + + + // If this was an independent item, remove from any collections + // where it existed previously and add source instead if + // there is one + if (!oldSourceItemID) { + var sql = "SELECT collectionID FROM collectionItems " + + "WHERE itemID=?"; + var changedCollections = Zotero.DB.columnQuery(sql, this.id); + if (changedCollections) { + if (this._sourceItemID) { + sql = "UPDATE OR REPLACE collectionItems " + + "SET itemID=? WHERE itemID=?"; + Zotero.DB.query(sql, [this._sourceItemID, this.id]); + } + else { + sql = "DELETE FROM collectionItems WHERE itemID=?"; + Zotero.DB.query(sql, this.id); + } + } + + // TODO: collection notifier trigger? + } + + // Update DB, if not a note or attachment we already changed above + if (!this._changedAttachmentData && + (!this._changedNote || !this.isNote())) { + var sql = "UPDATE item" + Type + "s SET sourceItemID=? " + + "WHERE itemID=?"; + var bindParams = [ + this._sourceItemID ? { int: this._sourceItemID } : null, + this.id + ]; + Zotero.DB.query(sql, bindParams); + } + + // Update the counts of the previous and new sources + if (oldSourceItem) { + switch (type) { + case 'note': + oldSourceItem.decrementNoteCount(); + break; + case 'attachment': + oldSourceItem.decrementAttachmentCount(); + break; + } + } + + if (newSourceItem) { + switch (type) { + case 'note': + newSourceItem.incrementNoteCount(); + break; + case 'attachment': + newSourceItem.incrementAttachmentCount(); + break; + } + } + } + } + + //Zotero.History.commit(); + Zotero.DB.commitTransaction(); + } + + catch (e) { + //Zotero.History.cancel(); + Zotero.DB.rollbackTransaction(); + throw(e); + } + + if (!this.id) { + this._itemID = itemID; + } + + if (!this.key) { + this._key = key; + } + + Zotero.Items.reload(this.id); + + if (isNew) { + Zotero.Notifier.trigger('add', 'item', this.id); + } + else { + Zotero.Notifier.trigger('modify', 'item', this.id, { old: this._previousData }); + } + + if (oldSourceItem) { + Zotero.Notifier.trigger('modify', 'item', + oldSourceItemID, oldSourceItemNotifierData); + } + if (newSourceItem) { + Zotero.Notifier.trigger('modify', 'item', + this._sourceItemID, newSourceItemNotifierData); + } + + if (isNew) { + var id = this.id; + Zotero.debug('DISABLING ITEM'); + this._disabled = true; + return id; + } + + return true; +} + + +Zotero.Item.prototype.updateDateModified = function() { + var sql = "UPDATE items SET dateModified=? WHERE itemID=?"; + Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]); + sql = "SELECT dateModified FROM items WHERE itemID=?"; + this._dateModified = Zotero.DB.valueQuery(sql, this.id); +} + + +Zotero.Item.prototype.isRegularItem = function() { + return !(this.isNote() || this.isAttachment()); +} + + +Zotero.Item.prototype.numChildren = function() { + return this.numNotes() + this.numAttachments(); +} + + +/** +* Get the itemID of the source item for a note or file +**/ +Zotero.Item.prototype.getSource = function() { + if (this._sourceItemID !== null) { + return this._sourceItemID; + } + + if (!this.id) { + return false; + } + + if (this.isNote()) { + var Type = 'Note'; + } + else if (this.isAttachment()) { + var Type = 'Attachment'; + } + else { + return false; + } + + var sql = "SELECT sourceItemID FROM item" + Type + "s WHERE itemID=?"; + var sourceItemID = Zotero.DB.valueQuery(sql, this.id); + if (!sourceItemID) { + sourceItemID = null; + } + this._sourceItemID = sourceItemID; + return sourceItemID; +} + + +Zotero.Item.prototype.setSource = function(sourceItemID) { + if (this.isNote()) { + var type = 'note'; + var Type = 'Note'; + } + else if (this.isAttachment()) { + var type = 'attachment'; + var Type = 'Attachment'; + } + else { + throw ("setSource() can only be called on items of type 'note' or 'attachment'"); + } + + if (this._sourceItemID == sourceItemID) { + Zotero.debug("Source item has not changed in Zotero.Item.setSource()"); + return false; + } + + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } + + this._sourceItemID = sourceItemID; + this._changedSource = true; + + return true; +} + + +//////////////////////////////////////////////////////// +// +// Methods dealing with note items +// +//////////////////////////////////////////////////////// +Zotero.Item.prototype.incrementNoteCount = function() { + this._numNotes++; +} + + +Zotero.Item.prototype.decrementNoteCount = function() { + this._numNotes--; +} + + +/** +* Determine if an item is a note +**/ +Zotero.Item.prototype.isNote = function() { + return Zotero.ItemTypes.getName(this.itemTypeID) == 'note'; +} + + +/** +* Update an item note +* +* Note: This can only be called on saved notes and attachments +**/ +Zotero.Item.prototype.updateNote = function(text) { + throw ('updateNote() removed -- use setNote() and save()'); +} + + +/** +* Returns number of notes in item +**/ +Zotero.Item.prototype.numNotes = function() { + if (this.isNote()) { + throw ("numNotes() cannot be called on items of type 'note'"); + } + + if (!this.id) { + return 0; + } + + return this._numNotes; +} + + +/** +* Get the first line of the note for display in the items list +**/ +Zotero.Item.prototype.getNoteTitle = function() { + if (!this.isNote() && !this.isAttachment()) { + throw ("getNoteTitle() can only be called on notes and attachments"); + } + + if (this._noteTitle !== null) { + return this._noteTitle; + } + + var sql = "SELECT title FROM itemNotes WHERE itemID=?"; + var title = Zotero.DB.valueQuery(sql, this.id); + + this._noteTitle = title ? title : ''; + + return title ? title : ''; +} + + +/** +* Get the text of an item note +**/ +Zotero.Item.prototype.getNote = function() { + if (!this.isNote() && !this.isAttachment()) { + throw ("getNote() can only be called on notes and attachments"); + } + + if (!this.id) { + return ''; + } + + // Store access time for later garbage collection + this._noteAccessTime = new Date(); + + if (this._noteText !== null) { + return this._noteText; + } + + var sql = "SELECT note FROM itemNotes WHERE itemID=" + this.id; + var note = Zotero.DB.valueQuery(sql); + + this._noteText = note ? note : ''; + + return this._noteText; +} + + +/** +* Set an item note +* +* Note: This can only be called on notes and attachments +**/ +Zotero.Item.prototype.setNote = function(text) { + if (!this.isNote() && !this.isAttachment()) { + throw ("updateNote() can only be called on notes and attachments"); + } + + if (text == this._noteText) { + Zotero.debug("Note has not changed in Zotero.Item.setNote()"); + return false; + } + + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } + + this._noteText = text; + this._noteTitle = Zotero.Notes.noteToTitle(text); + this._changedNote = true; + + return true; +} + + +/** +* Returns an array of note itemIDs for this item +**/ +Zotero.Item.prototype.getNotes = function() { + if (this.isNote()) { + throw ("getNotes() cannot be called on items of type 'note'"); + } + + if (!this.id) { + return []; + } + + var sql = "SELECT N.itemID, title FROM itemNotes N NATURAL JOIN items " + + "WHERE sourceItemID=?"; + + if (Zotero.Prefs.get('sortNotesChronologically')) { + sql += " ORDER BY dateAdded"; + return Zotero.DB.columnQuery(sql, this.id); + } + + var notes = Zotero.DB.query(sql, this.id); + if (!notes) { + return false; + } + + // Sort by title + var collation = Zotero.getLocaleCollation(); + var f = function (a, b) { + var aTitle = Zotero.Items.getSortTitle(a.title); + var bTitle = Zotero.Items.getSortTitle(b.title); + return collation.compareString(1, aTitle, bTitle); + } + + var noteIDs = []; + notes.sort(f); + for each(var note in notes) { + noteIDs.push(note.itemID); + } + return noteIDs; +} + + + +//////////////////////////////////////////////////////// +// +// Methods dealing with attachments +// +// save() is not required for attachment functions +// +/////////////////////////////////////////////////////// +Zotero.Item.prototype.incrementAttachmentCount = function() { + Zotero.debug('incrementing attachment count from ' + this._numAttachments); + this._numAttachments++; +} + + +Zotero.Item.prototype.decrementAttachmentCount = function() { + Zotero.debug('decrementing attachment count from ' + this._numAttachments); + this._numAttachments--; +} + + +/** +* Determine if an item is an attachment +**/ +Zotero.Item.prototype.isAttachment = function() { + return Zotero.ItemTypes.getName(this.itemTypeID) == 'attachment'; +} + + +/** +* Returns number of files in item +**/ +Zotero.Item.prototype.numAttachments = function() { + if (this.isAttachment()) { + throw ("numAttachments() cannot be called on attachment items"); + } + + if (!this.id) { + return 0; + } + + return this._numAttachments; +} + + +/** +* Get an nsILocalFile for the attachment, or false if the associated file +* doesn't exist +* +* _row_ is optional itemAttachments row if available to skip query +* +* Note: Always returns false for items with LINK_MODE_LINKED_URL, +* since they have no files -- use getField('url') instead +**/ +Zotero.Item.prototype.getFile = function(row, skipExistsCheck) { + if (!this.isAttachment()) { + throw ("getFile() can only be called on attachment items"); + } + + if (!row) { + var sql = "SELECT linkMode, path FROM itemAttachments WHERE itemID=" + + this.id; + var row = Zotero.DB.rowQuery(sql); + } + + if (!row) { + throw ('Attachment data not found for item ' + this.id + + ' in getFile()'); + } + + // No associated files for linked URLs + if (row['linkMode']==Zotero.Attachments.LINK_MODE_LINKED_URL) { + return false; + } + + var file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + + if (row['linkMode']==Zotero.Attachments.LINK_MODE_IMPORTED_URL || + row['linkMode']==Zotero.Attachments.LINK_MODE_IMPORTED_FILE) { + try { + var storageDir = Zotero.getStorageDirectory(); + storageDir.QueryInterface(Components.interfaces.nsILocalFile); + file.setRelativeDescriptor(storageDir, row['path']); + if (!file.exists()) { + throw('Invalid relative descriptor'); + } + } + catch (e) { + // See if this is a persistent path + // (deprecated for imported attachments) + Zotero.debug('Invalid relative descriptor -- trying persistent'); + try { + file.persistentDescriptor = row['path']; + + var storageDir = Zotero.getStorageDirectory(); + storageDir.QueryInterface(Components.interfaces.nsILocalFile); + var path = file.getRelativeDescriptor(storageDir); + + // If valid, convert this to a relative descriptor + if (file.exists()) { + Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?", + [path, this.id]); + } + } + catch (e) { + Zotero.debug('Invalid persistent descriptor'); + } + } + } + else { + try { + file.persistentDescriptor = row['path']; + } + catch (e) { + // See if this is an old relative path (deprecated) + Zotero.debug('Invalid persistent descriptor -- trying relative'); + try { + var refDir = (row['linkMode']==this.LINK_MODE_LINKED_FILE) + ? Zotero.getZoteroDirectory() : Zotero.getStorageDirectory(); + file.setRelativeDescriptor(refDir, row['path']); + // If valid, convert this to a persistent descriptor + if (file.exists()) { + Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?", + [file.persistentDescriptor, this.id]); + } + } + catch (e) { + Zotero.debug('Invalid relative descriptor'); + } + } + } + + if (!skipExistsCheck && !file.exists()) { + return false; + } + + return file; +} + + +/* + * Rename file associated with an attachment + * + * -1 Destination file exists -- use _force_ to overwrite + * -2 Error renaming + * false Attachment file not found or other error + */ +Zotero.Item.prototype.renameAttachmentFile = function(newName, overwrite) { + var file = this.getFile(); + if (!file) { + return false; + } + + try { + if (file.leafName == newName) { + return true; + } + + var dest = file.parent; + dest.append(newName); + + if (overwrite) { + dest.remove(null); + } + else if (dest.exists()) { + return -1; + } + + file.moveTo(file.parent, newName); + this.relinkAttachmentFile(file); + + return true; + } + catch (e) { + return -2; + } +} + + +Zotero.Item.prototype.relinkAttachmentFile = function(file) { + var linkMode = this.getAttachmentLinkMode(); + + if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { + throw('Cannot relink linked URL in Zotero.Items.relinkAttachmentFile()'); + } + + var path = Zotero.Attachments.getPath(file, linkMode); + + var sql = "UPDATE itemAttachments SET path=? WHERE itemID=?"; + Zotero.DB.query(sql, [path, this.id]); +} + + + +/* + * Return a file:/// URL path to files and snapshots + */ +Zotero.Item.prototype.getLocalFileURL = function() { + if (!this.isAttachment) { + throw ("getLocalFileURL() can only be called on attachment items"); + } + + var file = this.getFile(); + + var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"] + .getService(Components.interfaces.nsIFileProtocolHandler); + + return nsIFPH.getURLSpecFromFile(file); +} + + +Zotero.Item.prototype.getAttachmentLinkMode = function() { + Zotero.debug("getAttachmentLinkMode() deprecated -- use .attachmentLinkMode"); + return this.attachmentLinkMode; +} + +/** + * Link mode of an attachment + * + * Possible values specified as constants in Zotero.Attachments + * (e.g. Zotero.Attachments.LINK_MODE_LINKED_FILE) + */ +Zotero.Item.prototype.__defineGetter__('attachmentLinkMode', function () { + if (!this.isAttachment()) { + return undefined; + } + + if (this._attachmentLinkMode !== null) { + return this._attachmentLinkMode; + } + + var sql = "SELECT linkMode FROM itemAttachments WHERE itemID=?"; + var linkMode = Zotero.DB.valueQuery(sql, this.id); + this._attachmentLinkMode = linkMode; + return linkMode; +}); + + +Zotero.Item.prototype.__defineSetter__('attachmentLinkMode', function (val) { + if (!this.isAttachment()) { + throw (".attachmentLinkMode can only be set for attachment items"); + } + + switch (val) { + case Zotero.Attachments.LINK_MODE_IMPORTED_FILE: + case Zotero.Attachments.LINK_MODE_IMPORTED_URL: + case Zotero.Attachments.LINK_MODE_LINKED_FILE: + case Zotero.Attachments.LINK_MODE_LINKED_URL: + break; + + default: + throw ("Invalid attachment link mode '" + val + "' in Zotero.Item.attachmentLinkMode setter"); + } + + if (val === this._attachmentLinkMode) { + return; + } + + if (!this._changedAttachmentData) { + this._changedAttachmentData = {}; + } + this._changedAttachmentData.linkMode = true; + this._attachmentLinkMode = val; +}); + + +Zotero.Item.prototype.getAttachmentMIMEType = function() { + Zotero.debug("getAttachmentMIMEType() deprecated -- use .attachmentMIMEType"); + return this.attachmentMIMEType; +} + +/** + * MIME type of an attachment (e.g. 'text/plain') + */ +Zotero.Item.prototype.__defineGetter__('attachmentMIMEType', function () { + if (!this.isAttachment()) { + return undefined; + } + + if (this._attachmentMIMEType !== null) { + return this._attachmentMIMEType; + } + + var sql = "SELECT mimeType FROM itemAttachments WHERE itemID=?"; + var mimeType = Zotero.DB.valueQuery(sql, this.id); + if (!mimeType) { + mimeType = ''; + } + this._attachmentMIMEType = mimeType; + return mimeType; +}); + + +Zotero.Item.prototype.__defineSetter__('attachmentMIMEType', function (val) { + if (!this.isAttachment()) { + throw (".attachmentMIMEType can only be set for attachment items"); + } + + if (!val) { + val = ''; + } + + if (val == this._attachmentMIMEType) { + return; + } + + if (!this._changedAttachmentData) { + this._changedAttachmentData = {}; + } + this._changedAttachmentData.mimeType = true; + this._attachmentMIMEType = val; +}); + + +Zotero.Item.prototype.getAttachmentCharset = function() { + Zotero.debug("getAttachmentCharset() deprecated -- use .attachmentCharset"); + return this.attachmentCharset; +} + + +/** + * Character set of an attachment + */ +Zotero.Item.prototype.__defineGetter__('attachmentCharset', function () { + if (!this.isAttachment()) { + return undefined; + } + + if (this._attachmentCharset !== null) { + return this._attachmentCharset; + } + + var sql = "SELECT charsetID FROM itemAttachments WHERE itemID=?"; + var charset = Zotero.DB.valueQuery(sql, this.id); + if (!charset) { + charset = ''; + } + this._attachmentCharset = charset; + return charset; +}); + + +Zotero.Item.prototype.__defineSetter__('attachmentCharset', function (val) { + if (!this.isAttachment()) { + throw (".attachmentCharset can only be set for attachment items"); + } + + if (!val) { + val = ''; + } + + if (val == this._attachmentCharset) { + return; + } + + if (!this._changedAttachmentData) { + this._changedAttachmentData = {}; + } + this._changedAttachmentData.charset = true; + this._attachmentCharset = val; +}); + + +Zotero.Item.prototype.__defineGetter__('attachmentPath', function () { + if (!this.isAttachment()) { + return undefined; + } + + if (this._attachmentPath !== null) { + return this._attachmentPath; + } + + var sql = "SELECT path FROM itemAttachments WHERE itemID=?"; + var path = Zotero.DB.valueQuery(sql, this.id); + if (!path) { + path = ''; + } + this._attachmentPath = path; + return path; +}); + + +Zotero.Item.prototype.__defineSetter__('attachmentPath', function (val) { + if (!this.isAttachment()) { + throw (".attachmentPath can only be set for attachment items"); + } + + if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { + throw ('attachmentPath cannot be set for link attachments'); + } + + if (!val) { + val = ''; + } + + if (val == this._attachmentPath) { + return; + } + + if (!this._changedAttachmentData) { + this._changedAttachmentData = {}; + } + this._changedAttachmentData.path = true; + this._attachmentPath = val; +}); + + +/** +* Returns an array of attachment itemIDs that have this item as a source, +* or FALSE if none +**/ +Zotero.Item.prototype.getAttachments = function() { + if (this.isAttachment()) { + throw ("getAttachments() cannot be called on attachment items"); + } + + if (!this.id) { + return []; + } + + var sql = "SELECT A.itemID, value AS title FROM itemAttachments A " + + "NATURAL JOIN items I LEFT JOIN itemData ID USING (itemID) " + + "LEFT JOIN itemDataValues IDV " + + "ON (fieldID=110 AND ID.valueID=IDV.valueID) " + + "WHERE sourceItemID=?"; + + if (Zotero.Prefs.get('sortAttachmentsChronologically')) { + sql += " ORDER BY dateAdded"; + return Zotero.DB.columnQuery(sql, this.id); + } + + var attachments = Zotero.DB.query(sql, this.id); + if (!attachments) { + return false; + } + + // Sort by title + var collation = Zotero.getLocaleCollation(); + var f = function (a, b) { + return collation.compareString(1, a.title, b.title); + } + + var attachmentIDs = []; + attachments.sort(f); + for each(var attachment in attachments) { + attachmentIDs.push(attachment.itemID); + } + return attachmentIDs; +} + + +/* + * Returns the itemID of the latest child snapshot of this item with the + * same URL as the item itself, or false if none + */ +Zotero.Item.prototype.getBestSnapshot = function() { + if (!this.isRegularItem()) { + throw ("getBestSnapshot() can only be called on regular items"); + } + + if (!this.getField('url')) { + return false; + } + + var sql = "SELECT IA.itemID FROM itemAttachments IA NATURAL JOIN items I " + + "LEFT JOIN itemData ID ON (IA.itemID=ID.itemID AND fieldID=1) " + + "NATURAL JOIN ItemDataValues " + + "WHERE sourceItemID=? AND linkMode=? AND value=? " + + "ORDER BY dateAdded DESC LIMIT 1"; + + return Zotero.DB.valueQuery(sql, [this.id, + Zotero.Attachments.LINK_MODE_IMPORTED_URL, {string:this.getField('url')}]); +} + + +// +// Methods dealing with item tags +// +// save() is not required for tag functions +// +Zotero.Item.prototype.addTag = function(tag, type) { + if (!this.id) { + throw ('Cannot add tag to unsaved item in Item.addTag()'); + } + + if (!tag) { + Zotero.debug('Not saving empty tag in Item.addTag()', 2); + return false; + } + + if (!type) { + type = 0; + } + + if (type !=0 && type !=1) { + throw ('Invalid tag type in Item.addTag()'); + } + + Zotero.DB.beginTransaction(); + var tagID = Zotero.Tags.getID(tag, type); + var existingTypes = Zotero.Tags.getTypes(tag); + + if (existingTypes) { + // If existing automatic and adding identical user, remove automatic + if (type == 0 && existingTypes.indexOf(1) != -1) { + this.removeTag(Zotero.Tags.getID(tag, 1)); + } + // If existing user and adding automatic, skip + else if (type == 1 && existingTypes.indexOf(0) != -1) { + Zotero.debug('Identical user tag already exists -- skipping automatic tag add'); + Zotero.DB.commitTransaction(); + return false; + } + } + + if (!tagID) { + var tagID = Zotero.Tags.add(tag, type); + } + + try { + var result = this.addTagByID(tagID); + Zotero.DB.commitTransaction(); + } + catch (e) { + Zotero.DB.rollbackTransaction(); + throw (e); + } + + return result ? tagID : false; +} + + +Zotero.Item.prototype.addTags = function (tags, type) { + Zotero.DB.beginTransaction(); + try { + for each(var tag in tags) { + this.addTag(tag, type); + } + Zotero.DB.commitTransaction(); + } + catch (e) { + Zotero.DB.rollbackTransaction(); + throw (e); + } +} + + +Zotero.Item.prototype.addTagByID = function(tagID) { + if (!this.id) { + throw ('Cannot add tag to unsaved item in Item.addTagByID()'); + } + + if (!tagID) { + Zotero.debug('Not saving nonexistent tag in Item.addTagByID()', 2); + return false; + } + + var sql = "SELECT COUNT(*) FROM tags WHERE tagID = ?"; + var count = !!Zotero.DB.valueQuery(sql, tagID); + + if (!count) { + throw ('Cannot add invalid tag id ' + tagID + ' in Item.addTagByID()'); + } + + Zotero.DB.beginTransaction(); + + // If INSERT OR IGNORE gave us affected rows, we wouldn't need this... + if (this.hasTag(tagID)) { + Zotero.debug('Item ' + this.id + ' already has tag ' + tagID + ' in Item.addTagByID()'); + Zotero.DB.commitTransaction(); + return false; + } + + var sql = "INSERT INTO itemTags VALUES (?,?)"; + Zotero.DB.query(sql, [this.id, tagID]); + + Zotero.DB.commitTransaction(); + Zotero.Notifier.trigger('modify', 'item', this.id); + Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + tagID); + + return true; +} + +Zotero.Item.prototype.hasTag = function(tagID) { + return this.hasTags(tagID); +} + +/* + * Returns true if the item has one or more of |tagIDs| + * + * |tagIDs| can be an int or array of ints + */ +Zotero.Item.prototype.hasTags = function(tagIDs) { + var tagIDs = Zotero.flattenArguments(tagIDs); + + var sql = "SELECT COUNT(*) FROM itemTags WHERE itemID=? AND tagID IN (" + + tagIDs.map(function () '?').join() + ")"; + return !!Zotero.DB.valueQuery(sql, [this.id].concat(tagIDs)); +} + +Zotero.Item.prototype.getTags = function() { + if (!this.id) { + return false; + } + var sql = "SELECT tagID AS id, tag, tagType AS type FROM tags WHERE tagID IN " + + "(SELECT tagID FROM itemTags WHERE itemID=" + this.id + ")"; + + var tags = Zotero.DB.query(sql); + if (!tags) { + return false; + } + + var collation = Zotero.getLocaleCollation(); + tags.sort(function(a, b) { + return collation.compareString(1, a.tag, b.tag); + }); + return tags; +} + +Zotero.Item.prototype.getTagIDs = function() { + var sql = "SELECT tagID FROM itemTags WHERE itemID=" + this.id; + return Zotero.DB.columnQuery(sql); +} + +Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) { + if (!this.id) { + throw ('Cannot replace tag on unsaved item'); + } + + if (!newTag) { + Zotero.debug('Not replacing with empty tag', 2); + return false; + } + + Zotero.DB.beginTransaction(); + + var oldTag = Zotero.Tags.getName(oldTagID); + if (oldTag==newTag) { + Zotero.DB.commitTransaction(); + return false; + } + + this.removeTag(oldTagID); + var id = this.addTag(newTag); + Zotero.DB.commitTransaction(); + Zotero.Notifier.trigger('modify', 'item', this.id); + Zotero.Notifier.trigger('remove', 'item-tag', this.id + '-' + oldTagID); + Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + id); + return id; +} + +Zotero.Item.prototype.removeTag = function(tagID) { + if (!this.id) { + throw ('Cannot remove tag on unsaved item'); + } + + Zotero.DB.beginTransaction(); + var sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=?"; + Zotero.DB.query(sql, [this.id, { int: tagID }]); + Zotero.Tags.purge(); + Zotero.DB.commitTransaction(); + Zotero.Notifier.trigger('modify', 'item', this.id); + Zotero.Notifier.trigger('remove', 'item-tag', this.id + '-' + tagID); +} + +Zotero.Item.prototype.removeAllTags = function() { + if (!this.id) { + throw ('Cannot remove tags on unsaved item'); + } + + Zotero.DB.beginTransaction(); + var tagIDs = this.getTagIDs(); + if (!tagIDs) { + Zotero.DB.commitTransaction(); + return; + } + + Zotero.DB.query("DELETE FROM itemTags WHERE itemID=?", this.id); + Zotero.Tags.purge(); + Zotero.DB.commitTransaction(); + Zotero.Notifier.trigger('modify', 'item', this.id); + + for (var i in tagIDs) { + tagIDs[i] = this.id + '-' + tagIDs[i]; + } + Zotero.Notifier.trigger('remove', 'item-tag', tagIDs); +} + + +// +// Methods dealing with See Also links +// +// save() is not required for See Also functions +// +Zotero.Item.prototype.addSeeAlso = function(itemID) { + if (itemID==this.id) { + Zotero.debug('Cannot add item as See Also of itself', 2); + return false; + } + + Zotero.DB.beginTransaction(); + + var relatedItem = Zotero.Items.get(itemID); + + if (!relatedItem) { + Zotero.DB.commitTransaction(); + throw ("Cannot add invalid item " + itemID + " as See Also"); + return false; + } + + // Check both ways, using a UNION to take advantage of indexes + var sql = "SELECT (SELECT COUNT(*) FROM itemSeeAlso WHERE itemID=?1 AND " + + "linkedItemID=?2) + (SELECT COUNT(*) FROM itemSeeAlso WHERE " + + "linkedItemID=?1 AND itemID=?2)"; + if (Zotero.DB.valueQuery(sql, [this.id, itemID])) { + Zotero.DB.commitTransaction(); + Zotero.debug("Item " + itemID + " already linked", 2); + return false; + } + + var notifierData = {}; + notifierData[this.id] = { old: this.serialize() }; + notifierData[relatedItem.id] = { old: relatedItem.serialize() }; + + var sql = "INSERT INTO itemSeeAlso VALUES (?,?)"; + Zotero.DB.query(sql, [this.id, {int:itemID}]); + Zotero.DB.commitTransaction(); + Zotero.Notifier.trigger('modify', 'item', [this.id, itemID], notifierData); + return true; +} + +Zotero.Item.prototype.removeSeeAlso = function(itemID) { + if (!this.id) { + throw ('Cannot remove related item of unsaved item'); + } + + Zotero.DB.beginTransaction(); + + var relatedItem = Zotero.Items.get(itemID); + if (!relatedItem) { + Zotero.DB.commitTransaction(); + throw ("Cannot remove invalid item " + itemID + " as See Also"); + return false; + } + + var notifierData = {}; + notifierData[this.id] = { old: this.serialize() }; + notifierData[relatedItem.id] = { old: relatedItem.serialize() }; + + var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?"; + Zotero.DB.query(sql, [this.id, itemID]); + var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?"; + Zotero.DB.query(sql, [itemID, this.id]); + Zotero.DB.commitTransaction(); + Zotero.Notifier.trigger('modify', 'item', [this.id, itemID], notifierData); +} + +Zotero.Item.prototype.removeAllRelated = function() { + if (!this.id) { + throw ('Cannot remove related items of unsaved item'); + } + + Zotero.DB.beginTransaction(); + var relateds = this.getSeeAlso(); + if (!relateds) { + Zotero.DB.commitTransaction(); + return; + } + + var notifierData = {}; + notifierData[this.id] = { old: this.serialize() }; + + for each(var id in relateds) { + var item = Zotero.Items.get(id); + if (item) { + notifierData[item.id] = { old: item.serialize() }; + } + } + + Zotero.DB.query("DELETE FROM itemSeeAlso WHERE itemID=?", this.id); + Zotero.DB.query("DELETE FROM itemSeeAlso WHERE linkedItemID=?", this.id); + Zotero.DB.commitTransaction(); + + var ids = [this.id].concat(relateds); + + Zotero.Notifier.trigger('modify', 'item', ids, notifierData); +} + +Zotero.Item.prototype.getSeeAlso = function() { + if (!this.id) { + return false; + } + // Check both ways, using a UNION to take advantage of indexes + var sql ="SELECT linkedItemID FROM itemSeeAlso WHERE itemID=?1 UNION " + + "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?1"; + return Zotero.DB.columnQuery(sql, this.id); +} + + +Zotero.Item.prototype.getImageSrc = function() { + var itemType = Zotero.ItemTypes.getName(this.itemTypeID); + if (itemType == 'attachment') { + var linkMode = this.attachmentLinkMode; + + // Quick hack to use PDF icon for imported files and URLs -- + // extend to support other document types later + if ((linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE || + linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) && + this.attachmentMIMEType == 'application/pdf') { + itemType += '-pdf'; + } + else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) { + itemType += "-file"; + } + else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { + itemType += "-link"; + } + else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) { + itemType += "-snapshot"; + } + else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { + itemType += "-web-link"; + } + } + + return Zotero.ItemTypes.getImageSrc(itemType); +} + + + +/** + * Compares this item to another + * + * Returns a two-element array containing two objects with the differing values, + * or FALSE if no differences + * + * @param object item Zotero.Item to compare this item to + * @param bool includeMatches Include all fields, even those that aren't different + * @param bool ignoreOnlyDateModified If no fields other than dateModified + * are different, just return false + */ +Zotero.Item.prototype.diff = function (item, includeMatches, ignoreOnlyDateModified) { + var thisData = this.serialize(); + var otherData = item.serialize(); + + var diff = [{}, {}]; + var numDiffs = 0; + + var subs = ['primary', 'fields']; + + // TODO: base-mapped fields + for each(var sub in subs) { + diff[0][sub] = {}; + diff[1][sub] = {}; + for (var field in thisData[sub]) { + if (!thisData[sub][field] && !otherData[sub][field]) { + continue; + } + + var changed = !thisData[sub][field] || !otherData[sub][field] || + thisData[sub][field] != otherData[sub][field]; + + if (includeMatches || changed) { + diff[0][sub][field] = thisData[sub][field] ? + thisData[sub][field] : ''; + diff[1][sub][field] = otherData[sub][field] ? + otherData[sub][field] : ''; + } + + if (changed) { + numDiffs++; + } + } + + // DEBUG: some of this is probably redundant + for (var field in otherData[sub]) { + if (diff[0][sub][field] != undefined) { + continue; + } + + if (!thisData[sub][field] && !otherData[sub][field]) { + continue; + } + + var changed = !thisData[sub][field] || !otherData[sub][field] || + thisData[sub][field] != otherData[sub][field]; + + if (includeMatches || changed) { + diff[0][sub][field] = thisData[sub][field] ? + thisData[sub][field] : ''; + diff[1][sub][field] = otherData[sub][field] ? + otherData[sub][field] : ''; + } + + if (changed) { + numDiffs++; + } + } + } + + diff[0].creators = []; + diff[1].creators = []; + // TODO: creators + + // TODO: attachments + + // TODO: notes + + // TODO: tags + + // TODO: related + + // TODO: annotations + + if (numDiffs == 0 || + (ignoreOnlyDateModified && numDiffs == 1 + && diff[0].primary && diff[0].primary.dateModified)) { + return false; + } + + return diff; +} + + +/** + * Returns an unsaved copy of the item + */ +Zotero.Item.prototype.clone = function(includePrimary) { + if (this.isAttachment()) { + throw ('Cloning attachment items not supported in Zotero.Item.clone()'); + } + + Zotero.debug('Cloning item ' + this.id); + + Zotero.DB.beginTransaction(); + + var obj = this.serialize(); + + var itemTypeID = this.itemTypeID; + var newItem = new Zotero.Item(includePrimary ? this.id : false, itemTypeID); + + if (includePrimary) { + for (var field in obj.primary) { + switch (field) { + case 'itemID': + case 'itemType': + continue; + } + newItem.setField(field, obj.primary[field]); + } + } + + // Note + if (this.isNote()) { + newItem.setNote(this.getNote()); + var parent = this.getSource(); + if (parent) { + newItem.setSource(parent); + } + } + // Regular item + else { + for (var field in obj.fields) { + var fieldID = Zotero.ItemFields.getID(field); + if (fieldID && Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) { + newItem.setField(field, obj.fields[field]); + } + } + + if (includePrimary) { + // newItem = loaded from db + // obj = in-memory + var max = Math.max(newItem.numCreators(), this.numCreators()); + var deleteOffset = 0; + for (var i=0; i<max; i++) { + var newIndex = i - deleteOffset; + + // Remove existing creators (loaded because we set the itemID + // above) not in the in-memory version + if (!obj.creators[i]) { + if (newItem.getCreator(newIndex)) { + newItem.removeCreator(newIndex); + deleteOffset++; + } + continue; + } + // Add in-memory creators + newItem.setCreator( + newIndex, this.getCreator(i).ref, obj.creators[i].creatorType + ); + } + } + else { + var i = 0; + for (var c in obj.creators) { + newItem.setCreator(i, this.getCreator(c).ref, c.creatorType); + i++; + } + } + } + + if (obj.tags) { + for each(var tag in obj.tags) { + newItem.addTagByID(tag.id); + } + } + + if (obj.seeAlso) { + for each(var id in obj.seeAlso) { + newItem.addSeeAlso(id) + } + } + + Zotero.DB.commitTransaction(); + + return newItem; +} + + +/** +* Delete item from database and clear from Zotero.Items internal array +* +* Items.erase() should be used instead of this +**/ +Zotero.Item.prototype.erase = function(deleteChildren) { + if (!this.id) { + return false; + } + + Zotero.debug('Deleting item ' + this.id); + + var changedItems = []; + var changedItemsNotifierData = {}; + + Zotero.DB.beginTransaction(); + + var deletedItemNotifierData = {}; + deletedItemNotifierData[this.id] = { old: this.serialize() }; + + // Remove item from parent collections + var parentCollectionIDs = this.getCollections(); + if (parentCollectionIDs) { + for (var i=0; i<parentCollectionIDs.length; i++) { + Zotero.Collections.get(parentCollectionIDs[i]).removeItem(this.id); + } + } + + // Note + if (this.isNote()) { + // Decrement note count of source items + var sql = "SELECT sourceItemID FROM itemNotes WHERE itemID=" + this.id; + var sourceItemID = Zotero.DB.valueQuery(sql); + if (sourceItemID) { + var sourceItem = Zotero.Items.get(sourceItemID); + changedItemsNotifierData[sourceItem.id] = { old: sourceItem.serialize() }; + sourceItem.decrementNoteCount(); + changedItems.push(sourceItemID); + } + } + // Attachment + else if (this.isAttachment()) { + // Decrement file count of source items + var sql = "SELECT sourceItemID FROM itemAttachments WHERE itemID=" + this.id; + var sourceItemID = Zotero.DB.valueQuery(sql); + if (sourceItemID) { + var sourceItem = Zotero.Items.get(sourceItemID); + changedItemsNotifierData[sourceItem.id] = { old: sourceItem.serialize() }; + sourceItem.decrementAttachmentCount(); + changedItems.push(sourceItemID); + } + + // Delete associated files + var linkMode = this.getAttachmentLinkMode(); + switch (linkMode) { + // Link only -- nothing to delete + case Zotero.Attachments.LINK_MODE_LINKED_URL: + break; + default: + try { + var file = Zotero.getStorageDirectory(); + file.append(this.id); + if (file.exists()) { + file.remove(true); + } + } + catch (e) { + Components.utils.reportError(e); + } + } + } + + // Regular item + + // If flag given, delete child notes and files + else if (deleteChildren) { + var sql = "SELECT itemID FROM itemNotes WHERE sourceItemID=?1 UNION " + + "SELECT itemID FROM itemAttachments WHERE sourceItemID=?1"; + var toDelete = Zotero.DB.columnQuery(sql, [this.id]); + + if (toDelete) { + for (var i in toDelete) { + var obj = Zotero.Items.get(toDelete[i]); + obj.erase(true); + } + } + } + + // Otherwise just unlink any child notes or files without deleting + else { + // Notes + var sql = "SELECT itemID FROM itemNotes WHERE sourceItemID=" + this.id; + var childNotes = Zotero.DB.columnQuery(sql); + if (childNotes) { + for each(var id in childNotes) { + var i = Zotero.Items.get(id); + changedItemsNotifierData[i.id] = { old: i.serialize() }; + } + changedItems.push(childNotes); + } + var sql = "UPDATE itemNotes SET sourceItemID=NULL WHERE sourceItemID=" + + this.id; + Zotero.DB.query(sql); + + // Attachments + var sql = "SELECT itemID FROM itemAttachments WHERE sourceItemID=" + this.id; + var childAttachments = Zotero.DB.columnQuery(sql); + if (childAttachments) { + for each(var id in childAttachments) { + var i = Zotero.Items.get(id); + changedItemsNotifierData[i.id] = { old: i.serialize() }; + } + changedItems.push(childAttachments); + } + var sql = "UPDATE itemAttachments SET sourceItemID=NULL WHERE sourceItemID=" + + this.id; + Zotero.DB.query(sql); + } + + // Flag See Also links for notification + var relateds = this.getSeeAlso(); + if (relateds) { + for each(var id in relateds) { + var i = Zotero.Items.get(id); + if (!changedItemsNotifierData[i.id]) { + changedItemsNotifierData[i.id] = { old: i.serialize() }; + } + } + changedItems = changedItems.concat(relateds); + } + + // Clear fulltext cache + if (this.isAttachment()) { + Zotero.Fulltext.clearItemWords(this.id); + //Zotero.Fulltext.clearItemContent(this.id); + } + + + Zotero.DB.query('DELETE FROM itemCreators WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM itemNotes WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM itemAttachments WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM itemSeeAlso WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM itemSeeAlso WHERE linkedItemID=?', this.id); + Zotero.DB.query('DELETE FROM itemTags WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM itemData WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM items WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM annotations WHERE itemID=?', this.id); + Zotero.DB.query('DELETE FROM highlights WHERE itemID=?', this.id); + + Zotero.DB.query(sql); + + try { + Zotero.DB.commitTransaction(); + } + catch (e) { + // On failure, reset count of source items + if (sourceItem) { + if (this.isNote()) { + sourceItem.incrementNoteCount(); + } + else if (this.isAttachment()) { + sourceItem.incrementAttachmentCount(); + } + } + Zotero.DB.rollbackTransaction(); + throw (e); + } + + Zotero.Items.unload(this.id); + + // Send notification of changed items + if (changedItems.length) { + Zotero.Notifier.trigger('modify', 'item', changedItems, changedItemsNotifierData); + } + + Zotero.Notifier.trigger('delete', 'item', this.id, deletedItemNotifierData); +} + + +Zotero.Item.prototype.isCollection = function() { + return false; +} + + +Zotero.Item.prototype.toArray = function (mode) { + Zotero.debug('Zotero.Item.toArray() is deprecated -- use Zotero.Item.serialize()'); + + if (this.id) { + if (!this._primaryDataLoaded) { + this.loadPrimaryData(true); + } + if (!this._itemDataLoaded) { + this._loadItemData(); + } + } + + var arr = {}; + + // Primary fields + for (var i in Zotero.Item.primaryFields) { + switch (i) { + case 'itemTypeID': + arr.itemType = Zotero.ItemTypes.getName(this.itemTypeID); + break; + + // Skip virtual fields + case 'firstCreator': + case 'numNotes': + case 'numAttachments': + continue; + + // For the rest, just copy over + default: + arr[i] = this['_' + i]; + } + } + + // Item metadata + for (var i in this._itemData) { + arr[Zotero.ItemFields.getName(i)] = this._itemData[i] ? this._itemData[i] : ''; + } + + if (mode == 1 || mode == 2) { + if (!arr.title && + (this.itemTypeID == Zotero.ItemTypes.getID('letter') || + this.itemTypeID == Zotero.ItemTypes.getID('interview'))) { + arr.title = this.getDisplayTitle(mode == 2); + } + } + + if (!this.isNote() && !this.isAttachment()) { + // Creators + arr.creators = []; + var creators = this.getCreators(); + for (var i in creators) { + var creator = {}; + // Convert creatorTypeIDs to text + creator.creatorType = + Zotero.CreatorTypes.getName(creators[i].creatorTypeID); + creator.creatorID = creators[i].ref.id; + creator.firstName = creators[i].ref.firstName; + creator.lastName = creators[i].ref.lastName; + creator.fieldMode = creators[i].ref.fieldMode; + arr.creators.push(creator); + } + } + + // Notes + if (this.isNote()) { + arr.note = this.getNote(); + var parent = this.getSource(); + if (parent) { + arr.sourceItemID = parent; + } + } + + // Attachments + if (this.isAttachment()) { + // Attachments can have embedded notes + arr.note = this.getNote(); + + var parent = this.getSource(); + if (parent) { + arr.sourceItemID = parent; + } + } + + // Attach children of regular items + if (this.isRegularItem()) { + // Append attached notes + arr.notes = []; + var notes = this.getNotes(); + for (var i in notes) { + var note = Zotero.Items.get(notes[i]); + arr.notes.push(note.serialize()); + } + + arr.attachments = []; + var attachments = this.getAttachments(); + for (var i in attachments) { + var attachment = Zotero.Items.get(attachments[i]); + arr.attachments.push(attachment.serialize()); + } + } + + arr.tags = this.getTags(); + if (!arr.tags) { + arr.tags = []; + } + arr.related = this.getSeeAlso(); + if (!arr.related) { + arr.related = []; + } + + return arr; +} + +/* + * Convert the item object into a persistent form + * for use by the export functions + * + * Modes: + * + * 1 == e.g. [Letter to Valee] + * 2 == e.g. [Stothard; Letter to Valee; May 8, 1928] + */ +Zotero.Item.prototype.serialize = function(mode) { + if (this.id) { + if (!this._primaryDataLoaded) { + this.loadPrimaryData(true); + } + if (!this._itemDataLoaded) { + this._loadItemData(); + } + } + + var arr = {}; + arr.primary = {}; + arr.virtual = {}; + arr.fields = {}; + + // Primary fields + for (var i in Zotero.Item.primaryFields) { + switch (i) { + case 'itemTypeID': + arr.primary.itemType = Zotero.ItemTypes.getName(this.itemTypeID); + break; + + // Skip virtual fields + case 'firstCreator': + case 'numNotes': + case 'numAttachments': + arr.virtual[i] = this['_' + i]; + continue; + + // For the rest, just copy over + default: + arr.primary[i] = this['_' + i]; + } + } + + // Item metadata + for (var i in this._itemData) { + arr.fields[Zotero.ItemFields.getName(i)] = this._itemData[i] ? this._itemData[i] : ''; + } + + if (mode == 1 || mode == 2) { + if (!arr.fields.title && + (this.itemTypeID == Zotero.ItemTypes.getID('letter') || + this.itemTypeID == Zotero.ItemTypes.getID('interview'))) { + arr.fields.title = this.getDisplayTitle(mode == 2); + } + } + + + if (this.isRegularItem()) { + // Creators + arr.creators = []; + var creators = this.getCreators(); + for (var i in creators) { + var creator = {}; + // Convert creatorTypeIDs to text + creator.creatorType = + Zotero.CreatorTypes.getName(creators[i].creatorTypeID); + creator.creatorID = creators[i].ref.id; + creator.firstName = creators[i].ref.firstName; + creator.lastName = creators[i].ref.lastName; + creator.fieldMode = creators[i].ref.fieldMode; + arr.creators.push(creator); + } + + // Attach children of regular items + + // Append attached notes + arr.notes = []; + var notes = this.getNotes(); + for (var i in notes) { + var note = Zotero.Items.get(notes[i]); + arr.notes.push(note.serialize()); + } + + // Append attachments + arr.attachments = []; + var attachments = this.getAttachments(); + for (var i in attachments) { + var attachment = Zotero.Items.get(attachments[i]); + arr.attachments.push(attachment.serialize()); + } + } + // Notes and embedded attachment notes + else { + if (this.isAttachment()) { + arr.attachment = {}; + arr.attachment.linkMode = this.attachmentLinkMode; + var file = this.getFile(); + arr.attachment.mimeType = this.attachmentMIMEType; + var charsetID = this.attachmentCharset; + arr.attachment.charset = Zotero.CharacterSets.getName(charsetID); + arr.attachment.path = file ? + Zotero.Attachments.getPath(file, arr.attachment.linkMode) : ''; + } + + arr.note = this.getNote(); + var parent = this.getSource(); + if (parent) { + arr.sourceItemID = parent; + } + } + + arr.tags = this.getTags(); + if (!arr.tags) { + arr.tags = []; + } + arr.related = this.getSeeAlso(); + if (!arr.related) { + arr.related = []; + } + + return arr; +} + + + +////////////////////////////////////////////////////////////////////////////// +// +// Private Zotero.Item methods +// +////////////////////////////////////////////////////////////////////////////// + +/* + * Load in the creators from the database + */ +Zotero.Item.prototype._loadCreators = function() { + if (!this.id) { + throw ('ItemID not set for item before attempting to load creators'); + } + + var sql = 'SELECT creatorID, creatorTypeID, orderIndex FROM itemCreators ' + + 'WHERE itemID=? ORDER BY orderIndex'; + var creators = Zotero.DB.query(sql, this.id); + + this._creators = []; + this._creatorsLoaded = true; + + if (!creators) { + return true; + } + + for (var i=0; i<creators.length; i++) { + this._creators[creators[i].orderIndex] = { + ref: new Zotero.Creator(creators[i].creatorID), + creatorTypeID: creators[i].creatorTypeID + }; + } + + return true; +} + + +/* + * Load in the field data from the database + */ +Zotero.Item.prototype._loadItemData = function() { + if (!this.id) { + throw ('ItemID not set for object before attempting to load data'); + } + + var sql = "SELECT fieldID, value FROM itemData NATURAL JOIN itemDataValues " + + "WHERE itemID=?"; + var fields = Zotero.DB.query(sql, this.id); + + var itemTypeFields = Zotero.ItemFields.getItemTypeFields(this.itemTypeID); + + for each(var field in fields) { + this.setField(field.fieldID, field.value, true); + } + + // Mark nonexistent fields as loaded + for each(var fieldID in itemTypeFields) { + if (this._itemData[fieldID] === null) { + this._itemData[fieldID] = false; + } + } + + this._itemDataLoaded = true; +} + + +Zotero.Item.prototype._generateKey = function () { + return Zotero.ID.getKey(); +} + + +Zotero.Item.prototype._disabledCheck = function () { + if (this._disabled) { + var msg = "New Zotero.Item objects shouldn't be accessed after save -- use Zotero.Items.get()"; + Zotero.debug(msg, 2); + Components.utils.reportError(msg); + } +} diff --git a/chrome/content/zotero/xpcom/data/itemFields.js b/chrome/content/zotero/xpcom/data/itemFields.js @@ -0,0 +1,391 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +Zotero.ItemFields = new function() { + // Private members + var _fields = {}; + var _fieldsLoaded; + var _fieldFormats = []; + var _itemTypeFields = []; + var _baseTypeFields = []; + var _typeFieldIDsByBase = {}; + var _typeFieldNamesByBase = {}; + + var self = this; + + // Privileged methods + this.getName = getName; + this.getID = getID; + this.getLocalizedString = getLocalizedString; + this.isValidForType = isValidForType; + this.isInteger = isInteger; + this.getItemTypeFields = getItemTypeFields; + this.isBaseField = isBaseField; + this.isFieldOfBase = isFieldOfBase; + this.getBaseMappedFields = getBaseMappedFields; + this.getFieldIDFromTypeAndBase = getFieldIDFromTypeAndBase; + this.getBaseIDFromTypeAndField = getBaseIDFromTypeAndField; + this.getTypeFieldsFromBase = getTypeFieldsFromBase; + + + /* + * Return the fieldID for a passed fieldID or fieldName + */ + function getID(field) { + if (!_fieldsLoaded) { + _loadFields(); + } + + if (typeof field == 'number') { + return field; + } + + return _fields[field] ? _fields[field]['id'] : false; + } + + + /* + * Return the fieldName for a passed fieldID or fieldName + */ + function getName(field) { + if (!_fieldsLoaded) { + _loadFields(); + } + + return _fields[field] ? _fields[field]['name'] : false; + } + + + function getLocalizedString(itemType, field) { + // unused currently + //var typeName = Zotero.ItemTypes.getName(itemType); + var fieldName = this.getName(field); + + // Fields in the items table are special cases + switch (field) { + case 'dateAdded': + case 'dateModified': + case 'itemType': + fieldName = field; + } + + // TODO: different labels for different item types + + return Zotero.getString("itemFields." + fieldName); + } + + + function isValidForType(fieldID, itemTypeID) { + if (!_fieldsLoaded) { + _loadFields(); + } + + _fieldCheck(fieldID, 'isValidForType'); + + if (!_fields[fieldID]['itemTypes']) { + return false; + } + + return !!_fields[fieldID]['itemTypes'][itemTypeID]; + } + + + function isInteger(fieldID) { + if (!_fieldsLoaded) { + _loadFields(); + } + + _fieldCheck(fieldID, 'isInteger'); + + var ffid = _fields[fieldID]['formatID']; + return _fieldFormats[ffid] ? _fieldFormats[ffid]['isInteger'] : false; + } + + + /* + * Returns an array of fieldIDs for a given item type + */ + function getItemTypeFields(itemTypeID) { + if (_itemTypeFields[itemTypeID]) { + return _itemTypeFields[itemTypeID]; + } + + if (!itemTypeID) { + throw("Invalid item type id '" + itemTypeID + + "' provided to getItemTypeFields()"); + } + + var sql = 'SELECT fieldID FROM itemTypeFields ' + + 'WHERE itemTypeID=' + itemTypeID + ' ORDER BY orderIndex'; + var fields = Zotero.DB.columnQuery(sql); + + _itemTypeFields[itemTypeID] = fields ? fields : []; + return _itemTypeFields[itemTypeID]; + } + + + function isBaseField(field) { + if (!_fieldsLoaded) { + _loadFields(); + } + + _fieldCheck(field, arguments.callee.name); + + return _fields[field]['isBaseField']; + } + + + function isFieldOfBase(field, baseField) { + var fieldID = _fieldCheck(field, 'isFieldOfBase'); + + var baseFieldID = this.getID(baseField); + if (!baseFieldID) { + throw ("Invalid field '" + baseField + '" for base field in ItemFields.getFieldIDFromTypeAndBase()'); + } + + if (fieldID == baseFieldID) { + return true; + } + + var typeFields = this.getTypeFieldsFromBase(baseFieldID); + return typeFields.indexOf(fieldID) != -1; + } + + + function getBaseMappedFields() { + return Zotero.DB.columnQuery("SELECT DISTINCT fieldID FROM baseFieldMappings"); + } + + + /* + * Returns the fieldID of a type-specific field for a given base field + * or false if none + * + * Examples: + * + * 'audioRecording' and 'publisher' returns label's fieldID + * 'book' and 'publisher' returns publisher's fieldID + * 'audioRecording' and 'number' returns false + * + * Accepts names or ids + */ + function getFieldIDFromTypeAndBase(itemType, baseField) { + if (!_fieldsLoaded) { + _loadFields(); + } + + var itemTypeID = Zotero.ItemTypes.getID(itemType); + if (!itemTypeID) { + throw ("Invalid item type '" + itemType + "' in ItemFields.getFieldIDFromTypeAndBase()"); + } + + var baseFieldID = this.getID(baseField); + if (!baseFieldID) { + throw ("Invalid field '" + baseField + '" for base field in ItemFields.getFieldIDFromTypeAndBase()'); + } + + return _baseTypeFields[itemTypeID][baseFieldID]; + } + + + /* + * Returns the fieldID of the base field for a given type-specific field + * or false if none + * + * Examples: + * + * 'audioRecording' and 'label' returns publisher's fieldID + * 'book' and 'publisher' returns publisher's fieldID + * 'audioRecording' and 'runningTime' returns false + * + * Accepts names or ids + */ + function getBaseIDFromTypeAndField(itemType, typeField) { + var itemTypeID = Zotero.ItemTypes.getID(itemType); + var typeFieldID = this.getID(typeField); + + if (!itemTypeID) { + throw ("Invalid item type '" + itemType + "' in ItemFields.getBaseIDFromTypeAndField()"); + } + + _fieldCheck(typeField, 'getBaseIDFromTypeAndField'); + + if (!this.isValidForType(typeFieldID, itemTypeID)) { + throw ("'" + typeField + "' is not a valid field for '" + itemType + "' in ItemFields.getBaseIDFromTypeAndField()"); + } + + // If typeField is already a base field, just return that + if (this.isBaseField(typeFieldID)) { + return typeFieldID; + } + + return Zotero.DB.valueQuery("SELECT baseFieldID FROM baseFieldMappings " + + "WHERE itemTypeID=? AND fieldID=?", [itemTypeID, typeFieldID]); + } + + + /* + * Returns an array of fieldIDs associated with a given base field + * + * e.g. 'publisher' returns fieldIDs for [university, studio, label, network] + */ + function getTypeFieldsFromBase(baseField, asNames) { + var baseFieldID = this.getID(baseField); + if (!baseFieldID) { + throw ("Invalid base field '" + baseField + '" in ItemFields.getTypeFieldsFromBase()'); + } + + if (asNames) { + return _typeFieldNamesByBase[baseFieldID] ? + _typeFieldNamesByBase[baseFieldID] : false; + } + + return _typeFieldIDsByBase[baseFieldID] ? + _typeFieldIDsByBase[baseFieldID] : false; + } + + + /** + * Check whether a field is valid, throwing an exception if not + * (since it should never actually happen) + **/ + function _fieldCheck(field, func) { + var fieldID = self.getID(field); + if (!fieldID) { + throw ("Invalid field '" + field + (func ? "' in ItemFields." + func + "()" : "'")); + } + return fieldID; + } + + + /* + * Returns hash array of itemTypeIDs for which a given field is valid + */ + function _getFieldItemTypes() { + var sql = 'SELECT fieldID, itemTypeID FROM itemTypeFields'; + + var results = Zotero.DB.query(sql); + + if (!results) { + throw ('No fields in itemTypeFields!'); + } + var fields = new Array(); + for (var i=0; i<results.length; i++) { + if (!fields[results[i]['fieldID']]) { + fields[results[i]['fieldID']] = new Array(); + } + fields[results[i]['fieldID']][results[i]['itemTypeID']] = true; + } + return fields; + } + + + /* + * Build a lookup table for base field mappings + */ + function _loadBaseTypeFields() { + // Grab all fields, base field or not + var sql = "SELECT IT.itemTypeID, F.fieldID AS baseFieldID, BFM.fieldID " + + "FROM itemTypes IT LEFT JOIN fields F " + + "LEFT JOIN baseFieldMappings BFM" + + " ON (IT.itemTypeID=BFM.itemTypeID AND F.fieldID=BFM.baseFieldID)"; + var rows = Zotero.DB.query(sql); + + var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappings"; + var baseFields = Zotero.DB.columnQuery(sql); + + var fields = []; + for each(var row in rows) { + if (!fields[row.itemTypeID]) { + fields[row.itemTypeID] = []; + } + if (row.fieldID) { + fields[row.itemTypeID][row.baseFieldID] = row.fieldID; + } + // If a base field and already valid for the type, just use that + else if (isBaseField(row.baseFieldID) && + isValidForType(row.baseFieldID, row.itemTypeID)) { + fields[row.itemTypeID][row.baseFieldID] = row.baseFieldID; + } + // Set false for other fields so that we don't need to test for + // existence + else { + fields[row.itemTypeID][row.baseFieldID] = false; + } + } + + _baseTypeFields = fields; + + + var sql = "SELECT baseFieldID, fieldID, fieldName " + + "FROM baseFieldMappings JOIN fields USING (fieldID)"; + var rows = Zotero.DB.query(sql); + for each(var row in rows) { + if (!_typeFieldIDsByBase[row['baseFieldID']]) { + _typeFieldIDsByBase[row['baseFieldID']] = []; + _typeFieldNamesByBase[row['baseFieldID']] = []; + } + _typeFieldIDsByBase[row['baseFieldID']].push(row['fieldID']); + _typeFieldNamesByBase[row['baseFieldID']].push(row['fieldName']); + } + } + + + /* + * Load all fields into an internal hash array + */ + function _loadFields() { + var result = Zotero.DB.query('SELECT * FROM fieldFormats'); + + for (var i=0; i<result.length; i++) { + _fieldFormats[result[i]['fieldFormatID']] = { + regex: result[i]['regex'], + isInteger: result[i]['isInteger'] + }; + } + + var fields = Zotero.DB.query('SELECT * FROM fields'); + + var fieldItemTypes = _getFieldItemTypes(); + + var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappings"; + var baseFields = Zotero.DB.columnQuery(sql); + + for each(var field in fields) { + _fields[field['fieldID']] = { + id: field['fieldID'], + name: field['fieldName'], + isBaseField: (baseFields.indexOf(field['fieldID']) != -1), + formatID: field['fieldFormatID'], + itemTypes: fieldItemTypes[field['fieldID']] + }; + // Store by name as well as id + _fields[field['fieldName']] = _fields[field['fieldID']]; + } + + _fieldsLoaded = true; + + _loadBaseTypeFields(); + } +} + diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js @@ -0,0 +1,577 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +/* + * Primary interface for accessing Zotero items + */ +Zotero.Items = new function() { + // Privileged methods + this.get = get; + this.getAll = getAll; + this.getUpdated = getUpdated; + this.add = add; + this.reload = reload; + this.reloadAll = reloadAll; + this.cacheFields = cacheFields; + this.erase = erase; + this.purge = purge; + this.unload = unload; + this.getFirstCreatorSQL = getFirstCreatorSQL; + this.getSortTitle = getSortTitle; + + // Private members + var _items = []; + var _itemsLoaded = false; + var _cachedFields = []; + var _firstCreatorSQL = ''; + + + /* + * Retrieves (and loads, if necessary) an arbitrary number of items + * + * Can be passed ids as individual parameters or as an array of ids, or both + * + * If only one argument and it's an id, return object directly; + * otherwise, return array + */ + function get() { + var toLoad = []; + var loaded = []; + + if (!arguments[0]) { + Zotero.debug('No arguments provided to Items.get()'); + return false; + } + + var ids = Zotero.flattenArguments(arguments); + + for (var i=0; i<ids.length; i++) { + // Check if already loaded + if (!_items[ids[i]]) { + toLoad.push(ids[i]); + } + } + + // New items to load + if (toLoad.length) { + _load(toLoad); + } + + // If single id, return the object directly + if (arguments[0] && typeof arguments[0]!='object' + && typeof arguments[1]=='undefined') { + if (!_items[arguments[0]]) { + Zotero.debug("Item " + arguments[0] + " doesn't exist", 2); + return false; + } + return _items[arguments[0]]; + } + + // Otherwise, build return array + for (i=0; i<ids.length; i++) { + if (!_items[ids[i]]) { + Zotero.debug("Item " + ids[i] + " doesn't exist", 2); + continue; + } + loaded.push(_items[ids[i]]); + } + + return loaded; + } + + + /* + * Returns all items in the database + * + * If |onlyTopLevel|, don't include child items + */ + function getAll(onlyTopLevel) { + var sql = 'SELECT A.itemID FROM items A'; + if (onlyTopLevel) { + sql += ' LEFT JOIN itemNotes B USING (itemID) ' + + 'LEFT JOIN itemAttachments C ON (C.itemID=A.itemID) ' + + 'WHERE B.sourceItemID IS NULL AND C.sourceItemID IS NULL'; + } + + var ids = Zotero.DB.columnQuery(sql); + return this.get(ids); + } + + + function getUpdated(date) { + var s = new Zotero.Search(); + if (date) { + s.addCondition('dateModified', 'isAfter', Zotero.Date.dateToSQL(date, true)); + } + return s.search(); + } + + + /* + * Create a new item with optional metadata and pass back the primary reference + * + * Using "var item = new Zotero.Item()" and "item.save()" directly results + * in an orphaned reference to the created item. If other code retrieves the + * new item with Zotero.Items.get() and modifies it, the original reference + * will not reflect the changes. + * + * Using this method avoids the need to call Zotero.Items.get() after save() + * in order to get the primary item reference. Since it accepts metadata + * as a JavaScript object, it also offers a simpler syntax than + * item.setField() and item.setCreator(). + * + * Callers with no need for an up-to-date reference after save() (or who + * don't mind doing an extra Zotero.Items.get()) can use Zotero.Item + * directly if they prefer. + * + * Sample usage: + * + * var data = { + * title: "Shakespeare: The Invention of the Human", + * publisher: "Riverhead Hardcover", + * date: '1998-10-26', + * ISBN: 1573221201, + * pages: 745, + * creators: [ + * ['Harold', 'Bloom', 'author'] + * ] + * }; + * var item = Zotero.Items.add('book', data); + */ + function add(itemTypeOrID, data) { + var item = new Zotero.Item(false, itemTypeOrID); + for (var field in data) { + if (field == 'creators') { + var i = 0; + for each(var creator in data.creators) { + // TODO: accept format from toArray() + + var fields = { + firstName: creator[0], + lastName: creator[1], + fieldMode: creator[3] ? creator[3] : 0 + }; + + var creatorDataID = Zotero.Creators.getDataID(fields); + if (creatorDataID) { + var linkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID); + // TODO: identical creators? + var creatorID = linkedCreators[0]; + } + else { + var creatorObj = new Zotero.Creator; + creatorObj.setFields(fields); + var creatorID = creatorObj.save(); + } + + item.setCreator(i, Zotero.Creators.get(creatorID), creator[2]); + i++; + } + } + else { + item.setField(field, data[field]); + } + } + var id = item.save(); + + return this.get(id); + } + + + /* + * Reloads data for specified items into internal array + * + * Can be passed ids as individual parameters or as an array of ids, or both + */ + function reload() { + if (!arguments[0]) { + return false; + } + + var ids = Zotero.flattenArguments(arguments); + Zotero.debug('Reloading ' + ids); + _load(ids); + + return true; + } + + + function reloadAll() { + Zotero.debug("Loading all items"); + _items = []; + _itemsLoaded = false; + _load(); + } + + + function cacheFields(fields, items) { + Zotero.debug("Caching fields [" + fields.join() + "]" + + (items ? " for " + items + " items" : '')); + _load(items); + + var primaryFields = []; + var fieldIDs = []; + for each(var field in fields) { + // Check if field already cached + if (_cachedFields.indexOf(field) != -1) { + continue; + } + + _cachedFields.push(field); + + if (Zotero.Item.prototype.isPrimaryField(field)) { + primaryFields.push(field); + } + else { + fieldIDs.push(Zotero.ItemFields.getID(field)); + if (Zotero.ItemFields.isBaseField(field)) { + fieldIDs = fieldIDs.concat(Zotero.ItemFields.getTypeFieldsFromBase(field)); + } + } + } + + if (primaryFields.length) { + var sql = "SELECT itemID, " + primaryFields.join(', ') + " FROM items"; + if (items) { + sql += " WHERE itemID IN (" + items.join() + ")"; + } + var rows = Zotero.DB.query(sql); + for each(var row in rows) { + //Zotero.debug('Calling loadFromRow for item ' + row.itemID); + _items[row.itemID].loadFromRow(row); + } + } + + // All fields already cached + if (!fieldIDs.length) { + return; + } + + var allItemIDs = Zotero.DB.columnQuery("SELECT itemID FROM items"); + var itemFieldsCached = {}; + + var sql = "SELECT itemID, fieldID, value FROM itemData " + + "NATURAL JOIN itemDataValues WHERE "; + if (items) { + sql += "itemID IN (" + items.join() + ") AND "; + } + sql += "fieldID IN (" + fieldIDs.join() + ")"; + + var itemDataRows = Zotero.DB.query(sql); + for each(var row in itemDataRows) { + //Zotero.debug('Setting field ' + row.fieldID + ' for item ' + row.itemID); + if (_items[row.itemID]) { + _items[row.itemID].setField(row.fieldID, row.value, true); + } + else { + if (!missingItems) { + var missingItems = {}; + } + if (!missingItems[row.itemID]) { + missingItems[row.itemID] = true; + Components.utils.reportError("itemData row references nonexistent item " + row.itemID); + } + } + + if (!itemFieldsCached[row.itemID]) { + itemFieldsCached[row.itemID] = {}; + } + itemFieldsCached[row.itemID][row.fieldID] = true; + } + + // If 'title' is one of the fields, load in note titles + if (fields.indexOf('title') != -1) { + var titleFieldID = Zotero.ItemFields.getID('title'); + var sql = "SELECT itemID, title FROM itemNotes WHERE itemID" + + " NOT IN (SELECT itemID FROM itemAttachments)"; + if (items) { + sql += " AND itemID IN (" + items.join() + ")"; + } + var rows = Zotero.DB.query(sql); + + for each(var row in rows) { + //Zotero.debug('Setting title for note ' + row.itemID); + if (_items[row.itemID]) { + _items[row.itemID].setField(titleFieldID, row['title'], true); + } + else { + if (!missingItems) { + var missingItems = {}; + } + if (!missingItems[row.itemID]) { + missingItems[row.itemID] = true; + Components.utils.reportError("itemData row references nonexistent item " + row.itemID); + } + } + } + } + + // Set nonexistent fields in the cache list to false (instead of null) + for each(var itemID in allItemIDs) { + for each(var fieldID in fieldIDs) { + if (Zotero.ItemFields.isValidForType(fieldID, _items[itemID].itemTypeID)) { + if (!itemFieldsCached[itemID] || !itemFieldsCached[itemID][fieldID]) { + //Zotero.debug('Setting field ' + fieldID + ' to false for item ' + itemID); + _items[itemID].setField(fieldID, false, true); + } + } + } + } + } + + + /** + * Delete item(s) from database and clear from internal array + * + * If _eraseChildren_ is true, erase child items as well + **/ + function erase(ids, eraseChildren) { + ids = Zotero.flattenArguments(ids); + + Zotero.UnresponsiveScriptIndicator.disable(); + try { + Zotero.DB.beginTransaction(); + for each(var id in ids) { + var item = this.get(id); + if (!item) { + Zotero.debug('Item ' + id + ' does not exist in Items.erase()!', 1); + Zotero.Notifier.trigger('delete', 'item', id); + continue; + } + item.erase(eraseChildren); // calls unload() + item = undefined; + } + this.purge(); + Zotero.DB.commitTransaction(); + } + catch (e) { + Zotero.DB.rollbackTransaction(); + throw (e); + } + finally { + Zotero.UnresponsiveScriptIndicator.enable(); + } + } + + + /* + * Clear entries from various tables that no longer exist + * + * This is called automatically by Items.erase() but must be called + * manually after Item.erase() + */ + function purge() { + Zotero.Creators.purge(); + Zotero.Tags.purge(); + Zotero.Fulltext.purgeUnusedWords(); + + // Purge unused values + var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN " + + "(SELECT valueID FROM itemData)"; + Zotero.DB.query(sql); + } + + + /** + * Clear item from internal array (used by Zotero.Item.erase()) + **/ + function unload(id) { + delete _items[id]; + } + + + /* + * Generate SQL to retrieve firstCreator field + * + * Why do we do this entirely in SQL? Because we're crazy. Crazy like foxes. + */ + function getFirstCreatorSQL() { + if (_firstCreatorSQL) { + return _firstCreatorSQL; + } + + /* This whole block is to get the firstCreator */ + var localizedAnd = Zotero.getString('general.and'); + var sql = "COALESCE(" + + // First try for primary creator types + "CASE (" + + "SELECT COUNT(*) FROM itemCreators IC " + + "LEFT JOIN itemTypeCreatorTypes ITCT " + + "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + + "WHERE itemID=I.itemID AND primaryField=1" + + ") " + + "WHEN 0 THEN NULL " + + "WHEN 1 THEN (" + + "SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + + "NATURAL JOIN creatorData " + + "LEFT JOIN itemTypeCreatorTypes ITCT " + + "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + + "WHERE itemID=I.itemID AND primaryField=1" + + ") " + + "WHEN 2 THEN (" + + "SELECT " + + "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + + "NATURAL JOIN creatorData " + + "LEFT JOIN itemTypeCreatorTypes ITCT " + + "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + + "WHERE itemID=I.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + + " || ' " + localizedAnd + " ' || " + + "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + + "NATURAL JOIN creatorData " + + "LEFT JOIN itemTypeCreatorTypes ITCT " + + "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + + "WHERE itemID=I.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" + + ") " + + "ELSE (" + + "SELECT " + + "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + + "NATURAL JOIN creatorData " + + "LEFT JOIN itemTypeCreatorTypes ITCT " + + "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + + "WHERE itemID=I.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + + " || ' et al.' " + + ") " + + "END, " + + + // Then try editors + "CASE (" + + "SELECT COUNT(*) FROM itemCreators " + + "NATURAL JOIN creatorTypes WHERE itemID=I.itemID AND creatorTypeID IN (3)" + + ") " + + "WHEN 0 THEN NULL " + + "WHEN 1 THEN (" + + "SELECT lastName FROM itemCreators NATURAL JOIN creators " + + "NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (3)" + + ") " + + "WHEN 2 THEN (" + + "SELECT " + + "(SELECT lastName FROM itemCreators NATURAL JOIN creators NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + + " || ' " + localizedAnd + " ' || " + + "(SELECT lastName FROM itemCreators NATURAL JOIN creators NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1,1) " + + ") " + + "ELSE (" + + "SELECT " + + "(SELECT lastName FROM itemCreators NATURAL JOIN creators NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + + " || ' et al.' " + + ") " + + "END, " + + + // Then try contributors + "CASE (" + + "SELECT COUNT(*) FROM itemCreators " + + "NATURAL JOIN creatorTypes WHERE itemID=I.itemID AND creatorTypeID IN (2)" + + ") " + + "WHEN 0 THEN NULL " + + "WHEN 1 THEN (" + + "SELECT lastName FROM itemCreators NATURAL JOIN creators " + + "NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (2)" + + ") " + + "WHEN 2 THEN (" + + "SELECT " + + "(SELECT lastName FROM itemCreators NATURAL JOIN creators NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + + " || ' " + localizedAnd + " ' || " + + "(SELECT lastName FROM itemCreators NATURAL JOIN creators NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1,1) " + + ") " + + "ELSE (" + + "SELECT " + + "(SELECT lastName FROM itemCreators NATURAL JOIN creators NATURAL JOIN creatorData " + + "WHERE itemID=I.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + + " || ' et al.' " + + ") " + + "END" + + ") AS firstCreator"; + + _firstCreatorSQL = sql; + return sql; + } + + + function getSortTitle(title) { + if (!title) { + return ''; + } + if (typeof title == 'number') { + return title + ''; + } + return title.replace(/^[\[\'\"](.*)[\'\"\]]?$/, '$1') + } + + + function _load() { + if (!arguments[0] && _itemsLoaded) { + return; + } + + // Should be the same as parts in Zotero.Item.loadPrimaryData + var sql = 'SELECT I.*, ' + + getFirstCreatorSQL() + ', ' + + "(SELECT COUNT(*) FROM itemNotes WHERE sourceItemID=I.itemID) AS numNotes, " + + "(SELECT COUNT(*) FROM itemAttachments WHERE sourceItemID=I.itemID) AS numAttachments " + + 'FROM items I WHERE 1'; + + if (arguments[0]) { + sql += ' AND I.itemID IN (' + Zotero.join(arguments,',') + ')'; + } + var itemsRows = Zotero.DB.query(sql); + var itemIDs = []; + + for each(var row in itemsRows) { + var itemID = row.itemID; + itemIDs.push(itemID); + + // Item doesn't exist -- create new object and stuff in array + if (!_items[row.itemID]) { + var item = new Zotero.Item(); + item.loadFromRow(row, true); + _items[row.itemID] = item; + } + // Existing item -- reload in place + else { + _items[row.itemID].loadFromRow(row, true); + } + } + + // If loading all items, remove old items that no longer exist + if (!arguments[0]) { + for each(var c in _items) { + if (itemIDs.indexOf(c.id) == -1) { + this.unload(c.id); + } + } + } + + if (!arguments[0]) { + _itemsLoaded = true; + _cachedFields = ['itemID', 'itemTypeID', 'dateAdded', 'dateModified', + 'firstCreator', 'numNotes', 'numAttachments', 'numChildren']; + } + } +} + diff --git a/chrome/content/zotero/xpcom/data/notes.js b/chrome/content/zotero/xpcom/data/notes.js @@ -0,0 +1,43 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +Zotero.Notes = new function() { + this.noteToTitle = noteToTitle; + + this.__defineGetter__("MAX_TITLE_LENGTH", function() { return 80; }); + + /** + * Return first line (or first MAX_LENGTH characters) of note content + **/ + function noteToTitle(text) { + var max = this.MAX_TITLE_LENGTH; + + var t = text.substring(0, max); + var ln = t.indexOf("\n"); + if (ln>-1 && ln<max) { + t = t.substring(0, ln); + } + return t; + } +} + diff --git a/chrome/content/zotero/xpcom/data/tags.js b/chrome/content/zotero/xpcom/data/tags.js @@ -0,0 +1,418 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + + +/* + * Same structure as Zotero.Creators -- make changes in both places if possible + */ +Zotero.Tags = new function() { + var _tags = []; // indexed by tag text + var _tagsByID = []; // indexed by tagID + + this.get = get; + this.getName = getName; + this.getID = getID; + this.getIDs = getIDs; + this.getTypes = getTypes; + this.getAll = getAll; + this.getAllWithinSearch = getAllWithinSearch; + this.getTagItems = getTagItems; + this.search = search; + this.add = add; + this.rename = rename; + this.remove = remove; + this.purge = purge; + this.toArray = toArray; + + + /* + * Returns a tag and type for a given tagID + */ + function get(tagID) { + if (_tagsByID[tagID]) { + return _tagsByID[tagID]; + } + + var sql = 'SELECT tag, tagType FROM tags WHERE tagID=?'; + var result = Zotero.DB.rowQuery(sql, tagID); + + if (!result) { + return false; + } + + _tagsByID[tagID] = { + tag: result.tag, + type: result.tagType + }; + return result; + } + + + /* + * Returns a tag for a given tagID + */ + function getName(tagID) { + if (_tagsByID[tagID]) { + return _tagsByID[tagID].tag; + } + + var tag = this.get(tagID); + + return _tagsByID[tagID] ? _tagsByID[tagID].tag : false; + } + + + /* + * Returns the tagID matching given tag and type + */ + function getID(tag, type) { + if (_tags[type] && _tags[type]['_' + tag]) { + return _tags[type]['_' + tag]; + } + + var sql = 'SELECT tagID FROM tags WHERE tag=? AND tagType=?'; + var tagID = Zotero.DB.valueQuery(sql, [tag, type]); + + if (tagID) { + if (!_tags[type]) { + _tags[type] = []; + } + _tags[type]['_' + tag] = tagID; + } + + return tagID; + } + + + /* + * Returns all tagIDs for this tag (of all types) + */ + function getIDs(tag) { + var sql = 'SELECT tagID FROM tags WHERE tag=?'; + return Zotero.DB.columnQuery(sql, [tag]); + } + + + /* + * Returns an array of tagTypes for tags matching given tag + */ + function getTypes(tag) { + var sql = 'SELECT tagType FROM tags WHERE tag=?'; + return Zotero.DB.columnQuery(sql, [tag]); + } + + + /** + * Get all tags indexed by tagID + * + * _types_ is an optional array of tagTypes to fetch + */ + function getAll(types) { + var sql = "SELECT tagID, tag, tagType FROM tags "; + if (types) { + sql += "WHERE tagType IN (" + types.join() + ") "; + } + var tags = Zotero.DB.query(sql); + if (!tags) { + return {}; + } + + var collation = Zotero.getLocaleCollation(); + tags.sort(function(a, b) { + return collation.compareString(1, a.tag, b.tag); + }); + + var indexed = {}; + for (var i=0; i<tags.length; i++) { + indexed[tags[i].tagID] = { + tag: tags[i].tag, + type: tags[i].tagType + }; + } + return indexed; + } + + + /* + * Get all tags within the items of a Zotero.Search object + * + * _types_ is an optional array of tagTypes to fetch + */ + function getAllWithinSearch(search, types) { + // Save search results to temporary table + try { + var tmpTable = search.search(true); + } + catch (e) { + if (e.match(/Saved search [0-9]+ does not exist/)) { + Zotero.DB.rollbackTransaction(); + Zotero.debug(e, 2); + } + else { + throw (e); + } + } + if (!tmpTable) { + return {}; + } + + var sql = "SELECT DISTINCT tagID, tag, tagType FROM itemTags " + + "NATURAL JOIN tags WHERE itemID IN " + + "(SELECT itemID FROM " + tmpTable + ") "; + if (types) { + sql += "AND tagType IN (" + types.join() + ") "; + } + var tags = Zotero.DB.query(sql); + + Zotero.DB.query("DROP TABLE " + tmpTable); + + if (!tags) { + return {}; + } + + var collation = Zotero.getLocaleCollation(); + tags.sort(function(a, b) { + return collation.compareString(1, a.tag, b.tag); + }); + + var indexed = {}; + for (var i=0; i<tags.length; i++) { + indexed[tags[i].tagID] = { + tag: tags[i].tag, + type: tags[i].tagType + }; + } + return indexed; + } + + + function getTagItems(tagID) { + var sql = "SELECT itemID FROM itemTags WHERE tagID=?"; + return Zotero.DB.columnQuery(sql, tagID); + } + + + function search(str) { + var sql = 'SELECT tagID, tag, tagType FROM tags'; + if (str) { + sql += ' WHERE tag LIKE ?'; + } + sql += ' ORDER BY tag COLLATE NOCASE'; + var tags = Zotero.DB.query(sql, str ? '%' + str + '%' : undefined); + var indexed = {}; + for each(var tag in tags) { + indexed[tag.tagID] = { + tag: tag.tag, + type: tag.tagType + }; + } + return indexed; + } + + + /* + * Add a new tag to the database + * + * Returns new tagID + */ + function add(tag, type) { + if (type != 0 && type != 1) { + throw ('Invalid tag type ' + type + ' in Tags.add()'); + } + + if (!type) { + type = 0; + } + + Zotero.debug('Adding new tag of type ' + type, 4); + + Zotero.DB.beginTransaction(); + + var sql = 'INSERT INTO tags VALUES (?,?,?)'; + var rnd = Zotero.ID.get('tags'); + Zotero.DB.query(sql, [{int: rnd}, {string: tag}, {int: type}]); + + Zotero.DB.commitTransaction(); + Zotero.Notifier.trigger('add', 'tag', rnd); + return rnd; + } + + + function rename(tagID, tag) { + Zotero.debug('Renaming tag', 4); + + Zotero.DB.beginTransaction(); + + var tagObj = this.get(tagID); + var oldName = tagObj.tag; + var oldType = tagObj.type; + var notifierData = {}; + notifierData[this.id] = { old: this.toArray() }; + + if (oldName == tag) { + // Convert unchanged automatic tags to manual + if (oldType != 0) { + var sql = "UPDATE tags SET tagType=0 WHERE tagID=?"; + Zotero.DB.query(sql, tagID); + Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData); + } + Zotero.DB.commitTransaction(); + return; + } + + // Check if the new tag already exists + var sql = "SELECT tagID FROM tags WHERE tag=? AND tagType=0"; + var existingTagID = Zotero.DB.valueQuery(sql, tag); + if (existingTagID) { + var itemIDs = this.getTagItems(tagID); + var existingItemIDs = this.getTagItems(existingTagID); + + // Would be easier to just call removeTag(tagID) and addTag(existingID) + // here, but this is considerably more efficient + var sql = "UPDATE OR REPLACE itemTags SET tagID=? WHERE tagID=?"; + Zotero.DB.query(sql, [existingTagID, tagID]); + + // Manual purge of old tag + var sql = "DELETE FROM tags WHERE tagID=?"; + Zotero.DB.query(sql, tagID); + if (_tags[oldType]) { + delete _tags[oldType]['_' + oldName]; + } + delete _tagsByID[tagID]; + Zotero.Notifier.trigger('delete', 'tag', tagID, notifierData); + + // Simulate tag removal on items that used old tag + var itemTags = []; + for (var i in itemIDs) { + itemTags.push(itemIDs[i] + '-' + tagID); + } + Zotero.Notifier.trigger('remove', 'item-tag', itemTags); + + // And send tag add for new tag (except for those that already had it) + var itemTags = []; + for (var i in itemIDs) { + if (existingItemIDs.indexOf(itemIDs[i]) == -1) { + itemTags.push(itemIDs[i] + '-' + existingTagID); + } + } + Zotero.Notifier.trigger('add', 'item-tag', itemTags); + + Zotero.Notifier.trigger('modify', 'item', itemIDs); + Zotero.DB.commitTransaction(); + return; + } + + // 0 == user tag -- we set all renamed tags to 0 + var sql = "UPDATE tags SET tag=?, tagType=0 WHERE tagID=?"; + Zotero.DB.query(sql, [{string: tag}, tagID]); + + var itemIDs = this.getTagItems(tagID); + + if (_tags[oldType]) { + delete _tags[oldType]['_' + oldName]; + } + delete _tagsByID[tagID]; + + Zotero.DB.commitTransaction(); + + Zotero.Notifier.trigger('modify', 'item', itemIDs); + Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData); + } + + + function remove(tagID) { + Zotero.DB.beginTransaction(); + + var sql = "SELECT itemID FROM itemTags WHERE tagID=?"; + var itemIDs = Zotero.DB.columnQuery(sql, tagID); + + if (!itemIDs) { + Zotero.DB.commitTransaction(); + return; + } + + var sql = "DELETE FROM itemTags WHERE tagID=?"; + Zotero.DB.query(sql, tagID); + + Zotero.Notifier.trigger('modify', 'item', itemIDs) + var itemTags = []; + for (var i in itemIDs) { + itemTags.push(itemIDs[i] + '-' + tagID); + } + Zotero.Notifier.trigger('remove', 'item-tag', itemTags); + + this.purge(); + Zotero.DB.commitTransaction(); + return; + } + + + /* + * Delete obsolete tags from database and clear internal array entries + * + * Returns removed tagIDs on success + */ + function purge() { + Zotero.DB.beginTransaction(); + + var sql = 'SELECT tagID, tag, tagType FROM tags WHERE tagID ' + + 'NOT IN (SELECT tagID FROM itemTags);'; + var toDelete = Zotero.DB.query(sql); + + if (!toDelete) { + Zotero.DB.commitTransaction(); + return false; + } + + var purged = []; + var notifierData = {}; + + // Clear tag entries in internal array + for each(var tag in toDelete) { + notifierData[tag.tagID] = { old: Zotero.Tags.toArray(tag.tagID) } + + purged.push(tag.tagID); + if (_tags[tag.tagType]) { + delete _tags[tag.tagType]['_' + tag.tag]; + } + delete _tagsByID[tag.tagID]; + } + + sql = 'DELETE FROM tags WHERE tagID NOT IN ' + + '(SELECT tagID FROM itemTags);'; + var result = Zotero.DB.query(sql); + + Zotero.DB.commitTransaction(); + + Zotero.Notifier.trigger('delete', 'tag', purged, notifierData); + + return toDelete; + } + + + function toArray(tagID) { + var obj = this.get(tagID); + obj.id = tagID; + return obj; + } +} + diff --git a/chrome/content/zotero/xpcom/data_access.js b/chrome/content/zotero/xpcom/data_access.js @@ -20,5179 +20,6 @@ ***** END LICENSE BLOCK ***** */ -/* - * Constructor for Item object - * - * Generally should be called through Zotero.Items rather than directly - */ -Zotero.Item = function(itemTypeOrID){ - this._init(itemTypeOrID); - - if (itemTypeOrID) { - this._changed.set('itemTypeID'); - } -} - -Zotero.Item.prototype._init = function(itemTypeOrID, create) { - // - // These members are public so that they can be accessed by public methods - // -- do not access directly - // - this._data = {}; - this.isPrimaryField('itemID'); // make sure primary field hash array exists - for (var field in Zotero.Item.primaryFields) { - this._data[field] = null; - } - - this._creators = []; - this._itemData = null; - - if (itemTypeOrID) { - // setType initializes type-specific properties in this._itemData - this.setType(Zotero.ItemTypes.getID(itemTypeOrID), true); - } - - this._creatorsLoaded = false; - this._itemDataLoaded = false; - - this._changed = new Zotero.Hash(); - this._changedCreators = new Zotero.Hash(); - this._changedItemData = new Zotero.Hash(); - - this._preChangeArray = null; - - this._noteTitle = null; - this._noteText = null; - this._noteAccessTime = null; - - this._fileLinkMode = null; - this._fileMIMEType = null; -} - - -////////////////////////////////////////////////////////////////////////////// -// -// Public Zotero.Item methods -// -////////////////////////////////////////////////////////////////////////////// - -/* - * Check if the specified field is a primary field from the items table - */ -Zotero.Item.prototype.isPrimaryField = function(field){ - // Create primaryFields hash array if not yet created - if (!Zotero.Item.primaryFields){ - Zotero.Item.primaryFields = Zotero.DB.getColumnHash('items'); - Zotero.Item.primaryFields['firstCreator'] = true; - Zotero.Item.primaryFields['numNotes'] = true; - Zotero.Item.primaryFields['numAttachments'] = true; - } - - return !!Zotero.Item.primaryFields[field]; -} - -/* - * Build object from database - */ -Zotero.Item.prototype.loadFromID = function(id) { - var columns = [], join = [], where = []; - for (var field in Zotero.Item.primaryFields) { - var colSQL = null, joinSQL = null, whereSQL = null; - // If field not already set - if (this._data[field] === null) { - // Parts should be the same as query in Zotero.Items._load, just - // without itemID clause - switch (field) { - case 'itemTypeID': - case 'dateAdded': - case 'dateModified': - colSQL = 'I.' + field; - break; - - case 'firstCreator': - colSQL = Zotero.Items.getFirstCreatorSQL(); - break; - - case 'numNotes': - colSQL = '(SELECT COUNT(*) FROM itemNotes ' - + 'WHERE sourceItemID=I.itemID) AS numNotes'; - break; - - case 'numAttachments': - colSQL = '(SELECT COUNT(*) FROM itemAttachments ' - + 'WHERE sourceItemID=I.itemID) AS numAttachments'; - break; - } - if (colSQL) { - columns.push(colSQL); - } - if (joinSQL) { - join.push(joinSQL); - } - if (whereSQL) { - where.push(whereSQL); - } - } - } - - var sql = 'SELECT I.itemID' + (columns.length ? ', ' + columns.join(', ') : '') - + " FROM items I " + (join.length ? join.join(' ') + ' ' : '') - + "WHERE I.itemID=" + id + (where.length ? ' AND ' + where.join(' AND ') : ''); - var row = Zotero.DB.rowQuery(sql); - this.loadFromRow(row); -} - - -/* - * Populate basic item data from a database row - */ -Zotero.Item.prototype.loadFromRow = function(row, reload) { - // If necessary or reloading, set the type, initialize this._itemData, - // and reset _itemDataLoaded - if (reload || (!this.getType() && row['itemTypeID'])) { - this.setType(row['itemTypeID'], true); - } - - // This is a quick hack to reset the creators on reload -- - // there's probably a better place for this - this._creatorsLoaded = false; - this._changedCreators = new Zotero.Hash(); - this._creators = []; - - for (var col in row){ - // Only accept primary field data through loadFromRow() - if (this.isPrimaryField(col)){ - //Zotero.debug('Setting field ' + col + ' for item ' + this.getID()); - this._data[col] = row[col] ? row[col] : false; - } - else { - Zotero.debug(col + ' is not a valid primary field'); - } - } -} - - -/* - * Check if any data fields have changed since last save - */ -Zotero.Item.prototype.hasChanged = function(){ - return (this._changed.length || this._changedCreators.length || - this._changedItemData.length); -} - - -/* - * Deprecated -- use id property - */ -Zotero.Item.prototype.getID = function(){ - return this._data['itemID'] ? this._data['itemID'] : false; -} - - -Zotero.Item.prototype.__defineGetter__("id", function() { - return this._data['itemID'] ? this._data['itemID'] : false; -}); - - -Zotero.Item.prototype.getType = function(){ - return this._data['itemTypeID'] ? this._data['itemTypeID'] : false; -} - - -/* - * Set or change the item's type - */ -Zotero.Item.prototype.setType = function(itemTypeID, loadIn) { - if (itemTypeID==this.getType()){ - return true; - } - - // If there's an existing type - if (this.getType()){ - var copiedFields = []; - - var obsoleteFields = this.getFieldsNotInType(itemTypeID); - if (obsoleteFields) { - for each(var oldFieldID in obsoleteFields) { - // Try to get a base type for this field - var baseFieldID = - Zotero.ItemFields.getBaseIDFromTypeAndField(this.getType(), oldFieldID); - - if (baseFieldID) { - var newFieldID = - Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, baseFieldID); - - // If so, save value to copy to new field - if (newFieldID) { - copiedFields.push([newFieldID, this.getField(oldFieldID)]); - } - } - - // Clear old field - this.setField(oldFieldID, false); - } - } - - if (!loadIn) { - for (var fieldID in this._itemData) { - if (this._itemData[fieldID] && - (!obsoleteFields || obsoleteFields.indexOf(fieldID) == -1)) { - copiedFields.push([fieldID, this.getField(fieldID)]); - } - } - } - - // And reset custom creator types to the default - var creators = this.getCreators(); - if (creators){ - for (var i in creators){ - if (!Zotero.CreatorTypes.isValidForItemType(creators[i].creatorTypeID, itemTypeID)) - { - // Reset to contributor (creatorTypeID 2), which exists in all - this.setCreator(i, creators[i].firstName, - creators[i].lastName, 2, creators[i].fieldMode); - } - } - } - } - - this._data['itemTypeID'] = itemTypeID; - - // Initialize this._itemData with type-specific fields - this._itemData = {}; - var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID); - for each(var fieldID in fields) { - this._itemData[fieldID] = null; - } - - if (copiedFields) { - for each(var f in copiedFields) { - this.setField(f[0], f[1]); - } - } - - if (loadIn) { - this._itemDataLoaded = false; - } - else { - this._changed.set('itemTypeID'); - } - - return true; -} - - -/* - * Find existing fields from current type that aren't in another - * - * If _allowBaseConversion_, don't return fields that can be converted - * via base fields (e.g. label => publisher => studio) - */ -Zotero.Item.prototype.getFieldsNotInType = function (itemTypeID, allowBaseConversion) { - var sql = "SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?1 AND " - + "fieldID IN (SELECT fieldID FROM itemData WHERE itemID=?2) AND " - + "fieldID NOT IN (SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)"; - - if (allowBaseConversion) { - // Not the type-specific field for a base field in the new type - sql += " AND fieldID NOT IN (SELECT fieldID FROM baseFieldMappings " - + "WHERE itemTypeID=?1 AND baseFieldID IN " - + "(SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)) AND "; - // And not a base field with a type-specific field in the new type - sql += "fieldID NOT IN (SELECT baseFieldID FROM baseFieldMappings " - + "WHERE itemTypeID=?3) AND "; - // And not the type-specific field for a base field that has - // a type-specific field in the new type - sql += "fieldID NOT IN (SELECT fieldID FROM baseFieldMappings " - + "WHERE itemTypeID=?1 AND baseFieldID IN " - + "(SELECT baseFieldID FROM baseFieldMappings WHERE itemTypeID=?3))"; - } - - return Zotero.DB.columnQuery(sql, [this.getType(), this.getID(), {int: itemTypeID}]); -} - - -/** -* 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.getID()); -} - - -/** -* Determine whether the item belongs to a given collectionID -**/ -Zotero.Item.prototype.inCollection = function(collectionID){ - return !!parseInt(Zotero.DB.valueQuery("SELECT COUNT(*) " - + "FROM collectionItems WHERE collectionID=" + collectionID + " AND " - + "itemID=" + this.getID())); -} - - -/* - * Returns the number of creators for this item - */ -Zotero.Item.prototype.numCreators = function(){ - if (this.getID() && !this._creatorsLoaded){ - this._loadCreators(); - } - return this._creators.length; -} - - -Zotero.Item.prototype.hasCreatorAt = function(pos){ - if (this.getID() && !this._creatorsLoaded){ - this._loadCreators(); - } - - return !!this._creators[pos]; -} - - -/* - * Returns an array of the creator data at the given position, or false if none - * - * Note: Creator data array is returned by reference - */ -Zotero.Item.prototype.getCreator = function(pos){ - if (this.getID() && !this._creatorsLoaded){ - this._loadCreators(); - } - - return this._creators[pos] ? this._creators[pos] : false; -} - - -/* - * Returns a multidimensional array of creators, or an empty array if none - * - * Note: Creator data array is returned by reference - */ -Zotero.Item.prototype.getCreators = function(){ - if (this.getID() && !this._creatorsLoaded){ - this._loadCreators(); - } - - var creators = []; - for (var i=0; i<this._creators.length; i++){ - creators.push(this.getCreator(i)); - } - return creators; -} - - -/* - * Set or update the creator at the specified position - * - * _orderIndex_: the position of this creator in the item (from 0) - * _creatorTypeID_: id or type name - * _fieldMode_: 0 for double-field, 1 for single-field mode (default 0) - * - * If fieldMode==1, _firstName_ is ignored - */ -Zotero.Item.prototype.setCreator = function(orderIndex, firstName, lastName, creatorTypeID, fieldMode){ - if (this.getID() && !this._creatorsLoaded){ - this._loadCreators(); - } - - // Default to double-field mode if not specified - if (!fieldMode){ - fieldMode = 0; - } - - if (fieldMode==1 || !firstName){ - firstName = ''; - } - - if (!lastName){ - lastName = ''; - } - - creatorTypeID = Zotero.CreatorTypes.getID(creatorTypeID); - - // If creator at this position hasn't changed, cancel - if (this._creators[orderIndex] && - this._creators[orderIndex]['firstName']==firstName && - this._creators[orderIndex]['lastName']==lastName && - this._creators[orderIndex]['creatorTypeID']==creatorTypeID && - this._creators[orderIndex]['fieldMode']==fieldMode){ - return false; - } - - if (!creatorTypeID){ - creatorTypeID = 1; - } - - var creator = { - firstName: firstName, - lastName: lastName, - creatorTypeID: creatorTypeID, - fieldMode: fieldMode - } - - this._creators[orderIndex] = creator; - this._changedCreators.set(orderIndex); - return true; -} - - -/* - * Remove a creator and shift others down - */ -Zotero.Item.prototype.removeCreator = function(orderIndex){ - if (this.getID() && !this._creatorsLoaded){ - this._loadCreators(); - } - - if (!this._creators[orderIndex]){ - throw ('No creator exists at position ' + orderIndex); - } - this._creators[orderIndex] = false; - - // Shift creator orderIndexes down, going to length+1 so we clear the last one - for (var i=orderIndex, max=this._creators.length+1; i<max; i++){ - var next = this._creators[i+1] ? this._creators[i+1] : false; - if (next) { - this._creators[i] = next; - } - else { - delete this._creators[i]; - } - this._changedCreators.set(i); - } - return true; -} - - -// Currently unused -Zotero.Item.prototype.creatorExists = function(firstName, lastName, creatorTypeID, fieldMode, skipIndex){ - if (fieldMode==1 || !firstName){ - firstName = ''; - } - - if (!lastName){ - lastName = ''; - } - - for (var j=0, len=this.numCreators(); j<len; j++){ - if (typeof skipIndex!='undefined' && skipIndex==j){ - continue; - } - - var creator2 = this.getCreator(j); - - if (firstName==creator2['firstName'] && - lastName==creator2['lastName'] && - creatorTypeID==creator2['creatorTypeID'] && - fieldMode==creator2['fieldMode']){ - return true; - } - } - return false; -} - - -/* - * Retrieves (and loads from DB, if necessary) an itemData field value - * - * Field can be passed as fieldID or fieldName - * - * If |unformatted| is true, skip any special processing of DB value - * (e.g. multipart date field) (default false) - * - * If |includeBaseMapped| is true and field is a base field, returns value of - * type-specific field instead (e.g. 'label' for 'publisher' in 'audioRecording') - */ -Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) { - //Zotero.debug('Requesting field ' + field + ' for item ' + this.getID(), 4); - if (this.isPrimaryField(field)){ - if (this.getID() && this._data[field] === null) { - this.loadFromID(this.getID()); - } - //Zotero.debug('Returning ' + (this._data[field] ? this._data[field] : '')); - return this._data[field] ? this._data[field] : ''; - } - - if (this.isNote()) { - switch (Zotero.ItemFields.getName(field)) { - case 'title': - return this.getNoteTitle(); - - default: - return ''; - } - } - - if (includeBaseMapped) { - var fieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase( - this.getType(), field - ); - } - - if (!fieldID) { - var fieldID = Zotero.ItemFields.getID(field); - } - - if (typeof this._itemData[fieldID] == 'undefined') { - //Zotero.debug('Returning blank for ' + field + ' in ' + this.getType()); - return ''; - } - - if (this.getID() && this._itemData[fieldID] === null && !this._itemDataLoaded) { - this._loadItemData(); - } - - var value = this._itemData[fieldID] ? this._itemData[fieldID] : ''; - - if (!unformatted){ - // Multipart date fields - if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { - value = Zotero.Date.multipartToStr(value); - } - } - //Zotero.debug('Returning ' + value); - return value; -} - - -/* - * Set a field value, loading existing itemData first if necessary - * - * Field can be passed as fieldID or fieldName - */ -Zotero.Item.prototype.setField = function(field, value, loadIn){ - if (!field){ - throw ("Field not specified in Item.setField()"); - } - - // Primary field - if (this.isPrimaryField(field)){ - throw ('Primary field ' + field + ' cannot be changed through setField'); - } - - if (!this.getType()){ - throw ('Item type must be set before setting field data.'); - } - - // If existing item, load field data first unless we're already in - // the middle of a load - if (this.getID() && !loadIn && !this._itemDataLoaded) { - this._loadItemData(); - } - - var fieldID = Zotero.ItemFields.getID(field); - - if (!fieldID){ - throw ('"' + field + '" is not a valid itemData field.'); - } - - if (loadIn && this.isNote() && field == 110) { // title - this._noteTitle = value; - return true; - } - - if (!Zotero.ItemFields.isValidForType(fieldID, this.getType())){ - throw ('"' + field + "' is not a valid field for type " + this.getType()); - } - - if (!loadIn){ - // Save date field as multipart date - if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') && - !Zotero.Date.isMultipart(value)) { - value = Zotero.Date.strToMultipart(value); - } - // Validate access date - else if (fieldID == Zotero.ItemFields.getID('accessDate')) { - if (value && (!Zotero.Date.isSQLDate(value) && - !Zotero.Date.isSQLDateTime(value) && - value != 'CURRENT_TIMESTAMP')) { - Zotero.debug("Discarding invalid accessDate '" + value - + "' in Item.setField()"); - return false; - } - } - - // If existing value, make sure it's actually changing - if ((!this._itemData[fieldID] && !value) || - (this._itemData[fieldID] && this._itemData[fieldID]==value)) { - return false; - } - - // Save a copy of the object before modifying - if (!this._preChangeArray) { - this._preChangeArray = this.toArray(); - } - } - - this._itemData[fieldID] = value; - - if (!loadIn) { - this._changedItemData.set(fieldID); - } - return true; -} - - -/* - * Get the title for an item for display in the interface - * - * This is the same as the standard title field (with includeBaseMapped on) - * except for letters and interviews, which get placeholder titles in - * square braces (e.g. "[Letter to Thoreau]") - */ -Zotero.Item.prototype.getDisplayTitle = function (includeAuthorAndDate) { - var title = this.getField('title', false, true); - - var itemTypeID = this.getType(); - var itemTypeName = Zotero.ItemTypes.getName(itemTypeID); - - if (!title && (itemTypeID == 8 || itemTypeID == 10)) { // 'letter' and 'interview' itemTypeIDs - var creators = this.getCreators(); - var authors = []; - var participants = []; - if (creators) { - for each(var creator in creators) { - if ((itemTypeID == 8 && creator.creatorTypeID == 16) || // 'letter'/'recipient' - (itemTypeID == 10 && creator.creatorTypeID == 7)) { // 'interview'/'interviewer' - participants.push(creator); - } - else if ((itemTypeID == 8 && creator.creatorTypeID == 1) || // 'letter'/'author' - (itemTypeID == 10 && creator.creatorTypeID == 6)) { // 'interview'/'interviewee' - authors.push(creator); - } - } - } - - var strParts = []; - - if (includeAuthorAndDate) { - var names = []; - for each(author in authors) { - names.push(author.lastName); - } - - // TODO: Use same logic as getFirstCreatorSQL() (including "et al.") - if (names.length) { - strParts.push(Zotero.localeJoin(names, ', ')); - } - } - - if (participants.length > 0) { - var names = []; - for each(participant in participants) { - names.push(participant.lastName); - } - switch (names.length) { - case 1: - var str = 'oneParticipant'; - break; - - case 2: - var str = 'twoParticipants'; - break; - - case 3: - var str = 'threeParticipants'; - break; - - default: - var str = 'manyParticipants'; - } - strParts.push(Zotero.getString('pane.items.' + itemTypeName + '.' + str, names)); - } - else { - strParts.push(Zotero.getString('itemTypes.' + itemTypeName)); - } - - if (includeAuthorAndDate) { - var d = this.getField('date'); - if (d) { - strParts.push(d); - } - } - - title = '['; - title += Zotero.localeJoin(strParts, '; '); - title += ']'; - } - - return title; -} - - -/* - * Save changes back to database - * - * Returns true on item update or itemID of new item - */ -Zotero.Item.prototype.save = function(){ - if (!this.hasChanged()){ - Zotero.debug('Item ' + this.getID() + ' has not changed', 4); - return false; - } - - // Make sure there are no gaps in the creator indexes - var creators = this.getCreators(); - for (var i=0; i<creators.length; i++){ - if (!creators[i] || (!creators[i].firstName && !creators[i].lastName)){ - var lastCreator = true; - continue; - } - if (lastCreator){ - throw("Creator indices not contiguous or don't start at 0"); - } - } - - // - // Existing item, update - // - if (this.getID()){ - Zotero.debug('Updating database with new item data', 4); - - var itemID = this.getID(); - - try { - Zotero.DB.beginTransaction(); - - // Begin history transaction - Zotero.History.begin('modify-item', this.getID()); - - // - // Primary fields - // - Zotero.History.modify('items', 'itemID', this.getID()); - - var sql = "UPDATE items SET "; - var sql2; - var sqlValues = []; - - if (this._changed.has('itemTypeID')){ - sql += "itemTypeID=?, "; - sqlValues.push({'int':this.getField('itemTypeID')}); - } - - // Always update modified time - sql += "dateModified=CURRENT_TIMESTAMP "; - sql += "WHERE itemID=?"; - sqlValues.push({'int':this.getID()}); - - Zotero.DB.query(sql, sqlValues); - - // - // Creators - // - if (this._changedCreators.length){ - for (orderIndex in this._changedCreators.items){ - Zotero.debug('Creator ' + orderIndex + ' has changed', 4); - - var creator = this.getCreator(orderIndex); - - // Delete at position - Zotero.History.remove('itemCreators', 'itemID-orderIndex', - [this.getID(), orderIndex]); - - sql2 = 'DELETE FROM itemCreators' - + ' WHERE itemID=' + this.getID() - + ' AND orderIndex=' + orderIndex; - Zotero.DB.query(sql2); - - // If empty, move on - if (!creator['firstName'] && !creator['lastName']){ - continue; - } - - // See if this is an existing creator - var creatorID = Zotero.Creators.getID( - creator['firstName'], - creator['lastName'], - creator['fieldMode'] - ); - - // If not, add it - if (!creatorID){ - creatorID = Zotero.Creators.add( - creator['firstName'], - creator['lastName'], - creator['fieldMode'] - ); - Zotero.History.add('creators', 'creatorID', creatorID); - } - - sql = "INSERT INTO itemCreators VALUES (?,?,?,?)"; - - sqlValues = [ - {'int':itemID}, - {'int':creatorID}, - {'int':creator['creatorTypeID']}, - {'int':orderIndex} - ]; - - Zotero.DB.query(sql, sqlValues); - - Zotero.History.add('itemCreators', - 'itemID-creatorID-creatorTypeID', - [this.getID(), creatorID, creator['creatorTypeID']]); - } - } - - - // - // ItemData - // - if (this._changedItemData.length){ - var del = new Array(); - - sql = "SELECT valueID FROM itemDataValues WHERE value=?"; - var valueStatement = Zotero.DB.getStatement(sql); - - sql = "INSERT INTO itemDataValues VALUES (?,?)"; - var insertStatement = Zotero.DB.getStatement(sql); - - sql = "REPLACE INTO itemData VALUES (?,?,?)"; - var replaceStatement = Zotero.DB.getStatement(sql); - - for (fieldID in this._changedItemData.items){ - var value = this.getField(fieldID, true); - if (value) { - // Field exists - if (this._preChangeArray[Zotero.ItemFields.getName(fieldID)]) { - Zotero.History.modify('itemData', 'itemID-fieldID', - [this.getID(), fieldID]); - } - // Field is new - else { - Zotero.History.add('itemData', 'itemID-fieldID', - [this.getID(), fieldID]); - } - - valueStatement.bindUTF8StringParameter(0, value); - if (valueStatement.executeStep()) { - var valueID = valueStatement.getInt32(0); - } - else { - var valueID = null; - } - valueStatement.reset(); - - if (!valueID) { - valueID = Zotero.ID.get('itemDataValues'); - insertStatement.bindInt32Parameter(0, valueID); - - if (Zotero.ItemFields.getID('accessDate') == fieldID - && this.getField(fieldID) == 'CURRENT_TIMESTAMP') { - sql = "INSERT INTO itemDataValues VALUES " - + "(?,CURRENT_TIMESTAMP)"; - Zotero.DB.query(sql, {int: valueID}); - } - else { - // DISABLED - //if (Zotero.ItemFields.isInteger(fieldID)) { - - // If integer not beginning with 0, bind as integer - // - // If this is changed, search.js also needs to - // change - if (value.match(/^[1-9]+[0-9]*$/)) { - // Store as 32-bit signed integer - if (value <= 2147483647) { - insertStatement. - bindInt32Parameter(1, value); - } - // Store as 64-bit signed integer - else if (value < 9223372036800000000) { - insertStatement. - bindInt64Parameter(1, value); - } - // Store as string if larger then 64-bit - else { - insertStatement. - bindUTF8StringParameter(1, value); - } - } - else { - insertStatement. - bindUTF8StringParameter(1, value); - } - try { - insertStatement.execute(); - } - catch (e) { - throw (Zotero.DB.getLastErrorString()); - } - } - } - - replaceStatement.bindInt32Parameter(0, this.getID()); - replaceStatement.bindInt32Parameter(1, fieldID); - replaceStatement.bindInt32Parameter(2, valueID); - - try { - replaceStatement.execute(); - } - catch (e) { - throw (Zotero.DB.getLastErrorString()); - } - } - - // If field changed and is empty, mark row for deletion - else { - del.push(fieldID); - } - } - - insertStatement.reset(); - replaceStatement.reset(); - - // Delete blank fields - if (del.length){ - // Add to history - for (var i in del){ - Zotero.History.remove('itemData', 'itemID-fieldID', - [this.getID(), del[i]]); - } - - sql = 'DELETE from itemData ' - + 'WHERE itemID=' + this.getID() + ' ' - + 'AND fieldID IN (' + del.join() + ")"; - Zotero.DB.query(sql); - } - } - - Zotero.History.commit(); - Zotero.DB.commitTransaction(); - } - catch (e){ - Zotero.History.cancel(); - Zotero.DB.rollbackTransaction(); - throw(e); - } - } - - // - // New item, insert and return id - // - else { - Zotero.debug('Saving data for new item to database'); - - var isNew = true; - var sqlColumns = new Array(); - var sqlValues = new Array(); - - // - // Primary fields - // - var itemID = Zotero.ID.get('items'); - // If available id value, use it -- otherwise we'll use autoincrement - if (itemID) { - sqlColumns.push('itemID'); - sqlValues.push(itemID); - } - - sqlColumns.push('itemTypeID'); - sqlValues.push({'int':this.getField('itemTypeID')}); - - try { - Zotero.DB.beginTransaction(); - - // Begin history transaction - // No associated id yet, so we use false - Zotero.History.begin('add-item', false); - - // - // Primary fields - // - var sql = "INSERT INTO items (" + sqlColumns.join() + ')' - + ' VALUES ('; - // Insert placeholders for bind parameters - for (var i=0; i<sqlValues.length; i++){ - sql += '?,'; - } - sql = sql.substring(0,sql.length-1) + ")"; - - // Save basic data to items table - var lastInsertID = Zotero.DB.query(sql, sqlValues); - if (!itemID) { - itemID = lastInsertID; - } - this._data['itemID'] = itemID; - - Zotero.History.setAssociatedID(itemID); - Zotero.History.add('items', 'itemID', itemID); - - // - // ItemData - // - if (this._changedItemData.length){ - // Use manual bound parameters to speed things up - sql = "SELECT valueID FROM itemDataValues WHERE value=?"; - var valueStatement = Zotero.DB.getStatement(sql); - - sql = "INSERT INTO itemDataValues VALUES (?,?)"; - var insertValueStatement = Zotero.DB.getStatement(sql); - - sql = "INSERT INTO itemData VALUES (?,?,?)"; - var insertStatement = Zotero.DB.getStatement(sql); - - for (fieldID in this._changedItemData.items){ - var value = this.getField(fieldID, true); - if (!value) { - continue; - } - - valueStatement.bindUTF8StringParameter(0, value); - if (valueStatement.executeStep()) { - var valueID = valueStatement.getInt32(0); - } - else { - var valueID = null; - } - valueStatement.reset(); - - if (!valueID) { - valueID = Zotero.ID.get('itemDataValues'); - insertValueStatement.bindInt32Parameter(0, valueID); - - if (Zotero.ItemFields.getID('accessDate') == fieldID - && this.getField(fieldID) == 'CURRENT_TIMESTAMP') { - sql = "INSERT INTO itemDataValues VALUES " - + "(?,CURRENT_TIMESTAMP)"; - Zotero.DB.query(sql, {int: valueID}); - } - else { - if (Zotero.ItemFields.isInteger(fieldID)) { - insertValueStatement. - bindInt32Parameter(1, value); - } - else { - insertValueStatement. - bindUTF8StringParameter(1, value); - } - try { - insertValueStatement.execute(); - } - catch (e) { - throw (Zotero.DB.getLastErrorString()); - } - } - } - - insertStatement.bindInt32Parameter(0, this.getID()); - insertStatement.bindInt32Parameter(1, fieldID); - insertStatement.bindInt32Parameter(2, valueID); - - try { - insertStatement.execute(); - } - catch(e) { - throw(Zotero.DB.getLastErrorString()); - } - - Zotero.History.add('itemData', 'itemID-fieldID', - [itemID, fieldID]); - } - - insertValueStatement.reset(); - insertStatement.reset(); - } - - // - // Creators - // - if (this._changedCreators.length){ - for (orderIndex in this._changedCreators.items){ - var creator = this.getCreator(orderIndex); - - // If empty, skip - if (!creator['firstName'] && !creator['lastName']){ - continue; - } - - // See if this is an existing creator - var creatorID = Zotero.Creators.getID( - creator['firstName'], - creator['lastName'], - creator['fieldMode'] - ); - - // If not, add it - if (!creatorID){ - creatorID = Zotero.Creators.add( - creator['firstName'], - creator['lastName'], - creator['fieldMode'] - ); - Zotero.History.add('creators', 'creatorID', creatorID); - } - - sql = 'INSERT INTO itemCreators VALUES (' - + itemID + ',' + creatorID + ',' - + creator['creatorTypeID'] + ',' + orderIndex - + ")"; - Zotero.DB.query(sql); - - Zotero.History.add('itemCreators', - 'itemID-creatorID-creatorTypeID', - [this.getID(), creatorID, creator['creatorTypeID']]); - } - } - - Zotero.History.commit(); - Zotero.DB.commitTransaction(); - - // Reload collection to update isEmpty, - // in case this was the first item in a collection - Zotero.Collections.reloadAll(); - } - catch (e){ - Zotero.History.cancel(); - Zotero.DB.rollbackTransaction(); - throw(e); - } - } - - Zotero.Items.reload(this.getID()); - - if (isNew){ - Zotero.Notifier.trigger('add', 'item', this.getID()); - return this.getID(); - } - else { - Zotero.Notifier.trigger('modify', 'item', this.getID(), { old: this._preChangeArray }); - return true; - } -} - - -Zotero.Item.prototype.updateDateModified = function(){ - Zotero.DB.query("UPDATE items SET dateModified=CURRENT_TIMESTAMP " - + "WHERE itemID=" + this.getID()); - var date = Zotero.DB.valueQuery("SELECT dateModified FROM items " - + "WHERE itemID=" + this.getID()); - this._data['dateModified'] = date; -} - - -Zotero.Item.prototype.isRegularItem = function(){ - return !(this.isNote() || this.isAttachment()); -} - - -Zotero.Item.prototype.numChildren = function(){ - return this.numNotes() + this.numAttachments(); -} - - -//////////////////////////////////////////////////////// -// -// Methods dealing with note items -// -// save() is not required for note functions -// -//////////////////////////////////////////////////////// -Zotero.Item.prototype.incrementNoteCount = function(){ - this._data['numNotes']++; -} - - -Zotero.Item.prototype.decrementNoteCount = function(){ - this._data['numNotes']--; -} - - -/** -* Determine if an item is a note -**/ -Zotero.Item.prototype.isNote = function(){ - return Zotero.ItemTypes.getName(this.getType())=='note'; -} - - -/** -* Update an item note -* -* Note: This can only be called on notes and attachments -**/ -Zotero.Item.prototype.updateNote = function(text){ - if (!this.isNote() && !this.isAttachment()){ - throw ("updateNote() can only be called on notes and attachments"); - } - - if (!this.getID()){ - throw ("Cannot call updateNote() on unsaved item"); - } - - Zotero.DB.beginTransaction(); - - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - - var title = Zotero.Notes.noteToTitle(text); - - if (this.isNote()){ - var sourceItemID = this.getSource(); - - Zotero.DB.query("REPLACE INTO itemNoteTitles VALUES (?,?)", - [this.getID(), {string: title}]); - } - - if (sourceItemID) - { - var sql = "REPLACE INTO itemNotes VALUES (?,?,?)"; - var bindParams = [this.getID(), sourceItemID, {string:text}]; - } - else - { - var sql = "REPLACE INTO itemNotes (note, itemID) VALUES (?,?)"; - var bindParams = [{string:text}, this.getID()]; - } - - var updated = Zotero.DB.query(sql, bindParams); - if (updated){ - this.updateDateModified(); - Zotero.DB.commitTransaction(); - - this._noteText = text ? text : ''; - this._noteTitle = title ? title : ''; - - Zotero.Notifier.trigger('modify', 'item', this.getID(), notifierData); - } - else { - Zotero.DB.commitTransaction(); - } -} - - -/* - * Update the cached value of the note - */ -Zotero.Item.prototype.updateNoteCache = function(text, title) { - this._noteText = text ? text : ''; - this._noteTitle = title ? title : ''; -} - - -Zotero.Item.prototype.setSource = function(sourceItemID){ - if (this.isNote()){ - var type = 'note'; - var Type = 'Note'; - } - else if (this.isAttachment()){ - var type = 'attachment'; - var Type = 'Attachment'; - } - else { - throw ("setSource() can only be called on items of type 'note' or 'attachment'"); - } - - if (!this.getID()){ - throw ("Cannot call setSource() on unsaved " + type); - } - - Zotero.DB.beginTransaction(); - - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - - var newItem = Zotero.Items.get(sourceItemID); - // FK check - if (newItem) { - if (sourceItemID) { - var newItemNotifierData = {}; - newItemNotifierData[newItem.id] = { old: newItem.toArray() }; - } - else { - Zotero.DB.rollbackTransaction(); - throw ("Cannot set " + type + " source to invalid item " + sourceItemID); - } - } - - var oldSourceItemID = this.getSource(); - - if (oldSourceItemID==sourceItemID){ - Zotero.debug(Type + " source hasn't changed", 4); - Zotero.DB.commitTransaction(); - return false; - } - - var oldItem = Zotero.Items.get(oldSourceItemID); - if (oldSourceItemID && oldItem) { - var oldItemNotifierData = {}; - oldItemNotifierData[oldItem.id] = { old: oldItem.toArray() }; - } - else { - var oldItemNotifierData = null; - Zotero.debug("Old source item " + oldSourceItemID + "didn't exist in setSource()", 2); - } - - // If this was an independent item, remove from any collections where it - // existed previously and add source instead if there is one - if (!oldSourceItemID){ - var sql = "SELECT collectionID FROM collectionItems WHERE itemID=?"; - var changedCollections = Zotero.DB.columnQuery(sql, this.getID()); - if (changedCollections){ - if (sourceItemID){ - var sql = "UPDATE OR REPLACE collectionItems " - + "SET itemID=? WHERE itemID=?"; - Zotero.DB.query(sql, [sourceItemID, this.getID()]); - } - else { - var sql = "DELETE FROM collectionItems WHERE itemID=?"; - Zotero.DB.query(sql, this.getID()); - } - } - } - - var sql = "UPDATE item" + Type + "s SET sourceItemID=? WHERE itemID=?"; - var bindParams = [sourceItemID ? {int:sourceItemID} : null, this.getID()]; - Zotero.DB.query(sql, bindParams); - this.updateDateModified(); - Zotero.DB.commitTransaction(); - - Zotero.Notifier.trigger('modify', 'item', this.getID(), notifierData); - - // Update the counts of the previous and new sources - if (oldItem){ - switch (type){ - case 'note': - oldItem.decrementNoteCount(); - break; - case 'attachment': - oldItem.decrementAttachmentCount(); - break; - } - Zotero.Notifier.trigger('modify', 'item', oldSourceItemID, oldItemNotifierData); - } - - if (newItem){ - switch (type){ - case 'note': - newItem.incrementNoteCount(); - break; - case 'attachment': - newItem.incrementAttachmentCount(); - break; - } - Zotero.Notifier.trigger('modify', 'item', sourceItemID, newItemNotifierData); - } - - return true; -} - - -/** -* Returns number of notes in item -**/ -Zotero.Item.prototype.numNotes = function(){ - if (this.isNote()){ - throw ("numNotes() cannot be called on items of type 'note'"); - } - - if (!this.getID()){ - return 0; - } - - return this._data['numNotes']; -} - - -/** -* Get the first line of the note for display in the items list -**/ -Zotero.Item.prototype.getNoteTitle = function(){ - if (!this.isNote() && !this.isAttachment()){ - throw ("getNoteTitle() can only be called on notes and attachments"); - } - - if (this._noteTitle !== null){ - return this._noteTitle; - } - - var sql = "SELECT title FROM itemNoteTitles WHERE itemID=" + this.getID(); - var title = Zotero.DB.valueQuery(sql); - - this._noteTitle = title ? title : ''; - - return title ? title : ''; -} - - -/** -* Get the text of an item note -**/ -Zotero.Item.prototype.getNote = function(){ - if (!this.isNote() && !this.isAttachment()){ - throw ("getNote() can only be called on notes and attachments"); - } - - if (!this.getID()) { - return ''; - } - - // Store access time for later garbage collection - this._noteAccessTime = new Date(); - - if (this._noteText !== null){ - return this._noteText; - } - - var sql = "SELECT note FROM itemNotes WHERE itemID=" + this.getID(); - var note = Zotero.DB.valueQuery(sql); - - this._noteText = note ? note : ''; - - return note ? note : ''; -} - - -/** -* Get the itemID of the source item for a note or file -**/ -Zotero.Item.prototype.getSource = function(){ - if (!this.getID()) { - return false; - } - - if (this.isNote()){ - var Type = 'Note'; - } - else if (this.isAttachment()){ - var Type = 'Attachment'; - } - else { - return false; - } - - var sql = "SELECT sourceItemID FROM item" + Type + "s WHERE itemID=" + this.getID(); - return Zotero.DB.valueQuery(sql); -} - - -/** -* Returns an array of note itemIDs for this item -**/ -Zotero.Item.prototype.getNotes = function(){ - if (this.isNote()){ - throw ("getNotes() cannot be called on items of type 'note'"); - } - - if (!this.getID()){ - return []; - } - - // DEBUG: Not just using itemNoteTitles just in case something went wrong - // during migration and there's no titles row - // - // TODO: move titles into itemNotes table - var sql = "SELECT N.itemID, title FROM itemNotes N NATURAL JOIN items " - + "LEFT JOIN itemNoteTitles USING (itemID) WHERE sourceItemID=" + this.getID(); - - if (Zotero.Prefs.get('sortNotesChronologically')) { - sql += " ORDER BY dateAdded"; - return Zotero.DB.columnQuery(sql); - } - - var notes = Zotero.DB.query(sql); - if (!notes) { - return false; - } - - // Sort by title - var collation = Zotero.getLocaleCollation(); - var f = function (a, b) { - var aTitle = Zotero.Items.getSortTitle(a.title); - var bTitle = Zotero.Items.getSortTitle(b.title); - return collation.compareString(1, aTitle, bTitle); - } - - var noteIDs = []; - notes.sort(f); - for each(var note in notes) { - noteIDs.push(note.itemID); - } - return noteIDs; -} - - - - - - - - - -//////////////////////////////////////////////////////// -// -// Methods dealing with attachments -// -// save() is not required for attachment functions -// -/////////////////////////////////////////////////////// -Zotero.Item.prototype.incrementAttachmentCount = function(){ - this._data['numAttachments']++; -} - - -Zotero.Item.prototype.decrementAttachmentCount = function(){ - this._data['numAttachments']--; -} - - -/** -* Determine if an item is an attachment -**/ -Zotero.Item.prototype.isAttachment = function(){ - return Zotero.ItemTypes.getName(this.getType())=='attachment'; -} - - -/** -* Returns number of files in item -**/ -Zotero.Item.prototype.numAttachments = function(){ - if (this.isAttachment()){ - throw ("numAttachments() cannot be called on items of type 'attachment'"); - } - - if (!this.getID()){ - return 0; - } - - return this._data['numAttachments']; -} - - -/** -* Get an nsILocalFile for the attachment, or false if the associated file -* doesn't exist -* -* _row_ is optional itemAttachments row if available to skip query -* -* Note: Always returns false for items with LINK_MODE_LINKED_URL, -* since they have no files -- use getField('url') instead -**/ -Zotero.Item.prototype.getFile = function(row, skipExistsCheck) { - if (!this.isAttachment()){ - throw ("getFile() can only be called on items of type 'attachment'"); - } - - if (!row){ - var sql = "SELECT linkMode, path FROM itemAttachments WHERE itemID=" - + this.getID(); - var row = Zotero.DB.rowQuery(sql); - } - - if (!row){ - throw ('Attachment data not found for item ' + this.getID() - + ' in getFile()'); - } - - // No associated files for linked URLs - if (row['linkMode']==Zotero.Attachments.LINK_MODE_LINKED_URL){ - return false; - } - - var file = Components.classes["@mozilla.org/file/local;1"]. - createInstance(Components.interfaces.nsILocalFile); - - if (row['linkMode']==Zotero.Attachments.LINK_MODE_IMPORTED_URL || - row['linkMode']==Zotero.Attachments.LINK_MODE_IMPORTED_FILE){ - try { - var storageDir = Zotero.getStorageDirectory(); - storageDir.QueryInterface(Components.interfaces.nsILocalFile); - file.setRelativeDescriptor(storageDir, row['path']); - if (!file.exists()){ - throw('Invalid relative descriptor'); - } - } - catch (e){ - // See if this is a persistent path - // (deprecated for imported attachments) - Zotero.debug('Invalid relative descriptor -- trying persistent'); - try { - file.persistentDescriptor = row['path']; - - var storageDir = Zotero.getStorageDirectory(); - storageDir.QueryInterface(Components.interfaces.nsILocalFile); - var path = file.getRelativeDescriptor(storageDir); - - // If valid, convert this to a relative descriptor - if (file.exists()){ - Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?", - [path, this.getID()]); - } - } - catch (e){ - Zotero.debug('Invalid persistent descriptor'); - } - } - } - else { - try { - file.persistentDescriptor = row['path']; - } - catch (e){ - // See if this is an old relative path (deprecated) - Zotero.debug('Invalid persistent descriptor -- trying relative'); - try { - var refDir = (row['linkMode']==this.LINK_MODE_LINKED_FILE) - ? Zotero.getZoteroDirectory() : Zotero.getStorageDirectory(); - file.setRelativeDescriptor(refDir, row['path']); - // If valid, convert this to a persistent descriptor - if (file.exists()){ - Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?", - [file.persistentDescriptor, this.getID()]); - } - } - catch (e){ - Zotero.debug('Invalid relative descriptor'); - } - } - } - - if (!skipExistsCheck && !file.exists()){ - return false; - } - - return file; -} - - -/* - * Rename file associated with an attachment - * - * -1 Destination file exists -- use _force_ to overwrite - * -2 Error renaming - * false Attachment file not found or other error - */ -Zotero.Item.prototype.renameAttachmentFile = function(newName, overwrite) { - var file = this.getFile(); - if (!file) { - return false; - } - - try { - if (file.leafName == newName) { - return true; - } - - var dest = file.parent; - dest.append(newName); - - if (overwrite) { - dest.remove(null); - } - else if (dest.exists()) { - return -1; - } - - file.moveTo(file.parent, newName); - this.relinkAttachmentFile(file); - - return true; - } - catch (e) { - return -2; - } -} - - -Zotero.Item.prototype.relinkAttachmentFile = function(file) { - var linkMode = this.getAttachmentLinkMode(); - - if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { - throw('Cannot relink linked URL in Zotero.Items.relinkAttachmentFile()'); - } - - var path = Zotero.Attachments.getPath(file, linkMode); - - var sql = "UPDATE itemAttachments SET path=? WHERE itemID=?"; - Zotero.DB.query(sql, [path, this.getID()]); -} - - - -/* - * Return a file:/// URL path to files and snapshots - */ -Zotero.Item.prototype.getLocalFileURL = function(){ - if (!this.isAttachment){ - throw ("getLocalFileURL() can only be called on items of type 'attachment'"); - } - - var file = this.getFile(); - - var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"] - .getService(Components.interfaces.nsIFileProtocolHandler); - - return nsIFPH.getURLSpecFromFile(file); -} - - -/** -* Get the link mode of an attachment -* -* Possible return values specified as constants in Zotero.Attachments -* (e.g. Zotero.Attachments.LINK_MODE_LINKED_FILE) -**/ -Zotero.Item.prototype.getAttachmentLinkMode = function(){ - if (!this.isAttachment()){ - throw ("getAttachmentLinkMode() can only be called on items of type 'attachment'"); - } - - if (this._fileLinkMode !== null) { - return this._fileLinkMode; - } - - var sql = "SELECT linkMode FROM itemAttachments WHERE itemID=" + this.getID(); - this._fileLinkMode = Zotero.DB.valueQuery(sql); - return this._fileLinkMode; -} - - -/** -* Get the MIME type of an attachment (e.g. 'text/plain') -**/ -Zotero.Item.prototype.getAttachmentMIMEType = function(){ - if (!this.isAttachment()){ - throw ("getAttachmentMIMEType() can only be called on items of type 'attachment'"); - } - - if (this._fileMIMEType != null) { - return this._fileMIMEType; - } - - var sql = "SELECT mimeType FROM itemAttachments WHERE itemID=" + this.getID(); - this._fileMIMEType = Zotero.DB.valueQuery(sql); - return this._fileMIMEType; -} - - -/** -* Get the character set id of an attachment -**/ -Zotero.Item.prototype.getAttachmentCharset = function(){ - if (!this.isAttachment()){ - throw ("getAttachmentCharset() can only be called on items of type 'attachment'"); - } - - var sql = "SELECT charsetID FROM itemAttachments WHERE itemID=" + this.getID(); - return Zotero.DB.valueQuery(sql); -} - - -/** -* Returns an array of attachment itemIDs that have this item as a source, -* or FALSE if none -**/ -Zotero.Item.prototype.getAttachments = function(){ - if (this.isAttachment()){ - throw ("getAttachments() cannot be called on items of type 'attachment'"); - } - - if (!this.getID()){ - return []; - } - - var sql = "SELECT A.itemID, value AS title FROM itemAttachments A " - + "NATURAL JOIN items I LEFT JOIN itemData ID USING (itemID) " - + "LEFT JOIN itemDataValues IDV " - + "ON (fieldID=110 AND ID.valueID=IDV.valueID) " - + "WHERE sourceItemID=?"; - - if (Zotero.Prefs.get('sortAttachmentsChronologically')) { - sql += " ORDER BY dateAdded"; - return Zotero.DB.columnQuery(sql, this.getID()); - } - - var attachments = Zotero.DB.query(sql, this.getID()); - if (!attachments) { - return false; - } - - // Sort by title - var collation = Zotero.getLocaleCollation(); - var f = function (a, b) { - return collation.compareString(1, a.title, b.title); - } - - var attachmentIDs = []; - attachments.sort(f); - for each(var attachment in attachments) { - attachmentIDs.push(attachment.itemID); - } - return attachmentIDs; -} - - -/* - * Returns the itemID of the latest child snapshot of this item with the - * same URL as the item itself, or false if none - */ -Zotero.Item.prototype.getBestSnapshot = function(){ - if (!this.isRegularItem()){ - throw ("getBestSnapshot() can only be called on regular items"); - } - - if (!this.getField('url')){ - return false; - } - - var sql = "SELECT IA.itemID FROM itemAttachments IA NATURAL JOIN items I " - + "LEFT JOIN itemData ID ON (IA.itemID=ID.itemID AND fieldID=1) " - + "NATURAL JOIN ItemDataValues " - + "WHERE sourceItemID=? AND linkMode=? AND value=? " - + "ORDER BY dateAdded DESC LIMIT 1"; - - return Zotero.DB.valueQuery(sql, [this.getID(), - Zotero.Attachments.LINK_MODE_IMPORTED_URL, {string:this.getField('url')}]); -} - - -// -// Methods dealing with item tags -// -// save() is not required for tag functions -// -Zotero.Item.prototype.addTag = function(tag, type){ - if (!this.getID()){ - throw ('Cannot add tag to unsaved item in Item.addTag()'); - } - - if (!tag){ - Zotero.debug('Not saving empty tag in Item.addTag()', 2); - return false; - } - - if (!type) { - type = 0; - } - - if (type !=0 && type !=1) { - throw ('Invalid tag type in Item.addTag()'); - } - - Zotero.DB.beginTransaction(); - var tagID = Zotero.Tags.getID(tag, type); - var existingTypes = Zotero.Tags.getTypes(tag); - - if (existingTypes) { - // If existing automatic and adding identical user, remove automatic - if (type == 0 && existingTypes.indexOf(1) != -1) { - this.removeTag(Zotero.Tags.getID(tag, 1)); - } - // If existing user and adding automatic, skip - else if (type == 1 && existingTypes.indexOf(0) != -1) { - Zotero.debug('Identical user tag already exists -- skipping automatic tag add'); - Zotero.DB.commitTransaction(); - return false; - } - } - - if (!tagID) { - var tagID = Zotero.Tags.add(tag, type); - } - - try { - var result = this.addTagByID(tagID); - Zotero.DB.commitTransaction(); - } - catch (e) { - Zotero.DB.rollbackTransaction(); - throw (e); - } - - return result ? tagID : false; -} - - -Zotero.Item.prototype.addTags = function (tags, type) { - Zotero.DB.beginTransaction(); - try { - for each(var tag in tags) { - this.addTag(tag, type); - } - Zotero.DB.commitTransaction(); - } - catch (e) { - Zotero.DB.rollbackTransaction(); - throw (e); - } -} - - -Zotero.Item.prototype.addTagByID = function(tagID) { - if (!this.getID()) { - throw ('Cannot add tag to unsaved item in Item.addTagByID()'); - } - - if (!tagID) { - Zotero.debug('Not saving nonexistent tag in Item.addTagByID()', 2); - return false; - } - - var sql = "SELECT COUNT(*) FROM tags WHERE tagID = ?"; - var count = !!Zotero.DB.valueQuery(sql, tagID); - - if (!count) { - throw ('Cannot add invalid tag id ' + tagID + ' in Item.addTagByID()'); - } - - Zotero.DB.beginTransaction(); - - // If INSERT OR IGNORE gave us affected rows, we wouldn't need this... - if (this.hasTag(tagID)) { - Zotero.debug('Item ' + this.getID() + ' already has tag ' + tagID + ' in Item.addTagByID()'); - Zotero.DB.commitTransaction(); - return false; - } - - var sql = "INSERT INTO itemTags VALUES (?,?)"; - Zotero.DB.query(sql, [this.getID(), tagID]); - - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', this.getID()); - Zotero.Notifier.trigger('add', 'item-tag', this.getID() + '-' + tagID); - - return true; -} - -Zotero.Item.prototype.hasTag = function(tagID) { - return this.hasTags(tagID); -} - -/* - * Returns true if the item has one or more of |tagIDs| - * - * |tagIDs| can be an int or array of ints - */ -Zotero.Item.prototype.hasTags = function(tagIDs) { - var tagIDs = Zotero.flattenArguments(tagIDs); - - var sql = "SELECT COUNT(*) FROM itemTags WHERE itemID=? AND tagID IN ("; - var q = []; - var p = [this.getID()]; - for each(var tagID in tagIDs) { - q.push('?'); - p.push(tagID); - } - sql += q.join(); - sql += ")"; - return !!Zotero.DB.valueQuery(sql, p); -} - -Zotero.Item.prototype.getTags = function(){ - if (!this.getID()) { - return false; - } - var sql = "SELECT tagID AS id, tag, tagType AS type FROM tags WHERE tagID IN " - + "(SELECT tagID FROM itemTags WHERE itemID=" + this.getID() + ")"; - - var tags = Zotero.DB.query(sql); - if (!tags) { - return false; - } - - var collation = Zotero.getLocaleCollation(); - tags.sort(function(a, b) { - return collation.compareString(1, a.tag, b.tag); - }); - return tags; -} - -Zotero.Item.prototype.getTagIDs = function(){ - var sql = "SELECT tagID FROM itemTags WHERE itemID=" + this.getID(); - return Zotero.DB.columnQuery(sql); -} - -Zotero.Item.prototype.replaceTag = function(oldTagID, newTag){ - if (!this.getID()){ - throw ('Cannot replace tag on unsaved item'); - } - - if (!newTag){ - Zotero.debug('Not replacing with empty tag', 2); - return false; - } - - Zotero.DB.beginTransaction(); - - var oldTag = Zotero.Tags.getName(oldTagID); - if (oldTag==newTag){ - Zotero.DB.commitTransaction(); - return false; - } - - this.removeTag(oldTagID); - var id = this.addTag(newTag); - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', this.getID()); - Zotero.Notifier.trigger('remove', 'item-tag', this.getID() + '-' + oldTagID); - Zotero.Notifier.trigger('add', 'item-tag', this.getID() + '-' + id); - return id; -} - -Zotero.Item.prototype.removeTag = function(tagID){ - if (!this.getID()){ - throw ('Cannot remove tag on unsaved item'); - } - - Zotero.DB.beginTransaction(); - var sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=?"; - Zotero.DB.query(sql, [this.getID(), { int: tagID }]); - Zotero.Tags.purge(); - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', this.getID()); - Zotero.Notifier.trigger('remove', 'item-tag', this.getID() + '-' + tagID); -} - -Zotero.Item.prototype.removeAllTags = function(){ - if (!this.getID()) { - throw ('Cannot remove tags on unsaved item'); - } - - Zotero.DB.beginTransaction(); - var tagIDs = this.getTagIDs(); - if (!tagIDs) { - Zotero.DB.commitTransaction(); - return; - } - - Zotero.DB.query("DELETE FROM itemTags WHERE itemID=?", this.getID()); - Zotero.Tags.purge(); - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', this.getID()); - - for (var i in tagIDs) { - tagIDs[i] = this.getID() + '-' + tagIDs[i]; - } - Zotero.Notifier.trigger('remove', 'item-tag', tagIDs); -} - - -// -// Methods dealing with See Also links -// -// save() is not required for See Also functions -// -Zotero.Item.prototype.addSeeAlso = function(itemID){ - if (itemID==this.getID()){ - Zotero.debug('Cannot add item as See Also of itself', 2); - return false; - } - - Zotero.DB.beginTransaction(); - - var relatedItem = Zotero.Items.get(itemID); - - if (!relatedItem){ - Zotero.DB.commitTransaction(); - throw ("Cannot add invalid item " + itemID + " as See Also"); - return false; - } - - // Check both ways, using a UNION to take advantage of indexes - var sql = "SELECT (SELECT COUNT(*) FROM itemSeeAlso WHERE itemID=?1 AND " - + "linkedItemID=?2) + (SELECT COUNT(*) FROM itemSeeAlso WHERE " - + "linkedItemID=?1 AND itemID=?2)"; - if (Zotero.DB.valueQuery(sql, [this.getID(), itemID])){ - Zotero.DB.commitTransaction(); - Zotero.debug("Item " + itemID + " already linked", 2); - return false; - } - - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - notifierData[relatedItem.id] = { old: relatedItem.toArray() }; - - var sql = "INSERT INTO itemSeeAlso VALUES (?,?)"; - Zotero.DB.query(sql, [this.getID(), {int:itemID}]); - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', [this.getID(), itemID], notifierData); - return true; -} - -Zotero.Item.prototype.removeSeeAlso = function(itemID){ - if (!this.getID()) { - throw ('Cannot remove related item of unsaved item'); - } - - Zotero.DB.beginTransaction(); - - var relatedItem = Zotero.Items.get(itemID); - if (!relatedItem) { - Zotero.DB.commitTransaction(); - throw ("Cannot remove invalid item " + itemID + " as See Also"); - return false; - } - - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - notifierData[relatedItem.id] = { old: relatedItem.toArray() }; - - var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?"; - Zotero.DB.query(sql, [this.getID(), itemID]); - var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?"; - Zotero.DB.query(sql, [itemID, this.getID()]); - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', [this.getID(), itemID], notifierData); -} - -Zotero.Item.prototype.removeAllRelated = function() { - if (!this.getID()) { - throw ('Cannot remove related items of unsaved item'); - } - - Zotero.DB.beginTransaction(); - var relateds = this.getSeeAlso(); - if (!relateds) { - Zotero.DB.commitTransaction(); - return; - } - - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - - for each(var id in relateds) { - var item = Zotero.Items.get(id); - if (item) { - notifierData[item.id] = { old: item.toArray() }; - } - } - - Zotero.DB.query("DELETE FROM itemSeeAlso WHERE itemID=?", this.getID()); - Zotero.DB.query("DELETE FROM itemSeeAlso WHERE linkedItemID=?", this.getID()); - Zotero.DB.commitTransaction(); - - var ids = [this.getID()].concat(relateds); - - Zotero.Notifier.trigger('modify', 'item', ids, notifierData); -} - -Zotero.Item.prototype.getSeeAlso = function(){ - if (!this.getID()) { - return false; - } - // Check both ways, using a UNION to take advantage of indexes - var sql ="SELECT linkedItemID FROM itemSeeAlso WHERE itemID=?1 UNION " - + "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?1"; - return Zotero.DB.columnQuery(sql, this.getID()); -} - - -Zotero.Item.prototype.getImageSrc = function() { - var itemType = Zotero.ItemTypes.getName(this.getType()); - if (itemType == 'attachment') { - var linkMode = this.getAttachmentLinkMode(); - - // Quick hack to use PDF icon for imported files and URLs -- - // extend to support other document types later - if ((linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE || - linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) && - this.getAttachmentMIMEType() == 'application/pdf') { - itemType += '-pdf'; - } - else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) { - itemType += "-file"; - } - else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { - itemType += "-link"; - } - else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) { - itemType += "-snapshot"; - } - else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { - itemType += "-web-link"; - } - } - - return Zotero.ItemTypes.getImageSrc(itemType); -} - - -Zotero.Item.prototype.clone = function() { - if (!this.getID()) { - throw ('Cannot clone unsaved item in Zotero.Item.clone()'); - } - - if (this.isAttachment()) { - throw ('Cloning attachment items not supported in Zotero.Item.clone()'); - } - - Zotero.DB.beginTransaction(); - - var obj = this.toArray(); - - // Note - if (this.isNote()) { - var newItemID = Zotero.Notes.add(this.getNote(), this.getSource()); - var newItem = Zotero.Items.get(newItemID); - } - - // Regular item - else { - var itemTypeID = this.getType(); - var newItem = new Zotero.Item(itemTypeID); - - for (var i in obj) { - switch (i) { - case 'creators': - var i = 0; - for each(var c in obj.creators) { - newItem.setCreator(i, c.firstName, c.lastName, - c.creatorType, c.fieldMode ? c.fieldMode : null); - i++; - } - continue; - } - - var fieldID = Zotero.ItemFields.getID(i); - if (fieldID && Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) { - newItem.setField(i, obj[i]); - } - } - - newItem.save(); - } - - if (obj.tags) { - for each(var tag in obj.tags) { - newItem.addTagByID(tag.id); - } - } - - if (obj.seeAlso) { - for each(var id in obj.seeAlso) { - newItem.addSeeAlso(id) - } - } - - Zotero.DB.commitTransaction(); - return newItem.getID(); -} - - -/** -* Delete item from database and clear from Zotero.Items internal array -* -* Items.erase() should be used instead of this -**/ -Zotero.Item.prototype.erase = function(deleteChildren){ - if (!this.getID()){ - return false; - } - - Zotero.debug('Deleting item ' + this.getID()); - - var changedItems = []; - var changedItemsNotifierData = {}; - - Zotero.DB.beginTransaction(); - - var deletedItemNotifierData = {}; - deletedItemNotifierData[this.id] = { old: this.toArray() }; - - // Remove item from parent collections - var parentCollectionIDs = this.getCollections(); - if (parentCollectionIDs){ - for (var i=0; i<parentCollectionIDs.length; i++){ - Zotero.Collections.get(parentCollectionIDs[i]).removeItem(this.getID()); - } - } - - // Note - if (this.isNote()){ - // Decrement note count of source items - var sql = "SELECT sourceItemID FROM itemNotes WHERE itemID=" + this.getID(); - var sourceItemID = Zotero.DB.valueQuery(sql); - if (sourceItemID){ - var sourceItem = Zotero.Items.get(sourceItemID); - changedItemsNotifierData[sourceItem.id] = { old: sourceItem.toArray() }; - sourceItem.decrementNoteCount(); - changedItems.push(sourceItemID); - } - } - // Attachment - else if (this.isAttachment()){ - // Decrement file count of source items - var sql = "SELECT sourceItemID FROM itemAttachments WHERE itemID=" + this.getID(); - var sourceItemID = Zotero.DB.valueQuery(sql); - if (sourceItemID){ - var sourceItem = Zotero.Items.get(sourceItemID); - changedItemsNotifierData[sourceItem.id] = { old: sourceItem.toArray() }; - sourceItem.decrementAttachmentCount(); - changedItems.push(sourceItemID); - } - - // Delete associated files - var linkMode = this.getAttachmentLinkMode(); - switch (linkMode){ - // Link only -- nothing to delete - case Zotero.Attachments.LINK_MODE_LINKED_URL: - break; - default: - try { - var file = Zotero.getStorageDirectory(); - file.append(this.getID()); - if (file.exists()){ - file.remove(true); - } - } - catch (e) { - Components.utils.reportError(e); - } - } - } - - // Regular item - - // If flag given, delete child notes and files - else if (deleteChildren){ - var sql = "SELECT itemID FROM itemNotes WHERE sourceItemID=?1 UNION " - + "SELECT itemID FROM itemAttachments WHERE sourceItemID=?1"; - var toDelete = Zotero.DB.columnQuery(sql, [this.getID()]); - - if (toDelete) { - for (var i in toDelete){ - var obj = Zotero.Items.get(toDelete[i]); - obj.erase(true); - } - } - } - - // Otherwise just unlink any child notes or files without deleting - else { - // Notes - var sql = "SELECT itemID FROM itemNotes WHERE sourceItemID=" + this.getID(); - var childNotes = Zotero.DB.columnQuery(sql); - if (childNotes){ - for each(var id in childNotes) { - var i = Zotero.Items.get(id); - changedItemsNotifierData[i.id] = { old: i.toArray() }; - } - changedItems.push(childNotes); - } - var sql = "UPDATE itemNotes SET sourceItemID=NULL WHERE sourceItemID=" - + this.getID(); - Zotero.DB.query(sql); - - // Attachments - var sql = "SELECT itemID FROM itemAttachments WHERE sourceItemID=" + this.getID(); - var childAttachments = Zotero.DB.columnQuery(sql); - if (childAttachments){ - for each(var id in childAttachments) { - var i = Zotero.Items.get(id); - changedItemsNotifierData[i.id] = { old: i.toArray() }; - } - changedItems.push(childAttachments); - } - var sql = "UPDATE itemAttachments SET sourceItemID=NULL WHERE sourceItemID=" - + this.getID(); - Zotero.DB.query(sql); - } - - // Flag See Also links for notification - var relateds = this.getSeeAlso(); - if (relateds){ - for each(var id in relateds) { - var i = Zotero.Items.get(id); - if (!changedItemsNotifierData[i.id]) { - changedItemsNotifierData[i.id] = { old: i.toArray() }; - } - } - changedItems = changedItems.concat(relateds); - } - - // Clear fulltext cache - if (this.isAttachment()) { - Zotero.Fulltext.clearItemWords(this.getID()); - //Zotero.Fulltext.clearItemContent(this.getID()); - } - - sql = 'DELETE FROM itemCreators WHERE itemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM itemNotes WHERE itemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM itemNoteTitles WHERE itemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM itemAttachments WHERE itemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM itemSeeAlso WHERE itemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM itemSeeAlso WHERE linkedItemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM itemTags WHERE itemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM itemData WHERE itemID=' + this.getID() + ";\n"; - sql += 'DELETE FROM items WHERE itemID=' + this.getID() + ";\n"; - - Zotero.DB.query(sql); - - try { - Zotero.DB.commitTransaction(); - } - catch (e){ - // On failure, reset count of source items - if (sourceItem){ - if (this.isNote()){ - sourceItem.incrementNoteCount(); - } - else if (this.isAttachment()){ - sourceItem.incrementAttachmentCount(); - } - } - Zotero.DB.rollbackTransaction(); - throw (e); - } - - Zotero.Items.unload(this.getID()); - - // Send notification of changed items - if (changedItems.length){ - Zotero.Notifier.trigger('modify', 'item', changedItems, changedItemsNotifierData); - } - - Zotero.Notifier.trigger('delete', 'item', this.getID(), deletedItemNotifierData); -} - - -Zotero.Item.prototype.isCollection = function(){ - return false; -} - - -/* - * Convert the item data into a multidimensional associative array - * for use by the export functions - * - * Modes: - * - * 1 == e.g. [Letter to Valee] - * 2 == e.g. [Stothard; Letter to Valee; May 8, 1928] - */ -Zotero.Item.prototype.toArray = function(mode) { - if (this.getID() && !this._itemDataLoaded){ - this._loadItemData(); - } - - var arr = []; - - // Primary fields - for (var i in this._data){ - switch (i){ - case 'itemTypeID': - arr['itemType'] = Zotero.ItemTypes.getName(this._data[i]); - break; - - // Skip certain fields - //case 'firstCreator': - case 'numNotes': - case 'numAttachments': - continue; - - case 'firstCreator': - if (!this.isRegularItem()) { - continue; - } - // fall through - - // For the rest, just copy over - default: - arr[i] = this._data[i]; - } - } - - // Item metadata - for (var i in this._itemData){ - arr[Zotero.ItemFields.getName(i)] = this._itemData[i] ? this._itemData[i] : ''; - } - - if (mode == 1 || mode == 2) { - if (!arr.title && - (this.getType() == Zotero.ItemTypes.getID('letter') || - this.getType() == Zotero.ItemTypes.getID('interview'))) { - arr.title = this.getDisplayTitle(mode == 2); - } - } - - if (!this.isNote() && !this.isAttachment()){ - // Creators - arr['creators'] = []; - var creators = this.getCreators(); - for (var i in creators){ - arr['creators'][i] = []; - arr['creators'][i]['firstName'] = creators[i]['firstName']; - arr['creators'][i]['lastName'] = creators[i]['lastName']; - arr['creators'][i]['fieldMode'] = creators[i]['fieldMode']; - // Convert creatorTypeIDs to text - arr['creators'][i]['creatorType'] = - Zotero.CreatorTypes.getName(creators[i]['creatorTypeID']); - } - } - - // Notes - if (this.isNote()) { - arr['note'] = this.getNote(); - if (this.getSource()){ - arr['sourceItemID'] = this.getSource(); - } - } - - // Attachments - if (this.isAttachment()){ - // Attachments can have embedded notes - arr['note'] = this.getNote(); - - if (this.getSource()){ - arr['sourceItemID'] = this.getSource(); - } - } - - // Attach children of regular items - if (this.isRegularItem()){ - // Append attached notes - arr['notes'] = []; - var notes = this.getNotes(); - for (var i in notes){ - var note = Zotero.Items.get(notes[i]); - arr['notes'].push(note.toArray()); - } - - arr['attachments'] = []; - var attachments = this.getAttachments(); - for (var i in attachments){ - var attachment = Zotero.Items.get(attachments[i]); - arr['attachments'].push(attachment.toArray()); - } - } - - arr['tags'] = this.getTags(); - arr['seeAlso'] = this.getSeeAlso(); - - return arr; -} - - - -////////////////////////////////////////////////////////////////////////////// -// -// Private Zotero.Item methods -// -////////////////////////////////////////////////////////////////////////////// - -/* - * Load in the creators from the database - */ -Zotero.Item.prototype._loadCreators = function(){ - if (!this.getID()){ - throw ('ItemID not set for item before attempting to load creators'); - } - - var sql = 'SELECT C.creatorID, C.*, creatorTypeID, orderIndex ' - + 'FROM itemCreators IC ' - + 'LEFT JOIN creators C USING (creatorID) ' - + 'WHERE itemID=' + this.getID() + ' ORDER BY orderIndex'; - var creators = Zotero.DB.query(sql); - - this._creatorsLoaded = true; - - if (!creators){ - return true; - } - - this._creators = []; - for (var i=0; i<creators.length; i++){ - this._creators[creators[i]['orderIndex']] = { - firstName: creators[i]['firstName'], - lastName: creators[i]['lastName'], - creatorTypeID: creators[i]['creatorTypeID'], - fieldMode: creators[i]['fieldMode'] - }; - } - - return true; -} - - -/* - * Load in the field data from the database - */ -Zotero.Item.prototype._loadItemData = function(){ - if (!this.getID()){ - throw ('ItemID not set for object before attempting to load data'); - } - - var sql = "SELECT fieldID, value FROM itemData NATURAL JOIN itemDataValues " - + "WHERE itemID=?"; - var fields = Zotero.DB.query(sql, this.getID()); - - var itemTypeFields = Zotero.ItemFields.getItemTypeFields(this.getType()); - - for each(var field in fields) { - this.setField(field['fieldID'], field['value'], true); - } - - // Mark nonexistent fields as loaded - for each(var fieldID in itemTypeFields) { - if (this._itemData[fieldID] === null) { - this._itemData[fieldID] = false; - } - } - - this._itemDataLoaded = true; -} - - - -/* - * Primary interface for accessing Zotero items - */ -Zotero.Items = new function(){ - // Privileged methods - this.get = get; - this.getAll = getAll; - this.add = add; - this.reload = reload; - this.cacheFields = cacheFields; - this.erase = erase; - this.purge = purge; - this.unload = unload; - this.getFirstCreatorSQL = getFirstCreatorSQL; - this.getSortTitle = getSortTitle; - - // Private members - var _items = []; - var _itemsLoaded = false; - var _cachedFields = []; - var _firstCreatorSQL = ''; - - - /* - * Retrieves (and loads, if necessary) an arbitrary number of items - * - * Can be passed ids as individual parameters or as an array of ids, or both - * - * If only one argument and it's an id, return object directly; - * otherwise, return array - */ - function get(){ - var toLoad = new Array(); - var loaded = new Array(); - - if (!arguments[0]){ - Zotero.debug('No arguments provided to Items.get()'); - return false; - } - - var ids = Zotero.flattenArguments(arguments); - - for (var i=0; i<ids.length; i++){ - // Check if already loaded - if (!_items[ids[i]]){ - toLoad.push(ids[i]); - } - } - - // New items to load - if (toLoad.length){ - _load(toLoad); - } - - // If single id, return the object directly - if (arguments[0] && typeof arguments[0]!='object' - && typeof arguments[1]=='undefined'){ - if (!_items[arguments[0]]) { - Zotero.debug("Item " + arguments[0] + " doesn't exist", 2); - return false; - } - return _items[arguments[0]]; - } - - // Otherwise, build return array - for (i=0; i<ids.length; i++){ - if (!_items[ids[i]]){ - Zotero.debug("Item " + ids[i] + " doesn't exist", 2); - continue; - } - loaded.push(_items[ids[i]]); - } - - return loaded; - } - - - /* - * Returns all items in the database - * - * If |onlyTopLevel|, don't include child items - */ - function getAll(onlyTopLevel) { - var sql = 'SELECT A.itemID FROM items A'; - if (onlyTopLevel) { - sql += ' LEFT JOIN itemNotes B USING (itemID) ' - + 'LEFT JOIN itemAttachments C ON (C.itemID=A.itemID) ' - + 'WHERE B.sourceItemID IS NULL AND C.sourceItemID IS NULL'; - } - - var ids = Zotero.DB.columnQuery(sql); - return this.get(ids); - } - - - /* - * Create a new item with optional metadata and pass back the primary reference - * - * Using "var item = new Zotero.Item()" and "item.save()" directly results - * in an orphaned reference to the created item. If other code retrieves the - * new item with Zotero.Items.get() and modifies it, the original reference - * will not reflect the changes. - * - * Using this method avoids the need to call Zotero.Items.get() after save() - * in order to get the primary item reference. Since it accepts metadata - * as a JavaScript object, it also offers a simpler syntax than - * item.setField() and item.setCreator(). - * - * Callers with no need for an up-to-date reference after save() (or who - * don't mind doing an extra Zotero.Items.get()) can use Zotero.Item - * directly if they prefer. - * - * Sample usage: - * - * var data = { - * title: "Shakespeare: The Invention of the Human", - * publisher: "Riverhead Hardcover", - * date: '1998-10-26', - * ISBN: 1573221201, - * pages: 745, - * creators: [ - * ['Harold', 'Bloom', 'author'] - * ] - * }; - * var item = Zotero.Items.add('book', data); - */ - function add(itemTypeOrID, data) { - var item = new Zotero.Item(itemTypeOrID); - for (var field in data){ - if (field == 'creators') { - var i = 0; - for each(var creator in data.creators) { - // TODO: accept format from toArray() - item.setCreator(i, creator[0], creator[1], creator[2], creator[3] ? creator[3] : null); - i++; - } - } - else { - item.setField(field, data[field]); - } - } - var id = item.save(); - - return this.get(id); - } - - - /* - * Reloads data for specified items into internal array - * - * Can be passed ids as individual parameters or as an array of ids, or both - */ - function reload(){ - if (!arguments[0]){ - return false; - } - - var ids = Zotero.flattenArguments(arguments); - Zotero.debug('Reloading ' + ids); - _load(ids); - - return true; - } - - - function cacheFields(fields, items) { - Zotero.debug("Caching fields [" + fields.join() + "]" - + (items ? " for " + items + " items" : '')); - _load(items); - - var primaryFields = []; - var fieldIDs = []; - for each(var field in fields) { - // Check if field already cached - if (_cachedFields.indexOf(field) != -1) { - continue; - } - - _cachedFields.push(field); - - if (Zotero.Item.prototype.isPrimaryField(field)) { - primaryFields.push(field); - } - else { - fieldIDs.push(Zotero.ItemFields.getID(field)); - if (Zotero.ItemFields.isBaseField(field)) { - fieldIDs = fieldIDs.concat(Zotero.ItemFields.getTypeFieldsFromBase(field)); - } - } - } - - if (primaryFields.length) { - var sql = "SELECT itemID, " + primaryFields.join(', ') + " FROM items"; - if (items) { - sql += " WHERE itemID IN (" + items.join() + ")"; - } - var rows = Zotero.DB.query(sql); - for each(var row in rows) { - //Zotero.debug('Calling loadFromRow for item ' + row['itemID']); - _items[row['itemID']].loadFromRow(row); - } - } - - // All fields already cached - if (!fieldIDs.length) { - return; - } - - var allItemIDs = Zotero.DB.columnQuery("SELECT itemID FROM items"); - var itemFieldsCached = {}; - - var sql = "SELECT itemID, fieldID, value FROM itemData " - + "NATURAL JOIN itemDataValues WHERE "; - if (items) { - sql += "itemID IN (" + items.join() + ") AND "; - } - sql += "fieldID IN (" + fieldIDs.join() + ")"; - - var itemDataRows = Zotero.DB.query(sql); - for each(var row in itemDataRows) { - //Zotero.debug('Setting field for item ' + row['itemID']); - if (_items[row['itemID']]) { - _items[row['itemID']].setField(row['fieldID'], row['value'], true); - } - else { - if (!missingItems) { - var missingItems = {}; - } - if (!missingItems[row['itemID']]) { - missingItems[row['itemID']] = true; - Components.utils.reportError("itemData row references nonexistent item " + row['itemID']); - } - } - - if (!itemFieldsCached[row['itemID']]) { - itemFieldsCached[row['itemID']] = {}; - } - itemFieldsCached[row['itemID']][row['fieldID']] = true; - } - - // If 'title' is one of the fields, load in noteTitles - if (fields.indexOf('title') != -1) { - var titleFieldID = Zotero.ItemFields.getID('title'); - var sql = 'SELECT itemID, title FROM itemNoteTitles'; - if (items) { - sql += " WHERE itemID IN (" + items.join() + ")"; - } - - var rows = Zotero.DB.query(sql); - for each(var row in rows) { - //Zotero.debug('Setting title for note ' + row['itemID']); - if (_items[row['itemID']]) { - _items[row['itemID']].setField(titleFieldID, row['title'], true); - } - else { - if (!missingItems) { - var missingItems = {}; - } - if (!missingItems[row['itemID']]) { - missingItems[row['itemID']] = true; - Components.utils.reportError("itemData row references nonexistent item " + row['itemID']); - } - } - } - } - - // Set nonexistent fields in the cache list to false (instead of null) - for each(var itemID in allItemIDs) { - for each(var fieldID in fieldIDs) { - if (Zotero.ItemFields.isValidForType(fieldID, _items[itemID].getType())) { - if (!itemFieldsCached[itemID] || !itemFieldsCached[itemID][fieldID]) { - //Zotero.debug('Setting field ' + fieldID + ' to false for item ' + itemID); - _items[itemID].setField(fieldID, false, true); - } - } - } - } - } - - - /** - * Delete item(s) from database and clear from internal array - * - * If _eraseChildren_ is true, erase child items as well - **/ - function erase(ids, eraseChildren){ - var unlock = Zotero.Notifier.begin(true); - Zotero.UnresponsiveScriptIndicator.disable(); - try { - Zotero.DB.beginTransaction(); - for each(var id in ids) { - var item = this.get(id); - if (!item) { - Zotero.debug('Item ' + id + ' does not exist in Items.erase()!', 1); - Zotero.Notifier.trigger('delete', 'item', id, [false]); - continue; - } - item.erase(eraseChildren); // calls unload() - item = undefined; - } - this.purge(); - Zotero.DB.commitTransaction(); - } - catch (e) { - Zotero.DB.rollbackTransaction(); - throw (e); - } - finally { - Zotero.Notifier.commit(unlock); - Zotero.UnresponsiveScriptIndicator.enable(); - } - } - - - /* - * Clear entries from various tables that no longer exist - * - * This is called automatically by Items.erase() but must be called - * manually after Item.erase() - */ - function purge() { - Zotero.Creators.purge(); - Zotero.Tags.purge(); - Zotero.Fulltext.purgeUnusedWords(); - - // Purge unused values - var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN " - + "(SELECT valueID FROM itemData)"; - Zotero.DB.query(sql); - } - - - /** - * Clear item from internal array (used by Zotero.Item.erase()) - **/ - function unload(id){ - delete _items[id]; - } - - - /* - * Generate SQL to retrieve firstCreator field - * - * Why do we do this entirely in SQL? Because we're crazy. Crazy like foxes. - */ - function getFirstCreatorSQL() { - if (_firstCreatorSQL) { - return _firstCreatorSQL; - } - - /* This whole block is to get the firstCreator */ - var localizedAnd = Zotero.getString('general.and'); - var sql = "COALESCE(" + - // First try for primary creator types - "CASE (" + - "SELECT COUNT(*) FROM itemCreators IC " + - "LEFT JOIN itemTypeCreatorTypes ITCT " + - "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + - "WHERE itemID=I.itemID AND primaryField=1" + - ") " + - "WHEN 0 THEN NULL " + - "WHEN 1 THEN (" + - "SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + - "LEFT JOIN itemTypeCreatorTypes ITCT " + - "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + - "WHERE itemID=I.itemID AND primaryField=1" + - ") " + - "WHEN 2 THEN (" + - "SELECT " + - "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + - "LEFT JOIN itemTypeCreatorTypes ITCT " + - "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + - "WHERE itemID=I.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + - " || ' " + localizedAnd + " ' || " + - "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + - "LEFT JOIN itemTypeCreatorTypes ITCT " + - "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + - "WHERE itemID=I.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" + - ") " + - "ELSE (" + - "SELECT " + - "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + - "LEFT JOIN itemTypeCreatorTypes ITCT " + - "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=I.itemTypeID) " + - "WHERE itemID=I.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + - " || ' et al.' " + - ") " + - "END, " + - - // Then try editors - "CASE (" + - "SELECT COUNT(*) FROM itemCreators " + - "NATURAL JOIN creatorTypes WHERE itemID=I.itemID AND creatorTypeID IN (3)" + - ") " + - "WHEN 0 THEN NULL " + - "WHEN 1 THEN (" + - "SELECT lastName FROM itemCreators NATURAL JOIN creators " + - "WHERE itemID=I.itemID AND creatorTypeID IN (3)" + - ") " + - "WHEN 2 THEN (" + - "SELECT " + - "(SELECT lastName FROM itemCreators NATURAL JOIN creators WHERE itemID=I.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + - " || ' " + localizedAnd + " ' || " + - "(SELECT lastName FROM itemCreators NATURAL JOIN creators WHERE itemID=I.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1,1) " + - ") " + - "ELSE (" + - "SELECT " + - "(SELECT lastName FROM itemCreators NATURAL JOIN creators WHERE itemID=I.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + - " || ' et al.' " + - ") " + - "END, " + - - // Then try contributors - "CASE (" + - "SELECT COUNT(*) FROM itemCreators " + - "NATURAL JOIN creatorTypes WHERE itemID=I.itemID AND creatorTypeID IN (2)" + - ") " + - "WHEN 0 THEN NULL " + - "WHEN 1 THEN (" + - "SELECT lastName FROM itemCreators NATURAL JOIN creators " + - "WHERE itemID=I.itemID AND creatorTypeID IN (2)" + - ") " + - "WHEN 2 THEN (" + - "SELECT " + - "(SELECT lastName FROM itemCreators NATURAL JOIN creators WHERE itemID=I.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + - " || ' " + localizedAnd + " ' || " + - "(SELECT lastName FROM itemCreators NATURAL JOIN creators WHERE itemID=I.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1,1) " + - ") " + - "ELSE (" + - "SELECT " + - "(SELECT lastName FROM itemCreators NATURAL JOIN creators WHERE itemID=I.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + - " || ' et al.' " + - ") " + - "END" + - ") AS firstCreator"; - - _firstCreatorSQL = sql; - return sql; - } - - - function getSortTitle(title) { - return title.replace(/^[\[\'\"](.*)[\'\"\]]?$/, '$1') - } - - - function _load() { - if (!arguments[0] && _itemsLoaded) { - return; - } - - // Should be the same as parts in Zotero.Item.loadFromID - var sql = 'SELECT I.itemID, I.itemTypeID, I.dateAdded, I.dateModified, ' - + getFirstCreatorSQL() + ', ' - + "(SELECT COUNT(*) FROM itemNotes WHERE sourceItemID=I.itemID) AS numNotes, " - + "(SELECT COUNT(*) FROM itemAttachments WHERE sourceItemID=I.itemID) AS numAttachments " - + 'FROM items I WHERE 1'; - - if (arguments[0]){ - sql += ' AND I.itemID IN (' + Zotero.join(arguments,',') + ')'; - } - var itemsRows = Zotero.DB.query(sql); - - for each(var row in itemsRows) { - // Item doesn't exist -- create new object and stuff in array - if (!_items[row['itemID']]){ - var item = new Zotero.Item(); - item.loadFromRow(row, true); - _items[row['itemID']] = item; - } - // Existing item -- reload in place - else { - _items[row['itemID']].loadFromRow(row, true); - } - } - - if (!arguments[0]) { - _itemsLoaded = true; - _cachedFields = ['itemID', 'itemTypeID', 'dateAdded', 'dateModified', - 'firstCreator', 'numNotes', 'numAttachments', 'numChildren']; - } - } -} - - - - -Zotero.Notes = new function(){ - this.add = add; - this.noteToTitle = noteToTitle; - - this.__defineGetter__("MAX_TITLE_LENGTH", function() { return 80; }); - - /** - * Create a new item of type 'note' and add the note text to the itemNotes table - * - * Returns the itemID of the new note item - **/ - function add(text, sourceItemID){ - Zotero.DB.beginTransaction(); - - if (sourceItemID){ - var sourceItem = Zotero.Items.get(sourceItemID); - if (!sourceItem){ - Zotero.DB.commitTransaction(); - throw ("Cannot set note source to invalid item " + sourceItemID); - } - if (!sourceItem.isRegularItem()){ - Zotero.DB.commitTransaction(); - throw ("Cannot set note source to a note or attachment (" + sourceItemID + ")"); - } - } - - var note = new Zotero.Item('note'); - note.save(); - - var title = text ? this.noteToTitle(text) : ''; - var sql = "INSERT INTO itemNoteTitles VALUES (?,?)"; - Zotero.DB.query(sql, [note.getID(), title]); - - var sql = "INSERT INTO itemNotes VALUES (?,?,?)"; - var bindParams = [ - note.getID(), - (sourceItemID ? {int:sourceItemID} : null), - {string: text ? text : ''} - ]; - Zotero.DB.query(sql, bindParams); - Zotero.DB.commitTransaction(); - - // Switch to Zotero.Items version - var note = Zotero.Items.get(note.getID()); - note.updateNoteCache(text, title); - - if (sourceItemID){ - var notifierData = {}; - notifierData[sourceItem.id] = { old: sourceItem.toArray() }; - sourceItem.incrementNoteCount(); - Zotero.Notifier.trigger('modify', 'item', sourceItemID, notifierData); - } - - return note.getID(); - } - - - /** - * Return first line (or first MAX_LENGTH characters) of note content - **/ - function noteToTitle(text) { - var max = this.MAX_TITLE_LENGTH; - - var t = text.substring(0, max); - var ln = t.indexOf("\n"); - if (ln>-1 && ln<max) { - t = t.substring(0, ln); - } - return t; - } -} - - - - -/* - * Constructor for Collection object - * - * Generally should be called from Zotero.Collections rather than directly - */ -Zotero.Collection = function(){ - this._init(); -} - -Zotero.Collection.prototype._init = function(){ - // - // Public members for access by public methods -- do not access directly - // - this._id = null; - this._name = null; - this._parent = null; - this._hasChildCollections = false; - this._hasChildItems = false; - this._childItems = new Zotero.Hash(); - this._childItemsLoaded = false; -} - -/* - * Build collection from database - */ -Zotero.Collection.prototype.loadFromID = function(id){ - // Should be same as query in Zotero.Collections, just with collectionID - var sql = "SELECT collectionID, collectionName, parentCollectionID, " - + "(SELECT COUNT(*) FROM collections WHERE " - + "parentCollectionID=C.collectionID)!=0 AS hasChildCollections, " - + "(SELECT COUNT(*) FROM collectionItems WHERE " - + "collectionID=C.collectionID)!=0 AS hasChildItems " - + "FROM collections C " - + "WHERE collectionID=" + id; - - var row = Zotero.DB.rowQuery(sql); - this.loadFromRow(row); -} - - -/* - * Populate collection data from a database row - */ -Zotero.Collection.prototype.loadFromRow = function(row){ - this._init(); - this._id = row['collectionID']; - this._name = row['collectionName']; - this._parent = row['parentCollectionID']; - this._hasChildCollections = row['hasChildCollections']; - this._hasChildItems = row['hasChildItems']; - this._loadChildItems(); -} - - -/* - * Deprecated -- use id property - */ -Zotero.Collection.prototype.getID = function(){ - return this._id; -} - - -Zotero.Collection.prototype.__defineGetter__("id", function () { - return this._id; -}); - - -Zotero.Collection.prototype.getName = function(){ - return this._name; -} - -/** -* Returns collectionID of the parent collection -**/ -Zotero.Collection.prototype.getParent = function(){ - return this._parent; -} - - -Zotero.Collection.prototype.isEmpty = function(){ - return !(parseInt(this._hasChildCollections)) && !(parseInt(this._hasChildItems)); -} - -Zotero.Collection.prototype.hasChildCollections = function(){ - return !!(parseInt(this._hasChildCollections)); -} - -Zotero.Collection.prototype.hasChildItems = function(){ - return !!(parseInt(this._hasChildItems)); -} - -/** -* Rename the collection -* -* _name_ is non-empty string -* -* Returns true on success, or false on error -**/ -Zotero.Collection.prototype.rename = function(name){ - if (!name){ - return false; - } - - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - - var sql = "UPDATE collections SET collectionName=? " - + "WHERE collectionID=?"; - Zotero.DB.query(sql, [{'string':name},{'int':this.getID()}]); - this._name = name; - - Zotero.Notifier.trigger('modify', 'collection', this.getID(), notifierData); - return true; -} - - -/** -* Change the parentCollectionID of a collection -* -* Returns TRUE on success, FALSE on error -**/ -Zotero.Collection.prototype.changeParent = function(parent){ - if (!parent){ - parent = null; - } - - var previousParent = this.getParent(); - - if (parent==previousParent){ - Zotero.debug('Collection ' + this.getID() + ' is already in ' - + (parent ? 'collection ' + parent : 'root collection'), 2); - return false; - } - - if (parent && !Zotero.Collections.get(parent)){ - throw('Invalid parentCollectionID ' + parent + ' in changeParent()'); - } - - if (parent && parent==this.getID()){ - Zotero.debug('Cannot move collection into itself!', 2); - return false; - } - - if (parent){ - if (this.hasDescendent('collection', parent)){ - Zotero.debug('Cannot move collection into one of its own ' - + 'descendents!', 2); - return false; - } - } - - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - - var parentParam = parent ? {'int':parent} : {'null':true}; - - var sql = "UPDATE collections SET parentCollectionID=? " - + "WHERE collectionID=?"; - - Zotero.DB.query(sql, [parentParam, {'int':this.getID()}]); - this._parent = parent; - - var notifyIDs = [ - this.getID(), - (previousParent ? previousParent : null), - (parent ? parent : null) - ]; - - Zotero.Collections.reloadAll(); - - Zotero.Notifier.trigger('move', 'collection', notifyIDs, notifierData); - return true; -} - - -/** -* Add an item to the collection -**/ -Zotero.Collection.prototype.addItem = function(itemID){ - Zotero.DB.beginTransaction(); - - if (!Zotero.Items.get(itemID)){ - Zotero.DB.rollbackTransaction(); - throw(itemID + ' is not a valid item id'); - } - - var nextOrderIndex = Zotero.DB.valueQuery("SELECT IFNULL(MAX(orderIndex)+1, 0) " - + "FROM collectionItems WHERE collectionID=" + this._id); - - var sql = "INSERT OR IGNORE INTO collectionItems VALUES " - + "(" + this._id + ", " + itemID + ", " + nextOrderIndex + ")"; - - Zotero.DB.query(sql); - Zotero.DB.commitTransaction(); - - this._childItems.set(itemID); - - // If this was previously empty, update and send a notification to the tree - if (!this._hasChildItems){ - this._hasChildItems = true; - } - - Zotero.Notifier.trigger('add', 'collection-item', this.getID() + '-' + itemID); -} - - -/** - * Add multiple items to the collection in batch - */ -Zotero.Collection.prototype.addItems = function(itemIDs) { - if (!itemIDs || !itemIDs.length) { - return; - } - - Zotero.DB.beginTransaction(); - for (var i=0; i<itemIDs.length; i++) { - this.addItem(itemIDs[i]); - } - Zotero.DB.commitTransaction(); -} - - -/** -* Remove an item from the collection (does not delete item from library) -**/ -Zotero.Collection.prototype.removeItem = function(itemID){ - var sql = "DELETE FROM collectionItems WHERE collectionID=" + this._id - + " AND itemID=" + itemID; - Zotero.DB.query(sql); - - this._childItems.remove(itemID); - - // If this was the last item, set collection to empty - if (!this._childItems.length){ - this._hasChildItems = false; - } - - Zotero.Notifier.trigger('remove', 'collection-item', this.getID() + '-' + itemID); -} - - -/** - * Remove multiple items from the collection in batch - * (does not delete item from library) - */ -Zotero.Collection.prototype.removeItems = function(itemIDs) { - if (!itemIDs || !itemIDs.length) { - return; - } - - Zotero.DB.beginTransaction(); - for (var i=0; i<itemIDs.length; i++) { - this.removeItem(itemIDs[i]); - } - Zotero.DB.commitTransaction(); -} - - -/* - * Returns an array of child items of this collecetion as Zotero.Item instances, - * or FALSE if none - */ -Zotero.Collection.prototype.getChildItems = function () { - if (!this._childItemsLoaded){ - this._loadChildItems(); - } - - if (this._childItems.length == 0) { - return false; - } - - var toLoad = []; - for (var id in this._childItems.items) { - toLoad.push(id); - } - - return Zotero.Items.get(toLoad); -} - - -/** -* Check if an item belongs to the collection -**/ -Zotero.Collection.prototype.hasItem = function(itemID){ - if (!this._childItemsLoaded){ - this._loadChildItems(); - } - return this._childItems.has(itemID); -} - - -Zotero.Collection.prototype.hasDescendent = function(type, id){ - var descendents = this.getDescendents(); - for (var i=0, len=descendents.length; i<len; i++){ - if (descendents[i]['type']==type && descendents[i]['id']==id){ - return true; - } - } - return false; -} - - -/** -* Deletes collection and all descendent collections and items -**/ -Zotero.Collection.prototype.erase = function(deleteItems){ - Zotero.DB.beginTransaction(); - - var descendents = this.getDescendents(); - var collections = [this.id], items = []; - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - - for(var i=0, len=descendents.length; i<len; i++){ - // Descendent collections - if (descendents[i]['type']=='collection'){ - collections.push(descendents[i]['id']); - var c = Zotero.Collections.get(descendents[i]['id']); - if (c) { - notifierData[c.id] = { old: c.toArray() }; - } - } - // Descendent items - else { - if (deleteItems){ - // Delete items from DB - Zotero.Items.get(descendents[i]['id']).erase(); - } - } - } - - // Remove item associations for all descendent collections - Zotero.DB.query('DELETE FROM collectionItems WHERE collectionID IN (' - + collections.join() + ')'); - - // And delete all descendent collections - Zotero.DB.query('DELETE FROM collections WHERE collectionID IN (' - + collections.join() + ')'); - - Zotero.DB.commitTransaction(); - - // Clear deleted collection from internal memory - Zotero.Collections.unload(collections); - - Zotero.Collections.reloadAll(); - - Zotero.Notifier.trigger('delete', 'collection', collections, notifierData); -} - - -Zotero.Collection.prototype.isCollection = function(){ - return true; -} - - -Zotero.Collection.prototype.toArray = function() { - return { - id: this.getID(), - name: this.getName(), - parent: this.getParent(), - descendents: this.getDescendents(true) - }; -} - - -Zotero.Collection.prototype._loadChildItems = function(){ - this._childItems = new Zotero.Hash(); - - var sql = "SELECT itemID FROM collectionItems WHERE collectionID=" + this._id; - var itemIDs = Zotero.DB.columnQuery(sql); - - if (itemIDs){ - for (var i=0; i<itemIDs.length; i++){ - this._childItems.set(itemIDs[i]); - } - } - - this._childItemsLoaded = true; -} - - -/** -* Returns an array of descendent collections and items -* (rows of 'id', 'type' ('item' or 'collection'), and, if collection, 'name' -* and the nesting 'level') -* -* nested: Return multidimensional array with 'children' nodes instead of flat array -**/ -Zotero.Collection.prototype.getDescendents = function(nested, type, level){ - var toReturn = new Array(); - - if (!level) { - level = 1; - } - - // 0 == collection - // 1 == item - var children = Zotero.DB.query('SELECT collectionID AS id, ' - + "0 AS type, collectionName AS collectionName " - + 'FROM collections WHERE parentCollectionID=' + this._id - + ' UNION SELECT itemID AS id, 1 AS type, NULL AS collectionName ' - + 'FROM collectionItems WHERE collectionID=' + this._id); - - if (type){ - switch (type){ - case 'item': - case 'collection': - break; - default: - throw ("Invalid type '" + type + "' in Collection.getDescendents()"); - } - } - - for(var i=0, len=children.length; i<len; i++){ - // This seems to not work without parseInt() even though - // typeof children[i]['type'] == 'number' and - // children[i]['type'] === parseInt(children[i]['type']), - // which sure seems like a bug to me - switch (parseInt(children[i]['type'])){ - case 0: - if (!type || type=='collection'){ - toReturn.push({ - id: children[i]['id'], - name: children[i]['collectionName'], - type: 'collection', - level: level - }); - } - - var descendents = - Zotero.Collections.get(children[i]['id']).getDescendents(nested, type, level+1); - - if (nested){ - toReturn[toReturn.length-1]['children'] = descendents; - } - else { - for(var j=0, len2=descendents.length; j<len2; j++){ - toReturn.push(descendents[j]); - } - } - break; - - case 1: - if (!type || type=='item'){ - toReturn.push({ - id: children[i]['id'], - type: 'item' - }); - } - break; - } - } - - return toReturn; -} - - - - -/* - * Primary interface for accessing Zotero collection - */ -Zotero.Collections = new function(){ - var _collections = new Array(); - var _collectionsLoaded = false; - - this.get = get; - this.add = add; - this.getCollectionsContainingItems = getCollectionsContainingItems; - this.reloadAll = reloadAll; - this.unload = unload; - - /* - * Returns a Zotero.Collection object for a collectionID - */ - function get(id){ - if (!_collectionsLoaded){ - this.reloadAll(); - } - return (typeof _collections[id]!='undefined') ? _collections[id] : false; - } - - - /** - * Add new collection to DB and return Collection object - * - * _name_ is non-empty string - * _parent_ is optional collectionID -- creates root collection by default - * - * Returns true on success; false on error - **/ - function add(name, parent){ - if (!name){ - return false; - } - - Zotero.DB.beginTransaction(); - - if (parent && !this.get(parent)){ - Zotero.DB.rollbackTransaction(); - throw('Cannot add collection to invalid parent ' + parent); - } - - var parentParam = parent ? {'int':parent} : {'null':true}; - - var rnd = Zotero.ID.get('collections'); - - var sql = "INSERT INTO collections VALUES (?,?,?)"; - var sqlValues = [ {'int':rnd}, {'string':name}, parentParam ]; - Zotero.DB.query(sql, sqlValues); - - Zotero.DB.commitTransaction(); - - this.reloadAll(); - - Zotero.Notifier.trigger('add', 'collection', rnd); - - return this.get(rnd); - } - - - function getCollectionsContainingItems(itemIDs, asIDs) { - var sql = "SELECT collectionID FROM collections WHERE "; - var sqlParams = []; - for each(var id in itemIDs) { - sql += "collectionID IN (SELECT collectionID FROM collectionItems " - + "WHERE itemID=?) AND " - sqlParams.push(id); - } - sql = sql.substring(0, sql.length - 5); - var collectionIDs = Zotero.DB.columnQuery(sql, sqlParams); - - if (asIDs) { - return collectionIDs; - } - - return Zotero.Collections.get(collectionIDs); - } - - - - /** - * Clear collection from internal cache (used by Zotero.Collection.erase()) - * - * Can be passed ids as individual parameters or as an array of ids, or both - **/ - function unload(){ - var ids = Zotero.flattenArguments(arguments); - - for(var i=0; i<ids.length; i++){ - delete _collections[ids[i]]; - } - } - - - /** - * Loads collection data from DB and adds to internal cache - **/ - function reloadAll() { - // This should be the same as the query in Zotero.Collection.loadFromID, - // just without a specific collectionID - var sql = "SELECT collectionID, collectionName, parentCollectionID, " - + "(SELECT COUNT(*) FROM collections WHERE " - + "parentCollectionID=C.collectionID)!=0 AS hasChildCollections, " - + "(SELECT COUNT(*) FROM collectionItems WHERE " - + "collectionID=C.collectionID)!=0 AS hasChildItems " - + "FROM collections C"; - - var ids = Zotero.flattenArguments(arguments) - if (ids.length){ - sql += " WHERE collectionID IN (" + ids.join() + ")"; - } - - var result = Zotero.DB.query(sql); - - var collectionIDs = []; - - if (result){ - for (var i=0; i<result.length; i++){ - var collectionID = result[i]['collectionID']; - collectionIDs.push(collectionID); - - // If collection doesn't exist, create new object and stuff in array - if (!_collections[collectionID]){ - var collection = new Zotero.Collection(); - collection.loadFromRow(result[i]); - _collections[collectionID] = collection; - } - // If existing collection, reload in place - else { - _collections[collectionID].loadFromRow(result[i]); - } - } - } - - // Remove old collections that no longer exist - for each(var c in _collections) { - if (collectionIDs.indexOf(c.getID()) == -1) { - this.unload(c.getID()); - } - } - - _collectionsLoaded = true; - } -} - - - -/* - * Same structure as Zotero.Tags -- make changes in both places if possible - */ -Zotero.Creators = new function(){ - var _creators = new Array; // indexed by first%%%last%%%fieldMode hash - var _creatorsByID = new Array; // indexed by creatorID - - this.get = get; - this.getID = getID; - this.add = add; - this.purge = purge; - - var self = this; - - /* - * Returns an array of creator data for a given creatorID - */ - function get(creatorID){ - if (_creatorsByID[creatorID]){ - return _creatorsByID[creatorID]; - } - - var sql = 'SELECT * FROM creators WHERE creatorID=' + creatorID; - var result = Zotero.DB.rowQuery(sql); - - if (!result){ - return false; - } - - _creatorsByID[creatorID] = result; - return result; - } - - - /* - * Returns the creatorID matching given name and type - */ - function getID(firstName, lastName, fieldMode){ - if (!firstName){ - firstName = ''; - } - if (!lastName){ - lastName = ''; - } - - // Only two modes for now - if (fieldMode){ - firstName = ''; - fieldMode = 1; - } - else { - fieldMode = 0; - } - - var hash = firstName + '%%%' + lastName + '%%%' + fieldMode; - - if (_creators[hash]){ - return _creators[hash]; - } - - var sql = 'SELECT creatorID FROM creators ' - + 'WHERE firstName=? AND lastName=? AND fieldMode=?'; - var params = [{string: firstName}, {string: lastName}, fieldMode]; - var creatorID = Zotero.DB.valueQuery(sql, params); - - if (creatorID){ - _creators[hash] = creatorID; - } - - return creatorID; - } - - - /* - * Add a new creator to the database - * - * Returns new creatorID - */ - function add(firstName, lastName, fieldMode){ - Zotero.debug('Adding new creator', 4); - - Zotero.DB.beginTransaction(); - - var sql = 'INSERT INTO creators VALUES (?,?,?,?)'; - var rnd = Zotero.ID.get('creators'); - var params = [ - rnd, fieldMode ? '' : {string: firstName}, {string: lastName}, - fieldMode ? 1 : 0 - ]; - Zotero.DB.query(sql, params); - - Zotero.DB.commitTransaction(); - return rnd; - } - - - /* - * Delete obsolete creators from database and clear internal array entries - * - * Returns removed creatorIDs on success - */ - function purge(){ - var sql = 'SELECT creatorID FROM creators WHERE creatorID NOT IN ' - + '(SELECT creatorID FROM itemCreators);'; - var toDelete = Zotero.DB.columnQuery(sql); - - if (!toDelete){ - return false; - } - - // Clear creator entries in internal array - for (var i=0; i<toDelete.length; i++){ - var hash = _getHash(toDelete[i]); - delete _creators[hash]; - delete _creatorsByID[toDelete[i]]; - } - - sql = 'DELETE FROM creators WHERE creatorID NOT IN ' - + '(SELECT creatorID FROM itemCreators);'; - var result = Zotero.DB.query(sql); - - return toDelete; - } - - - function _getHash(creatorID){ - var creator = self.get(creatorID); - if (!creator){ - return false; - } - return creator['firstName'] + '%%%' + creator['lastName'] + '%%%' + - creator['fieldMode']; - } -} - - -/* - * Same structure as Zotero.Creators -- make changes in both places if possible - */ -Zotero.Tags = new function(){ - var _tags = []; // indexed by tag text - var _tagsByID = []; // indexed by tagID - - this.get = get; - this.getName = getName; - this.getID = getID; - this.getIDs = getIDs; - this.getTypes = getTypes; - this.getAll = getAll; - this.getAllWithinSearch = getAllWithinSearch; - this.getTagItems = getTagItems; - this.search = search; - this.add = add; - this.rename = rename; - this.remove = remove; - this.purge = purge; - this.toArray = toArray; - - - /* - * Returns a tag and type for a given tagID - */ - function get(tagID) { - if (_tagsByID[tagID]){ - return _tagsByID[tagID]; - } - - var sql = 'SELECT tag, tagType FROM tags WHERE tagID=?'; - var result = Zotero.DB.rowQuery(sql, tagID); - - if (!result){ - return false; - } - - _tagsByID[tagID] = { - tag: result.tag, - type: result.tagType - }; - return result; - } - - - /* - * Returns a tag for a given tagID - */ - function getName(tagID) { - if (_tagsByID[tagID]){ - return _tagsByID[tagID].tag; - } - - var tag = this.get(tagID); - - return _tagsByID[tagID] ? _tagsByID[tagID].tag : false; - } - - - /* - * Returns the tagID matching given tag and type - */ - function getID(tag, type) { - if (_tags[type] && _tags[type]['_' + tag]){ - return _tags[type]['_' + tag]; - } - - var sql = 'SELECT tagID FROM tags WHERE tag=? AND tagType=?'; - var tagID = Zotero.DB.valueQuery(sql, [tag, type]); - - if (tagID) { - if (!_tags[type]) { - _tags[type] = []; - } - _tags[type]['_' + tag] = tagID; - } - - return tagID; - } - - - /* - * Returns all tagIDs for this tag (of all types) - */ - function getIDs(tag) { - var sql = 'SELECT tagID FROM tags WHERE tag=?'; - return Zotero.DB.columnQuery(sql, [tag]); - } - - - /* - * Returns an array of tagTypes for tags matching given tag - */ - function getTypes(tag) { - var sql = 'SELECT tagType FROM tags WHERE tag=?'; - return Zotero.DB.columnQuery(sql, [tag]); - } - - - /** - * Get all tags indexed by tagID - * - * _types_ is an optional array of tagTypes to fetch - */ - function getAll(types) { - var sql = "SELECT tagID, tag, tagType FROM tags "; - if (types) { - sql += "WHERE tagType IN (" + types.join() + ") "; - } - var tags = Zotero.DB.query(sql); - if (!tags) { - return {}; - } - - var collation = Zotero.getLocaleCollation(); - tags.sort(function(a, b) { - return collation.compareString(1, a.tag, b.tag); - }); - - var indexed = {}; - for (var i=0; i<tags.length; i++) { - indexed[tags[i].tagID] = { - tag: tags[i].tag, - type: tags[i].tagType - }; - } - return indexed; - } - - - /* - * Get all tags within the items of a Zotero.Search object - * - * _types_ is an optional array of tagTypes to fetch - */ - function getAllWithinSearch(search, types) { - // Save search results to temporary table - try { - var tmpTable = search.search(true); - } - catch (e) { - if (e.match(/Saved search [0-9]+ does not exist/)) { - Zotero.DB.rollbackTransaction(); - Zotero.debug(e, 2); - } - else { - throw (e); - } - } - if (!tmpTable) { - return {}; - } - - var sql = "SELECT DISTINCT tagID, tag, tagType FROM itemTags " - + "NATURAL JOIN tags WHERE itemID IN " - + "(SELECT itemID FROM " + tmpTable + ") "; - if (types) { - sql += "AND tagType IN (" + types.join() + ") "; - } - var tags = Zotero.DB.query(sql); - - Zotero.DB.query("DROP TABLE " + tmpTable); - - if (!tags) { - return {}; - } - - var collation = Zotero.getLocaleCollation(); - tags.sort(function(a, b) { - return collation.compareString(1, a.tag, b.tag); - }); - - var indexed = {}; - for (var i=0; i<tags.length; i++) { - indexed[tags[i].tagID] = { - tag: tags[i].tag, - type: tags[i].tagType - }; - } - return indexed; - } - - - function getTagItems(tagID) { - var sql = "SELECT itemID FROM itemTags WHERE tagID=?"; - return Zotero.DB.columnQuery(sql, tagID); - } - - - function search(str){ - var sql = 'SELECT tagID, tag, tagType FROM tags'; - if (str) { - sql += ' WHERE tag LIKE ?'; - } - sql += ' ORDER BY tag COLLATE NOCASE'; - var tags = Zotero.DB.query(sql, str ? '%' + str + '%' : undefined); - var indexed = {}; - for each(var tag in tags) { - indexed[tag.tagID] = { - tag: tag.tag, - type: tag.tagType - }; - } - return indexed; - } - - - /* - * Add a new tag to the database - * - * Returns new tagID - */ - function add(tag, type){ - if (type != 0 && type != 1) { - throw ('Invalid tag type ' + type + ' in Tags.add()'); - } - - if (!type) { - type = 0; - } - - Zotero.debug('Adding new tag of type ' + type, 4); - - Zotero.DB.beginTransaction(); - - var sql = 'INSERT INTO tags VALUES (?,?,?)'; - var rnd = Zotero.ID.get('tags'); - Zotero.DB.query(sql, [{int: rnd}, {string: tag}, {int: type}]); - - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('add', 'tag', rnd); - return rnd; - } - - - function rename(tagID, tag) { - Zotero.debug('Renaming tag', 4); - - Zotero.DB.beginTransaction(); - - var tagObj = this.get(tagID); - var oldName = tagObj.tag; - var oldType = tagObj.type; - var notifierData = {}; - notifierData[this.id] = { old: this.toArray() }; - - if (oldName == tag) { - // Convert unchanged automatic tags to manual - if (oldType != 0) { - var sql = "UPDATE tags SET tagType=0 WHERE tagID=?"; - Zotero.DB.query(sql, tagID); - Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData); - } - Zotero.DB.commitTransaction(); - return; - } - - // Check if the new tag already exists - var sql = "SELECT tagID FROM tags WHERE tag=? AND tagType=0"; - var existingTagID = Zotero.DB.valueQuery(sql, tag); - if (existingTagID) { - var itemIDs = this.getTagItems(tagID); - var existingItemIDs = this.getTagItems(existingTagID); - - // Would be easier to just call removeTag(tagID) and addTag(existingID) - // here, but this is considerably more efficient - var sql = "UPDATE OR REPLACE itemTags SET tagID=? WHERE tagID=?"; - Zotero.DB.query(sql, [existingTagID, tagID]); - - // Manual purge of old tag - var sql = "DELETE FROM tags WHERE tagID=?"; - Zotero.DB.query(sql, tagID); - if (_tags[oldType]) { - delete _tags[oldType]['_' + oldName]; - } - delete _tagsByID[tagID]; - Zotero.Notifier.trigger('delete', 'tag', tagID, notifierData); - - // Simulate tag removal on items that used old tag - var itemTags = []; - for (var i in itemIDs) { - itemTags.push(itemIDs[i] + '-' + tagID); - } - Zotero.Notifier.trigger('remove', 'item-tag', itemTags); - - // And send tag add for new tag (except for those that already had it) - var itemTags = []; - for (var i in itemIDs) { - if (existingItemIDs.indexOf(itemIDs[i]) == -1) { - itemTags.push(itemIDs[i] + '-' + existingTagID); - } - } - Zotero.Notifier.trigger('add', 'item-tag', itemTags); - - Zotero.Notifier.trigger('modify', 'item', itemIDs); - Zotero.DB.commitTransaction(); - return; - } - - // 0 == user tag -- we set all renamed tags to 0 - var sql = "UPDATE tags SET tag=?, tagType=0 WHERE tagID=?"; - Zotero.DB.query(sql, [{string: tag}, tagID]); - - var itemIDs = this.getTagItems(tagID); - - if (_tags[oldType]) { - delete _tags[oldType]['_' + oldName]; - } - delete _tagsByID[tagID]; - - Zotero.DB.commitTransaction(); - - Zotero.Notifier.trigger('modify', 'item', itemIDs); - Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData); - } - - - function remove(tagID) { - Zotero.DB.beginTransaction(); - - var sql = "SELECT itemID FROM itemTags WHERE tagID=?"; - var itemIDs = Zotero.DB.columnQuery(sql, tagID); - - if (!itemIDs) { - Zotero.DB.commitTransaction(); - return; - } - - var sql = "DELETE FROM itemTags WHERE tagID=?"; - Zotero.DB.query(sql, tagID); - - Zotero.Notifier.trigger('modify', 'item', itemIDs) - var itemTags = []; - for (var i in itemIDs) { - itemTags.push(itemIDs[i] + '-' + tagID); - } - Zotero.Notifier.trigger('remove', 'item-tag', itemTags); - - this.purge(); - Zotero.DB.commitTransaction(); - return; - } - - - /* - * Delete obsolete tags from database and clear internal array entries - * - * Returns removed tagIDs on success - */ - function purge(){ - Zotero.DB.beginTransaction(); - - var sql = 'SELECT tagID, tag, tagType FROM tags WHERE tagID ' - + 'NOT IN (SELECT tagID FROM itemTags);'; - var toDelete = Zotero.DB.query(sql); - - if (!toDelete){ - Zotero.DB.commitTransaction(); - return false; - } - - var purged = []; - var notifierData = {}; - - // Clear tag entries in internal array - for each(var tag in toDelete) { - notifierData[tag.tagID] = { old: Zotero.Tags.toArray(tag.tagID) } - - purged.push(tag.tagID); - if (_tags[tag.tagType]) { - delete _tags[tag.tagType]['_' + tag.tag]; - } - delete _tagsByID[tag.tagID]; - } - - sql = 'DELETE FROM tags WHERE tagID NOT IN ' - + '(SELECT tagID FROM itemTags);'; - var result = Zotero.DB.query(sql); - - Zotero.DB.commitTransaction(); - - Zotero.Notifier.trigger('delete', 'tag', purged, notifierData); - - return toDelete; - } - - - function toArray(tagID) { - var obj = this.get(tagID); - obj.id = tagID; - return obj; - } -} - - - - -/* - * Base function for retrieving ids and names of static types stored in the DB - * (e.g. creatorType, fileType, charset, itemType) - * - * Extend using the following code within a child constructor: - * - * Zotero.CachedTypes.apply(this, arguments); - * this.constructor.prototype = new Zotero.CachedTypes(); - * - * And the following properties: - * - * this._typeDesc = 'c'; - * this._idCol = ''; - * this._nameCol = ''; - * this._table = ''; - * this._ignoreCase = false; - * - */ -Zotero.CachedTypes = function(){ - var _types = []; - var _typesLoaded; - var self = this; - - // Override these variables in child classes - this._typeDesc = ''; - this._idCol = ''; - this._nameCol = ''; - this._table = ''; - this._ignoreCase = false; - - this.getName = getName; - this.getID = getID; - this.getTypes = getTypes; - - function getName(idOrName){ - if (!_typesLoaded){ - _load(); - } - - if (this._ignoreCase){ - idOrName = idOrName + ''; - idOrName = idOrName.toLowerCase(); - } - - if (!_types['_' + idOrName]){ - Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1); - return ''; - } - - return _types['_' + idOrName]['name']; - } - - - function getID(idOrName){ - if (!_typesLoaded){ - _load(); - } - - if (this._ignoreCase){ - idOrName = idOrName + ''; - idOrName = idOrName.toLowerCase(); - } - - if (!_types['_' + idOrName]){ - Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1); - return false; - } - - return _types['_' + idOrName]['id']; - } - - - function getTypes(where){ - return Zotero.DB.query('SELECT ' + this._idCol + ' AS id, ' - + this._nameCol + ' AS name FROM ' + this._table - + (where ? ' ' + where : '') + ' ORDER BY ' + this._nameCol); - } - - - function _load(){ - var types = self.getTypes(); - - for (i in types){ - // Store as both id and name for access by either - var typeData = { - id: types[i]['id'], - name: types[i]['name'] - } - _types['_' + types[i]['id']] = typeData; - if (self._ignoreCase){ - _types['_' + types[i]['name'].toLowerCase()] = _types['_' + types[i]['id']]; - } - else { - _types['_' + types[i]['name']] = _types['_' + types[i]['id']]; - } - } - - _typesLoaded = true; - } -} - - -Zotero.CreatorTypes = new function(){ - Zotero.CachedTypes.apply(this, arguments); - this.constructor.prototype = new Zotero.CachedTypes(); - - this.getTypesForItemType = getTypesForItemType; - this.isValidForItemType = isValidForItemType; - this.getPrimaryIDForType = getPrimaryIDForType; - - this._typeDesc = 'creator type'; - this._idCol = 'creatorTypeID'; - this._nameCol = 'creatorType'; - this._table = 'creatorTypes'; - - function getTypesForItemType(itemTypeID){ - var sql = "SELECT creatorTypeID AS id, creatorType AS name " - + "FROM itemTypeCreatorTypes NATURAL JOIN creatorTypes " - // DEBUG: sort needs to be on localized strings in itemPane.js - // (though still put primary field at top) - + "WHERE itemTypeID=? ORDER BY primaryField=1 DESC, name"; - return Zotero.DB.query(sql, itemTypeID); - } - - - function isValidForItemType(creatorTypeID, itemTypeID){ - var sql = "SELECT COUNT(*) FROM itemTypeCreatorTypes " - + "WHERE itemTypeID=? AND creatorTypeID=?"; - return !!Zotero.DB.valueQuery(sql, [itemTypeID, creatorTypeID]); - } - - - function getPrimaryIDForType(itemTypeID){ - var sql = "SELECT creatorTypeID FROM itemTypeCreatorTypes " - + "WHERE itemTypeID=? AND primaryField=1"; - return Zotero.DB.valueQuery(sql, itemTypeID); - } -} - - -Zotero.ItemTypes = new function(){ - Zotero.CachedTypes.apply(this, arguments); - this.constructor.prototype = new Zotero.CachedTypes(); - - this.getPrimaryTypes = getPrimaryTypes; - this.getSecondaryTypes = getSecondaryTypes; - this.getHiddenTypes = getHiddenTypes; - this.getImageSrc = getImageSrc; - - this._typeDesc = 'item type'; - this._idCol = 'itemTypeID'; - this._nameCol = 'typeName'; - this._table = 'itemTypes'; - - function getPrimaryTypes(){ - return this.getTypes('WHERE display=2'); - } - - function getSecondaryTypes(){ - return this.getTypes('WHERE display=1'); - } - - function getHiddenTypes(){ - return this.getTypes('WHERE display=0'); - } - - function getImageSrc(itemType) { - // DEBUG: only have icons for some types so far - switch (itemType) { - case 'attachment-file': - case 'attachment-link': - case 'attachment-snapshot': - case 'attachment-web-link': - case 'attachment-pdf': - case 'artwork': - case 'audioRecording': - case 'blogPost': - case 'book': - case 'bookSection': - case 'computerProgram': - case 'conferencePaper': - case 'email': - case 'film': - case 'forumPost': - case 'interview': - case 'journalArticle': - case 'letter': - case 'magazineArticle': - case 'manuscript': - case 'map': - case 'newspaperArticle': - case 'note': - case 'podcast': - case 'radioBroadcast': - case 'report': - case 'thesis': - case 'tvBroadcast': - case 'videoRecording': - case 'webpage': - return "chrome://zotero/skin/treeitem-" + itemType + ".png"; - } - - return "chrome://zotero/skin/treeitem.png"; - } -} - - -Zotero.FileTypes = new function(){ - Zotero.CachedTypes.apply(this, arguments); - this.constructor.prototype = new Zotero.CachedTypes(); - - this._typeDesc = 'file type'; - this._idCol = 'fileTypeID'; - this._nameCol = 'fileType'; - this._table = 'fileTypes'; - - this.getIDFromMIMEType = getIDFromMIMEType; - - function getIDFromMIMEType(mimeType){ - var sql = "SELECT fileTypeID FROM fileTypeMIMETypes " - + "WHERE ? LIKE mimeType || '%'"; - - return Zotero.DB.valueQuery(sql, [mimeType]); - } -} - - -Zotero.CharacterSets = new function(){ - Zotero.CachedTypes.apply(this, arguments); - this.constructor.prototype = new Zotero.CachedTypes(); - - this._typeDesc = 'character sets'; - this._idCol = 'charsetID'; - this._nameCol = 'charset'; - this._table = 'charsets'; - this._ignoreCase = true; - - this.getAll = getAll; - - function getAll(){ - return this.getTypes(); - } -} - - - - -Zotero.ItemFields = new function(){ - // Private members - var _fields = {}; - var _fieldsLoaded; - var _fieldFormats = []; - var _itemTypeFields = []; - var _baseTypeFields = []; - var _typeFieldIDsByBase = {}; - var _typeFieldNamesByBase = {}; - - var self = this; - - // Privileged methods - this.getName = getName; - this.getID = getID; - this.getLocalizedString = getLocalizedString; - this.isValidForType = isValidForType; - this.isInteger = isInteger; - this.getItemTypeFields = getItemTypeFields; - this.isBaseField = isBaseField; - this.isFieldOfBase = isFieldOfBase; - this.getBaseMappedFields = getBaseMappedFields; - this.getFieldIDFromTypeAndBase = getFieldIDFromTypeAndBase; - this.getBaseIDFromTypeAndField = getBaseIDFromTypeAndField; - this.getTypeFieldsFromBase = getTypeFieldsFromBase; - - - /* - * Return the fieldID for a passed fieldID or fieldName - */ - function getID(field){ - if (!_fieldsLoaded){ - _loadFields(); - } - - if (typeof field == 'number') { - return field; - } - - return _fields[field] ? _fields[field]['id'] : false; - } - - - /* - * Return the fieldName for a passed fieldID or fieldName - */ - function getName(field){ - if (!_fieldsLoaded){ - _loadFields(); - } - - return _fields[field] ? _fields[field]['name'] : false; - } - - - function getLocalizedString(itemType, field) { - // unused currently - //var typeName = Zotero.ItemTypes.getName(itemType); - var fieldName = this.getName(field); - - // Fields in the items table are special cases - switch (field) { - case 'dateAdded': - case 'dateModified': - case 'itemType': - fieldName = field; - } - - // TODO: different labels for different item types - - return Zotero.getString("itemFields." + fieldName); - } - - - function isValidForType(fieldID, itemTypeID){ - if (!_fieldsLoaded){ - _loadFields(); - } - - _fieldCheck(fieldID, 'isValidForType'); - - if (!_fields[fieldID]['itemTypes']){ - return false; - } - - return !!_fields[fieldID]['itemTypes'][itemTypeID]; - } - - - function isInteger(fieldID){ - if (!_fieldsLoaded){ - _loadFields(); - } - - _fieldCheck(fieldID, 'isInteger'); - - var ffid = _fields[fieldID]['formatID']; - return _fieldFormats[ffid] ? _fieldFormats[ffid]['isInteger'] : false; - } - - - /* - * Returns an array of fieldIDs for a given item type - */ - function getItemTypeFields(itemTypeID){ - if (_itemTypeFields[itemTypeID]){ - return _itemTypeFields[itemTypeID]; - } - - if (!itemTypeID){ - throw("Invalid item type id '" + itemTypeID - + "' provided to getItemTypeFields()"); - } - - var sql = 'SELECT fieldID FROM itemTypeFields ' - + 'WHERE itemTypeID=' + itemTypeID + ' ORDER BY orderIndex'; - var fields = Zotero.DB.columnQuery(sql); - - _itemTypeFields[itemTypeID] = fields ? fields : []; - return _itemTypeFields[itemTypeID]; - } - - - function isBaseField(field) { - if (!_fieldsLoaded){ - _loadFields(); - } - - _fieldCheck(field, arguments.callee.name); - - return _fields[field]['isBaseField']; - } - - - function isFieldOfBase(field, baseField) { - var fieldID = _fieldCheck(field, 'isFieldOfBase'); - - var baseFieldID = this.getID(baseField); - if (!baseFieldID) { - throw ("Invalid field '" + baseField + '" for base field in ItemFields.getFieldIDFromTypeAndBase()'); - } - - if (fieldID == baseFieldID) { - return true; - } - - var typeFields = this.getTypeFieldsFromBase(baseFieldID); - return typeFields.indexOf(fieldID) != -1; - } - - - function getBaseMappedFields() { - return Zotero.DB.columnQuery("SELECT DISTINCT fieldID FROM baseFieldMappings"); - } - - - /* - * Returns the fieldID of a type-specific field for a given base field - * or false if none - * - * Examples: - * - * 'audioRecording' and 'publisher' returns label's fieldID - * 'book' and 'publisher' returns publisher's fieldID - * 'audioRecording' and 'number' returns false - * - * Accepts names or ids - */ - function getFieldIDFromTypeAndBase(itemType, baseField) { - if (!_fieldsLoaded){ - _loadFields(); - } - - var itemTypeID = Zotero.ItemTypes.getID(itemType); - if (!itemTypeID) { - throw ("Invalid item type '" + itemType + "' in ItemFields.getFieldIDFromTypeAndBase()"); - } - - var baseFieldID = this.getID(baseField); - if (!baseFieldID) { - throw ("Invalid field '" + baseField + '" for base field in ItemFields.getFieldIDFromTypeAndBase()'); - } - - return _baseTypeFields[itemTypeID][baseFieldID]; - } - - - /* - * Returns the fieldID of the base field for a given type-specific field - * or false if none - * - * Examples: - * - * 'audioRecording' and 'label' returns publisher's fieldID - * 'book' and 'publisher' returns publisher's fieldID - * 'audioRecording' and 'runningTime' returns false - * - * Accepts names or ids - */ - function getBaseIDFromTypeAndField(itemType, typeField) { - var itemTypeID = Zotero.ItemTypes.getID(itemType); - var typeFieldID = this.getID(typeField); - - if (!itemTypeID) { - throw ("Invalid item type '" + itemType + "' in ItemFields.getBaseIDFromTypeAndField()"); - } - - _fieldCheck(typeField, 'getBaseIDFromTypeAndField'); - - if (!this.isValidForType(typeFieldID, itemTypeID)) { - throw ("'" + typeField + "' is not a valid field for '" + itemType + "' in ItemFields.getBaseIDFromTypeAndField()"); - } - - // If typeField is already a base field, just return that - if (this.isBaseField(typeFieldID)) { - return typeFieldID; - } - - return Zotero.DB.valueQuery("SELECT baseFieldID FROM baseFieldMappings " - + "WHERE itemTypeID=? AND fieldID=?", [itemTypeID, typeFieldID]); - } - - - /* - * Returns an array of fieldIDs associated with a given base field - * - * e.g. 'publisher' returns fieldIDs for [university, studio, label, network] - */ - function getTypeFieldsFromBase(baseField, asNames) { - var baseFieldID = this.getID(baseField); - if (!baseFieldID) { - throw ("Invalid base field '" + baseField + '" in ItemFields.getTypeFieldsFromBase()'); - } - - if (asNames) { - return _typeFieldNamesByBase[baseFieldID] ? - _typeFieldNamesByBase[baseFieldID] : false; - } - - return _typeFieldIDsByBase[baseFieldID] ? - _typeFieldIDsByBase[baseFieldID] : false; - } - - - /** - * Check whether a field is valid, throwing an exception if not - * (since it should never actually happen) - **/ - function _fieldCheck(field, func) { - var fieldID = self.getID(field); - if (!fieldID) { - throw ("Invalid field '" + field + (func ? "' in ItemFields." + func + "()" : "'")); - } - return fieldID; - } - - - /* - * Returns hash array of itemTypeIDs for which a given field is valid - */ - function _getFieldItemTypes(){ - var sql = 'SELECT fieldID, itemTypeID FROM itemTypeFields'; - - var results = Zotero.DB.query(sql); - - if (!results){ - throw ('No fields in itemTypeFields!'); - } - var fields = new Array(); - for (var i=0; i<results.length; i++){ - if (!fields[results[i]['fieldID']]){ - fields[results[i]['fieldID']] = new Array(); - } - fields[results[i]['fieldID']][results[i]['itemTypeID']] = true; - } - return fields; - } - - - /* - * Build a lookup table for base field mappings - */ - function _loadBaseTypeFields() { - // Grab all fields, base field or not - var sql = "SELECT IT.itemTypeID, F.fieldID AS baseFieldID, BFM.fieldID " - + "FROM itemTypes IT LEFT JOIN fields F " - + "LEFT JOIN baseFieldMappings BFM" - + " ON (IT.itemTypeID=BFM.itemTypeID AND F.fieldID=BFM.baseFieldID)"; - var rows = Zotero.DB.query(sql); - - var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappings"; - var baseFields = Zotero.DB.columnQuery(sql); - - var fields = []; - for each(var row in rows) { - if (!fields[row.itemTypeID]) { - fields[row.itemTypeID] = []; - } - if (row.fieldID) { - fields[row.itemTypeID][row.baseFieldID] = row.fieldID; - } - // If a base field and already valid for the type, just use that - else if (isBaseField(row.baseFieldID) && - isValidForType(row.baseFieldID, row.itemTypeID)) { - fields[row.itemTypeID][row.baseFieldID] = row.baseFieldID; - } - // Set false for other fields so that we don't need to test for - // existence - else { - fields[row.itemTypeID][row.baseFieldID] = false; - } - } - - _baseTypeFields = fields; - - - var sql = "SELECT baseFieldID, fieldID, fieldName " - + "FROM baseFieldMappings JOIN fields USING (fieldID)"; - var rows = Zotero.DB.query(sql); - for each(var row in rows) { - if (!_typeFieldIDsByBase[row['baseFieldID']]) { - _typeFieldIDsByBase[row['baseFieldID']] = []; - _typeFieldNamesByBase[row['baseFieldID']] = []; - } - _typeFieldIDsByBase[row['baseFieldID']].push(row['fieldID']); - _typeFieldNamesByBase[row['baseFieldID']].push(row['fieldName']); - } - } - - - /* - * Load all fields into an internal hash array - */ - function _loadFields(){ - var result = Zotero.DB.query('SELECT * FROM fieldFormats'); - - for (var i=0; i<result.length; i++){ - _fieldFormats[result[i]['fieldFormatID']] = { - regex: result[i]['regex'], - isInteger: result[i]['isInteger'] - }; - } - - var fields = Zotero.DB.query('SELECT * FROM fields'); - - var fieldItemTypes = _getFieldItemTypes(); - - var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappings"; - var baseFields = Zotero.DB.columnQuery(sql); - - for each(var field in fields){ - _fields[field['fieldID']] = { - id: field['fieldID'], - name: field['fieldName'], - isBaseField: (baseFields.indexOf(field['fieldID']) != -1), - formatID: field['fieldFormatID'], - itemTypes: fieldItemTypes[field['fieldID']] - }; - // Store by name as well as id - _fields[field['fieldName']] = _fields[field['fieldID']]; - } - - _fieldsLoaded = true; - - _loadBaseTypeFields(); - } -} - - -Zotero.ID = new function () { - this.get = get; - - _available = {}; - - /* - * Gets an unused primary key id for a DB table - */ - function get(table, notNull) { - switch (table) { - // Autoincrement tables - // - // Callers need to handle a potential NULL for these unless they - // pass |notNull| - case 'items': - var id = _getNextAvailable(table); - if (!id && notNull) { - return _getNext(table); - } - return id; - - // Non-autoincrement tables - // - // TODO: use autoincrement instead where available in 1.5 - case 'creators': - case 'collections': - case 'itemDataValues': - case 'savedSearches': - case 'tags': - var id = _getNextAvailable(table); - if (!id) { - // If we can't find an empty id quickly, just use MAX() + 1 - return _getNext(table); - } - return id; - - default: - throw ("Unsupported table '" + table + "' in Zotero.ID.get()"); - } - } - - - /* - * Returns the lowest available unused primary key id for table - */ - function _getNextAvailable(table) { - if (!_available[table]) { - _loadAvailable(table); - } - - var arr = _available[table]; - - for (var i in arr) { - var id = arr[i][0]; - // End of range -- remove range - if (id == arr[i][1]) { - arr.shift(); - } - // Within range -- increment - else { - arr[i][0]++; - } - - // Prepare table for refresh if all rows used - if (arr.length == 0) { - delete _available[table]; - } - - return id; - } - return null; - } - - - /* - * Get MAX(id) + 1 from table - */ - function _getNext(table) { - var column = _getTableColumn(table); - var sql = 'SELECT MAX(' + column + ') + 1 FROM ' + table; - return Zotero.DB.valueQuery(sql); - } - - - /* - * Loads available ids for table into memory - */ - function _loadAvailable(table) { - Zotero.debug("Loading available ids for table '" + table + "'"); - - var numIDs = 3; // Number of ids to compare against at a time - var maxTries = 3; // Number of times to try increasing the maxID - var maxToFind = 1000; - - var column = _getTableColumn(table); - - switch (table) { - case 'creators': - case 'items': - case 'itemDataValues': - case 'tags': - break; - - case 'collections': - case 'savedSearches': - var maxToFind = 100; - break; - - default: - throw ("Unsupported table '" + table + "' in Zotero.ID._loadAvailable()"); - } - - var maxID = numIDs; - var sql = "SELECT " + column + " FROM " + table - + " WHERE " + column + "<=? ORDER BY " + column; - var ids = Zotero.DB.columnQuery(sql, maxID); - // If no ids found, we have maxID unused ids - if (!ids) { - Zotero.debug('none found'); - var found = Math.min(maxID, maxToFind); - Zotero.debug("Found " + found + " available ids in table '" + table + "'"); - _available[table] = [[1, found]]; - return; - } - - // If we didn't find any unused ids, try increasing maxID a few times - while (ids.length == maxID && maxTries>0) { - Zotero.debug('nope'); - maxID = maxID + numIDs; - ids = Zotero.DB.columnQuery(sql, maxID); - maxTries--; - } - - // Didn't find any unused ids - if (ids.length == maxID) { - Zotero.debug('none!'); - Zotero.debug("Found 0 available ids in table '" + table + "'"); - _available[table] = []; - return; - } - - var available = [], found = 0, j=0, availableStart = null; - - for (var i=1; i<=maxID && found<maxToFind; i++) { - // We've gone past the found ids, so all remaining ids up to maxID - // are available - if (!ids[j]) { - Zotero.debug('all remaining are available'); - available.push([i, maxID]); - found += (maxID - i) + 1; - break; - } - - // Skip ahead while ids are occupied - if (ids[j] == i) { - Zotero.debug('skipping'); - j++; - continue; - } - - // Advance counter while it's below the next used id - while (ids[j] > i && i<=maxID) { - Zotero.debug('b'); - if (!availableStart) { - availableStart = i; - } - i++; - - if ((found + (i - availableStart) + 1) > maxToFind) { - break; - } - } - if (availableStart) { - available.push([availableStart, i-1]); - // Keep track of how many empties we've found - found += ((i-1) - availableStart) + 1; - availableStart = null; - } - j++; - } - - Zotero.debug("Found " + found + " available ids in table '" + table + "'"); - - _available[table] = available; - Zotero.debug(available); - } - - - /** - * Find a unique random id for use in a DB table - * - * (No longer used) - **/ - function _getRandomID(table, max){ - var column = _getTableColumn(table); - - var sql = 'SELECT COUNT(*) FROM ' + table + ' WHERE ' + column + '= ?'; - - if (!max){ - max = 16383; - } - - max--; // since we use ceil(), decrement max by 1 - var tries = 3; // # of tries to find a unique id - for (var i=0; i<tries; i++) { - var rnd = Math.ceil(Math.random() * max); - var exists = Zotero.DB.valueQuery(sql, { int: rnd }); - if (!exists) { - return rnd; - } - } - - // If no luck after number of tries, try a larger range - var sql = 'SELECT MAX(' + column + ') + 1 FROM ' + table; - return Zotero.valueQuery(sql); - } - - - function _getTableColumn(table) { - switch (table) { - case 'itemDataValues': - return 'valueID'; - - case 'savedSearches': - return 'savedSearchID'; - - default: - return table.substr(0, table.length - 1) + 'ID'; - } - } -} - - - - /* * Zotero.getCollections(parent) @@ -5203,18 +30,18 @@ Zotero.ID = new function () { * Takes parent collectionID as optional parameter; * by default, returns root collections */ -Zotero.getCollections = function(parent, recursive){ +Zotero.getCollections = function(parent, recursive) { var toReturn = new Array(); - if (!parent){ + if (!parent) { parent = null; } var sql = "SELECT collectionID AS id, collectionName AS name FROM collections C " - + "WHERE parentCollectionID " + (parent ? '=' + parent : ' IS NULL'); + + "WHERE parentCollectionID " + (parent ? '=' + parent : 'IS NULL'); var children = Zotero.DB.query(sql); - if (!children){ + if (!children) { Zotero.debug('No child collections of collection ' + parent, 5); return toReturn; } @@ -5225,20 +52,20 @@ Zotero.getCollections = function(parent, recursive){ return collation.compareString(1, a.name, b.name); }); - for (var i=0, len=children.length; i<len; i++){ + for (var i=0, len=children.length; i<len; i++) { var obj = Zotero.Collections.get(children[i].id); - if (!obj){ + if (!obj) { throw ('Collection ' + children[i].id + ' not found'); } toReturn.push(obj); // If recursive, get descendents - if (recursive){ + if (recursive) { var desc = obj.getDescendents(false, 'collection'); - for (var j in desc){ + for (var j in desc) { var obj2 = Zotero.Collections.get(desc[j]['id']); - if (!obj2){ + if (!obj2) { throw ('Collection ' + desc[j] + ' not found'); } diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -26,11 +26,33 @@ Zotero.DBConnection = function(dbName) { } this.skipBackup = false; + this.transactionVacuum = false; + + // JS Date + this.__defineGetter__('transactionDate', function () { + if (this._transactionDate) { + return this._transactionDate; + } + // Use second granularity rather than millisecond + // for comparison purposes + return new Date(Math.floor(new Date / 1000) * 1000); + }); + // SQL DATETIME + this.__defineGetter__('transactionDateTime', function () { + var d = this.transactionDate; + return Zotero.Date.dateToSQL(d, true); + }); + // Unix timestamp + this.__defineGetter__('transactionTimestamp', function () { + var d = this.transactionDate; + return Zotero.Date.toUnixTimestamp(d); + }); // Private members this._dbName = dbName; this._shutdown = false; this._connection = null; + this._transactionDate = null; this._transactionRollback = null; this._transactionNestingLevel = 0; this._callbacks = { begin: [], commit: [], rollback: [] }; @@ -76,7 +98,7 @@ Zotero.DBConnection.prototype.query = function (sql,params) { } // If SELECT statement, return result - if (op=='select') { + if (op == 'select') { // Until the native dataset methods work (or at least exist), // we build a multi-dimensional associative array manually @@ -105,7 +127,7 @@ Zotero.DBConnection.prototype.query = function (sql,params) { db.executeSimpleSQL(sql); } - if (op=='insert') { + if (op == 'insert' || op == 'replace') { return db.lastInsertRowID; } // DEBUG: Can't get affected rows for UPDATE or DELETE? @@ -201,6 +223,12 @@ Zotero.DBConnection.prototype.getStatement = function (sql, params) { params = [params]; } + var matches = sql.match(/\?([^0-9]|$)/g); + if (matches && matches.length != params.length) { + throw ('Incorrect number of parameters in query (' + + params.length + ', expecting ' + matches.length + ')'); + } + for (var i=0; i<params.length; i++) { // Integer if (params[i]!==null && typeof params[i]['int'] != 'undefined') { @@ -241,9 +269,28 @@ Zotero.DBConnection.prototype.getStatement = function (sql, params) { // Bind the parameter as the correct type switch (type) { case 'int': - this._debug('Binding parameter ' + (i+1) - + ' of type int: ' + value, 5); - statement.bindInt32Parameter(i, value); + var intVal = parseInt(value); + if (isNaN(intVal)) { + throw ("Invalid integer value '" + value + "'") + } + + // Store as 32-bit signed integer + if (intVal <= 2147483647) { + this._debug('Binding parameter ' + (i+1) + + ' of type int: ' + value, 5); + statement.bindInt32Parameter(i, intVal); + } + // Store as 64-bit signed integer + // 2^53 is JS's upper-bound for decimal integers + else if (intVal < 9007199254740992) { + this._debug('Binding parameter ' + (i+1) + + ' of type int64: ' + value, 5); + statement.bindInt64Parameter(i, intVal); + } + else { + throw ("Integer value '" + intVal + "' too large"); + } + break; case 'string': @@ -294,6 +341,9 @@ Zotero.DBConnection.prototype.beginTransaction = function () { this._debug('Beginning DB transaction', 5); db.beginTransaction(); + // Set a timestamp for this transaction + this._transactionDate = new Date(Math.floor(new Date / 1000) * 1000); + // Run callbacks for (var i=0; i<this._callbacks.begin.length; i++) { if (this._callbacks.begin[i]) { @@ -317,9 +367,19 @@ Zotero.DBConnection.prototype.commitTransaction = function () { } else { this._debug('Committing transaction',5); + + // Clear transaction timestamp + this._transactionDate = null; + try { db.commitTransaction(); + if (this.transactionVacuum) { + Zotero.debug('Vacuuming database'); + db.executeSimpleSQL('VACUUM'); + this.transactionVacuum = false; + } + // Run callbacks for (var i=0; i<this._callbacks.commit.length; i++) { if (this._callbacks.commit[i]) { @@ -882,7 +942,10 @@ Zotero.DBConnection.prototype._getDBConnection = function () { throw (e); } - // Register shutdown handler to call this.onShutdown() for DB backup + // Get exclusive lock on DB + Zotero.DB.query("PRAGMA locking_mode=EXCLUSIVE"); + + // Register shutdown handler to call this.observe() for DB backup var observerService = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService); observerService.addObserver(this, "xpcom-shutdown", false); @@ -921,7 +984,5 @@ Zotero.DBConnection.prototype._getTypedValue = function (statement, i) { } - - // Initialize main database connection Zotero.DB = new Zotero.DBConnection('zotero'); diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js @@ -113,7 +113,7 @@ Zotero.File = new function(){ is.close(); - return contents.join(); + return contents.join(''); } diff --git a/chrome/content/zotero/xpcom/fulltext.js b/chrome/content/zotero/xpcom/fulltext.js @@ -194,25 +194,39 @@ Zotero.Fulltext = new function(){ return false; } - var sqlQues = []; - var sqlParams = []; - - for each(var word in words){ - sqlQues.push('?'); - sqlParams.push({string:word}); - } + var existing = []; + var done = 0; + var maxWords = 500; + var numWords = words.length; Zotero.DB.beginTransaction(); - var sql = "SELECT word, wordID from fulltextWords WHERE word IN (" - sql += sqlQues.join() + ")"; - var wordIDs = Zotero.DB.query(sql, sqlParams); + var origWords = []; - var existing = []; - for (var i in wordIDs){ - // Underscore avoids problems with JS reserved words - existing['_' + wordIDs[i]['word']] = wordIDs[i]['wordID']; + do { + var chunk = words.splice(0, maxWords); + origWords = origWords.concat(chunk); + + var sqlQues = []; + var sqlParams = []; + + for each(var word in chunk) { + sqlQues.push('?'); + sqlParams.push( { string: word } ); + } + + var sql = "SELECT word, wordID from fulltextWords WHERE word IN (" + sql += sqlQues.join() + ")"; + var wordIDs = Zotero.DB.query(sql, sqlParams); + + for (var i in wordIDs) { + // Underscore avoids problems with JS reserved words + existing['_' + wordIDs[i].word] = wordIDs[i].wordID; + } + + done += chunk.length; } + while (done < numWords); Zotero.DB.query("REPLACE INTO fulltextItems (itemID, version) VALUES (?,?)", [itemID, FULLTEXT_VERSION]); @@ -221,7 +235,7 @@ Zotero.Fulltext = new function(){ var statement1 = Zotero.DB.getStatement("INSERT INTO fulltextWords (word) VALUES (?)"); var statement2 = Zotero.DB.getStatement("INSERT OR IGNORE INTO fulltextItemWords VALUES (?,?)"); - for each(var word in words){ + for each(var word in origWords) { if (existing['_' + word]){ var wordID = existing['_' + word]; } diff --git a/chrome/content/zotero/xpcom/id.js b/chrome/content/zotero/xpcom/id.js @@ -0,0 +1,286 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.ID = new function () { + this.get = get; + this.getKey = getKey; + this.getBigInt = getBigInt; + + _available = {}; + + /* + * Gets an unused primary key id for a DB table + */ + function get(table, notNull, skip) { + switch (table) { + // Autoincrement tables + // + // Callers need to handle a potential NULL for these unless they + // pass |notNull| + case 'items': + case 'creators': + case 'creatorData': + case 'collections': + case 'savedSearches': + var id = _getNextAvailable(table, skip); + if (!id && notNull) { + return _getNext(table, skip); + } + return id; + + // Non-autoincrement tables + // + // TODO: use autoincrement instead where available in 1.5 + case 'itemDataValues': + case 'tags': + var id = _getNextAvailable(table, skip); + if (!id) { + // If we can't find an empty id quickly, just use MAX() + 1 + return _getNext(table, skip); + } + return id; + + default: + throw ("Unsupported table '" + table + "' in Zotero.ID.get()"); + } + } + + + function getKey() { + var baseString = "23456789ABCDEFGHIJKMNPQRSTUVWXTZ"; + return Zotero.randomString(8, baseString); + } + + + function getBigInt() { + return Math.floor(Math.random() * (9007199254740991)) + 1; + } + + + /* + * Returns the lowest available unused primary key id for table + */ + function _getNextAvailable(table, skip) { + if (!_available[table]) { + _loadAvailable(table); + } + + var arr = _available[table]; + + for (var i in arr) { + var id = arr[i][0]; + + if (skip && skip.indexOf(id) != -1) { + continue; + } + + // End of range -- remove range + if (id == arr[i][1]) { + arr.shift(); + } + // Within range -- increment + else { + arr[i][0]++; + } + + // Prepare table for refresh if all rows used + if (arr.length == 0) { + delete _available[table]; + } + + return id; + } + return null; + } + + + /* + * Get MAX(id) + 1 from table + */ + function _getNext(table, skip) { + var column = _getTableColumn(table); + + var sql = 'SELECT MAX('; + if (skip && skip.length) { + var max = Math.max.apply(this, skip); + sql += 'MAX(' + column + ', ' + max + ')'; + } + else { + sql += column; + } + sql += ')+1 FROM ' + table; + return Zotero.DB.valueQuery(sql); + } + + + /* + * Loads available ids for table into memory + */ + function _loadAvailable(table) { + Zotero.debug("Loading available ids for table '" + table + "'"); + + var numIDs = 3; // Number of ids to compare against at a time + var maxTries = 3; // Number of times to try increasing the maxID + var maxToFind = 1000; + + var column = _getTableColumn(table); + + switch (table) { + case 'creators': + case 'creatorData': + case 'items': + case 'itemDataValues': + case 'tags': + break; + + case 'collections': + case 'savedSearches': + var maxToFind = 100; + break; + + default: + throw ("Unsupported table '" + table + "' in Zotero.ID._loadAvailable()"); + } + + var maxID = numIDs; + var sql = "SELECT " + column + " FROM " + table + + " WHERE " + column + "<=? ORDER BY " + column; + var ids = Zotero.DB.columnQuery(sql, maxID); + // If no ids found, we have maxID unused ids + if (!ids) { + Zotero.debug('none found'); + var found = Math.min(maxID, maxToFind); + Zotero.debug("Found " + found + " available ids in table '" + table + "'"); + _available[table] = [[1, found]]; + return; + } + + // If we didn't find any unused ids, try increasing maxID a few times + while (ids.length == maxID && maxTries>0) { + Zotero.debug('nope'); + maxID = maxID + numIDs; + ids = Zotero.DB.columnQuery(sql, maxID); + maxTries--; + } + + // Didn't find any unused ids + if (ids.length == maxID) { + Zotero.debug('none!'); + Zotero.debug("Found 0 available ids in table '" + table + "'"); + _available[table] = []; + return; + } + + var available = [], found = 0, j=0, availableStart = null; + + for (var i=1; i<=maxID && found<maxToFind; i++) { + // We've gone past the found ids, so all remaining ids up to maxID + // are available + if (!ids[j]) { + Zotero.debug('all remaining are available'); + available.push([i, maxID]); + found += (maxID - i) + 1; + break; + } + + // Skip ahead while ids are occupied + if (ids[j] == i) { + Zotero.debug('skipping'); + j++; + continue; + } + + // Advance counter while it's below the next used id + while (ids[j] > i && i<=maxID) { + Zotero.debug('b'); + if (!availableStart) { + availableStart = i; + } + i++; + + if ((found + (i - availableStart) + 1) > maxToFind) { + break; + } + } + if (availableStart) { + available.push([availableStart, i-1]); + // Keep track of how many empties we've found + found += ((i-1) - availableStart) + 1; + availableStart = null; + } + j++; + } + + Zotero.debug("Found " + found + " available ids in table '" + table + "'"); + + _available[table] = available; + Zotero.debug(available); + } + + + /** + * Find a unique random id for use in a DB table + * + * (No longer used) + **/ + function _getRandomID(table, max){ + var column = _getTableColumn(table); + + var sql = 'SELECT COUNT(*) FROM ' + table + ' WHERE ' + column + '= ?'; + + if (!max){ + max = 16383; + } + + max--; // since we use ceil(), decrement max by 1 + var tries = 3; // # of tries to find a unique id + for (var i=0; i<tries; i++) { + var rnd = Math.ceil(Math.random() * max); + var exists = Zotero.DB.valueQuery(sql, { int: rnd }); + if (!exists) { + return rnd; + } + } + + // If no luck after number of tries, try a larger range + var sql = 'SELECT MAX(' + column + ') + 1 FROM ' + table; + return Zotero.valueQuery(sql); + } + + + function _getTableColumn(table) { + switch (table) { + case 'itemDataValues': + return 'valueID'; + + case 'savedSearches': + return 'savedSearchID'; + + case 'creatorData': + return 'creatorDataID'; + + default: + return table.substr(0, table.length - 1) + 'ID'; + } + } +} + diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js @@ -114,12 +114,12 @@ Zotero.ItemTreeView.prototype.setTree = function(treebox) var key = String.fromCharCode(event.which); if (key == '+' && !(event.ctrlKey || event.altKey || event.metaKey)) { - expandAllRows(treebox); + obj.expandAllRows(treebox); return; } else if (key == '-' && !(event.shiftKey || event.ctrlKey || event.altKey || event.metaKey)) { - collapseAllRows(treebox); + obj.collapseAllRows(treebox); return; } }, false); @@ -152,6 +152,7 @@ Zotero.ItemTreeView.prototype.setTree = function(treebox) */ Zotero.ItemTreeView.prototype.refresh = function() { + Zotero.debug('Refreshing items list'); this._searchMode = this._itemGroup.isSearchMode(); var oldRows = this.rowCount; @@ -160,6 +161,7 @@ Zotero.ItemTreeView.prototype.refresh = function() this._searchParentIDs = {}; this.rowCount = 0; var cacheFields = ['title', 'date']; + // Cache the visible fields so they don't load individually try { var visibleFields = this.getVisibleFields(); @@ -168,6 +170,7 @@ Zotero.ItemTreeView.prototype.refresh = function() catch (e) { return; } + for (var i=0; i<visibleFields.length; i++) { var field = visibleFields[i]; if (field == 'year') { @@ -201,7 +204,7 @@ Zotero.ItemTreeView.prototype.refresh = function() this._showItem(new Zotero.ItemTreeView.TreeRow(newRows[i], 0, false), added + 1); //item ref, before row added++; } - this._searchItemIDs[newRows[i].getID()] = true; + this._searchItemIDs[newRows[i].id] = true; } // Add parents of matches if not matches themselves @@ -218,7 +221,8 @@ Zotero.ItemTreeView.prototype.refresh = function() this._refreshHashMap(); // Update the treebox's row count - var diff = this.rowCount - oldRows; + // this.rowCount isn't always up-to-date, so use the view's count + var diff = this._treebox.view.rowCount - oldRows; if (diff != 0) { this._treebox.rowCountChanged(0, diff); } @@ -235,6 +239,11 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) return; } + if (!this._itemRowMap) { + Zotero.debug("Item row map didn't exist in itemTreeView.notify()"); + return; + } + var madeChanges = false; var sort = false; @@ -267,7 +276,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) for each(var id in ids) { var split = id.split('-'); // Skip if not collection or not an item in this collection - if (!this._itemGroup.isCollection() || split[0] != this._itemGroup.ref.getID()) { + if (!this._itemGroup.isCollection() || split[0] != this._itemGroup.ref.id) { continue; } splitIDs.push(split[1]); @@ -281,15 +290,23 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) } } - if((action == 'remove' && !this._itemGroup.isLibrary()) || action == 'delete') - { - //Since a remove involves shifting of rows, we have to do it in order + if ((action == 'remove' && !this._itemGroup.isLibrary()) + || action == 'delete' || action == 'id-change') { - //sort the ids by row - var rows = new Array(); + // We only care about the old ids + if (action == 'id-change') { + for (var i=0, len=ids.length; i<len; i++) { + ids[i] = ids[i].split('-')[0]; + } + } + + // 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' || !this._itemGroup.ref.hasItem(ids[i])) { + if (action == 'delete' || action == 'id-change' || + !this._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) { @@ -417,9 +434,9 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) for (var i in items) { // if the item belongs in this collection - if((this._itemGroup.isLibrary() || items[i].inCollection(this._itemGroup.ref.getID())) + if((this._itemGroup.isLibrary() || items[i].inCollection(this._itemGroup.ref.id)) // if we haven't already added it to our hash map - && this._itemRowMap[items[i].getID()] == null + && this._itemRowMap[items[i].id] == null // Regular item or standalone note/attachment && (items[i].isRegularItem() || !items[i].getSource())) { @@ -489,13 +506,28 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData) } else { + var previousRow = this._itemRowMap[ids[0]]; + if (sort) { this.sort(typeof sort == 'number' ? sort : false); } else { this._refreshHashMap(); } - this.rememberSelection(savedSelection); + + // On delete, select item at previous position + if (action == 'delete') { + 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 { + this.rememberSelection(savedSelection); + } } this._treebox.invalidate(); @@ -528,17 +560,20 @@ Zotero.ItemTreeView.prototype.unregister = function() Zotero.ItemTreeView.prototype.getCellText = function(row, column) { var obj = this._getItemAtRow(row); + var val; if(column.id == "zotero-items-column-numChildren") { var c = obj.numChildren(); - if(c) //don't display '0' + // Don't display '0' + if(c && parseInt(c) > 0) { val = c; + } } else if(column.id == "zotero-items-column-type") { - val = Zotero.getString('itemTypes.'+Zotero.ItemTypes.getName(obj.getType())); + val = Zotero.getString('itemTypes.'+Zotero.ItemTypes.getName(obj.ref.itemTypeID)); } // Year column is just date field truncated else if (column.id == "zotero-items-column-year") { @@ -595,7 +630,7 @@ Zotero.ItemTreeView.prototype.getImageSrc = function(row, col) Zotero.ItemTreeView.prototype.isContainer = function(row) { - return this._getItemAtRow(row).isRegularItem(); + return this._getItemAtRow(row).ref.isRegularItem(); } Zotero.ItemTreeView.prototype.isContainerOpen = function(row) @@ -643,7 +678,7 @@ Zotero.ItemTreeView.prototype.hasNextSibling = function(row,afterIndex) } } -Zotero.ItemTreeView.prototype.toggleOpenState = function(row) +Zotero.ItemTreeView.prototype.toggleOpenState = function(row, skipItemMapRefresh) { // Shouldn't happen but does if an item is dragged over a closed // container until it opens and then released, since the container @@ -655,16 +690,16 @@ Zotero.ItemTreeView.prototype.toggleOpenState = function(row) var count = 0; //used to tell the tree how many rows were added/removed var thisLevel = this.getLevel(row); - if(this.isContainerOpen(row)) - { + // Close + if (this.isContainerOpen(row)) { while((row + 1 < this._dataItems.length) && (this.getLevel(row + 1) > thisLevel)) { this._hideItem(row+1); count--; //count is negative when closing a container because we are removing rows } } - else - { + // Open + else { var item = this._getItemAtRow(row).ref; //Get children var attachments = item.getAttachments(); @@ -683,19 +718,35 @@ Zotero.ItemTreeView.prototype.toggleOpenState = function(row) for(var i = 0; i < newRows.length; i++) { + // If item already exists elsewhere in the tree, we have to + // remove it -- this can happen when moving an item into a + // collection if the collection gets the modify event before + // the item + var existingRow = this._itemRowMap[newRows[i].id]; + if (existingRow != null) { + /* + this._hideItem(existingRow); + this._treebox.rowCountChanged(existingRow + 1, -1); + if (existingRow < row) { + row--; + } + */ + throw ("Item already exists outside of collection in Zotero.ItemTreeView.toggleOpenRow()"); + } count++; this._showItem(new Zotero.ItemTreeView.TreeRow(newRows[i], thisLevel + 1, false), row + i + 1); // item ref, before row } } } - this._treebox.beginUpdateBatch(); - this._dataItems[row].isOpen = !this._dataItems[row].isOpen; this._treebox.rowCountChanged(row+1, count); //tell treebox to repaint these this._treebox.invalidateRow(row); - this._treebox.endUpdateBatch(); - this._refreshHashMap(); + + if (!skipItemMapRefresh) { + Zotero.debug('Refreshing hash map'); + this._refreshHashMap(); + } } @@ -786,7 +837,7 @@ Zotero.ItemTreeView.prototype.sort = function(itemID) // Get the display field for a row (which might be a placeholder title) function getField(row) { var field; - var type = row.getType(); + var type = row.ref.itemTypeID; if (columnField == 'title') { if (type == 8 || type == 10) { // 'letter' and 'interview' itemTypeIDs field = row.ref.getDisplayTitle(); @@ -817,8 +868,8 @@ Zotero.ItemTreeView.prototype.sort = function(itemID) switch (columnField) { case 'type': - var typeA = Zotero.getString('itemTypes.'+Zotero.ItemTypes.getName(a.getType())); - var typeB = Zotero.getString('itemTypes.'+Zotero.ItemTypes.getName(b.getType())); + var typeA = Zotero.getString('itemTypes.'+Zotero.ItemTypes.getName(a.ref.itemTypeID)); + var typeB = Zotero.getString('itemTypes.'+Zotero.ItemTypes.getName(b.ref.itemTypeID)); cmp = (typeA > typeB) ? -1 : (typeA < typeB) ? 1 : 0; if (cmp) { @@ -914,10 +965,11 @@ Zotero.ItemTreeView.prototype.sort = function(itemID) for (var i=0; i<this._dataItems.length; i++) { if(this.isContainer(i) && this.isContainerOpen(i)) { - openRows.push(this._getItemAtRow(i).ref.getID()); - this.toggleOpenState(i); + openRows.push(this._getItemAtRow(i).ref.id); + this.toggleOpenState(i, true); } } + this._refreshHashMap(); // Single-row sort if (itemID) { @@ -961,13 +1013,13 @@ Zotero.ItemTreeView.prototype.sort = function(itemID) this._dataItems.sort(reverseSort); } } - - this._refreshHashMap(); // Reopen closed containers for (var i = 0; i < openRows.length; i++) { - this.toggleOpenState(this._itemRowMap[openRows[i]]); + this.toggleOpenState(this._itemRowMap[openRows[i]], true); } + + this._refreshHashMap(); } //////////////////////////////////////////////////////////////////////////////// @@ -1070,7 +1122,7 @@ Zotero.ItemTreeView.prototype.getSelectedItems = function(asIDs) this.selection.getRangeAt(i,start,end); for (var j=start.value; j<=end.value; j++) { if (asIDs) { - items.push(this._getItemAtRow(j).ref.getID()); + items.push(this._getItemAtRow(j).ref.id); } else { items.push(this._getItemAtRow(j).ref); @@ -1088,16 +1140,21 @@ Zotero.ItemTreeView.prototype.getSelectedItems = function(asIDs) */ Zotero.ItemTreeView.prototype.deleteSelection = function(eraseChildren, force) { - if(this.selection.count == 0) + if (this.selection.count == 0) { return; - - //collapse open items - for(var i=0; i<this.rowCount; i++) - if(this.selection.isSelected(i) && this.isContainer(i) && this.isContainerOpen(i)) - this.toggleOpenState(i); + } + + this._treebox.beginUpdateBatch(); + + // Collapse open items + for (var i=0; i<this.rowCount; i++) { + if (this.selection.isSelected(i) && this.isContainer(i) && this.isContainerOpen(i)) { + this.toggleOpenState(i, true); + } + } this._refreshHashMap(); - //create an array of selected items + // Create an array of selected items var ids = []; var start = {}; var end = {}; @@ -1105,12 +1162,9 @@ Zotero.ItemTreeView.prototype.deleteSelection = function(eraseChildren, force) { this.selection.getRangeAt(i,start,end); for (var j=start.value; j<=end.value; j++) - ids.push(this._getItemAtRow(j).ref.getID()); + ids.push(this._getItemAtRow(j).ref.id); } - //iterate and erase... - this._treebox.beginUpdateBatch(); - // Erase item(s) from DB if (this._itemGroup.isLibrary() || force) { Zotero.Items.erase(ids, eraseChildren); @@ -1197,12 +1251,12 @@ Zotero.ItemTreeView.prototype._getItemAtRow = function(row) */ Zotero.ItemTreeView.prototype._refreshHashMap = function() { - this._itemRowMap = new Array(); - for(var i=0; i < this.rowCount; i++) - { + var rowMap = {}; + for (var i=0, len=this.rowCount; i<len; i++) { var row = this._getItemAtRow(i); - this._itemRowMap[row.ref.getID()] = i; + rowMap[row.ref.id] = i; } + this._itemRowMap = rowMap; } /* @@ -1223,7 +1277,7 @@ Zotero.ItemTreeView.prototype.saveSelection = function() if (!item) { continue; } - savedSelection.push(item.ref.getID()); + savedSelection.push(item.ref.id); } } return savedSelection; @@ -1269,7 +1323,7 @@ Zotero.ItemTreeView.prototype.saveOpenState = function() { var ids = []; for (var i=0, len=this.rowCount; i<len; i++) { if (this.isContainer(i) && this.isContainerOpen(i)) { - ids.push(this._getItemAtRow(i).ref.getID()); + ids.push(this._getItemAtRow(i).ref.id); } } return ids; @@ -1277,33 +1331,50 @@ Zotero.ItemTreeView.prototype.saveOpenState = function() { Zotero.ItemTreeView.prototype.rememberOpenState = function(ids) { + var hash = {}; for each(var id in ids) { - var row = this._itemRowMap[id]; - if (row == undefined || !this.isContainer(row) || this.isContainerOpen(row)) { - continue; + hash[id] = true; + } + + this._treebox.beginUpdateBatch(); + for (var i=0; i<this.rowCount; i++) { + var id = this._getItemAtRow(i).ref.id; + if (hash[id] && this.isContainer(i) && this.isContainerOpen(i)) { + this.toggleOpenState(i, true); } - this.toggleOpenState(row); } + this._refreshHashMap(); + this._treebox.endUpdateBatch(); } Zotero.ItemTreeView.prototype.expandMatchParents = function () { // Expand parents of child matches - if (this._searchMode) { - var view = this._treebox.view; - for (var id in this._searchParentIDs) { - if (!view.isContainerOpen(this._itemRowMap[id])) { - view.toggleOpenState(this._itemRowMap[id]); - } + if (!this._searchMode) { + return; + } + + var hash = {}; + for (var id in this._searchParentIDs) { + hash[id] = true; + } + + this._treebox.beginUpdateBatch(); + for (var i=0; i<this.rowCount; i++) { + var id = this._getItemAtRow(i).ref.id; + if (hash[id] && this.isContainer(i) && !this.isContainerOpen(i)) { + this.toggleOpenState(i, true); } } + this._refreshHashMap(); + this._treebox.endUpdateBatch(); } Zotero.ItemTreeView.prototype.saveFirstRow = function() { var row = this._treebox.getFirstVisibleRow(); if (row) { - return this._getItemAtRow(row).ref.getID(); + return this._getItemAtRow(row).ref.id; } return false; } @@ -1317,26 +1388,26 @@ Zotero.ItemTreeView.prototype.rememberFirstRow = function(firstRow) { Zotero.ItemTreeView.prototype.expandAllRows = function(treebox) { - var view = treebox.view; - treebox.beginUpdateBatch(); - for (var i=0; i<view.rowCount; i++) { - if (view.isContainer(i) && !view.isContainerOpen(i)) { - view.toggleOpenState(i); + this._treebox.beginUpdateBatch(); + for (var i=0; i<this.rowCount; i++) { + if (this.isContainer(i) && !this.isContainerOpen(i)) { + this.toggleOpenState(i, true); } } - treebox.endUpdateBatch(); + this._refreshHashMap(); + this._treebox.endUpdateBatch(); } Zotero.ItemTreeView.prototype.collapseAllRows = function(treebox) { - var view = treebox.view; - treebox.beginUpdateBatch(); - for (var i=0; i<view.rowCount; i++) { - if (view.isContainer(i) && view.isContainerOpen(i)) { - view.toggleOpenState(i); + this._treebox.beginUpdateBatch(); + for (var i=0; i<this.rowCount; i++) { + if (this.isContainer(i) && this.isContainerOpen(i)) { + this.toggleOpenState(i, true); } } - treebox.endUpdateBatch(); + this._refreshHashMap(); + this._treebox.endUpdateBatch(); } @@ -1358,7 +1429,7 @@ Zotero.ItemTreeView.prototype.getVisibleFields = function() { Zotero.ItemTreeView.prototype.getSortedItems = function() { var ids = []; for each(var item in this._dataItems) { - ids.push(item.ref.getID()); + ids.push(item.ref.id); } return ids; } @@ -1441,7 +1512,7 @@ Zotero.ItemTreeView.prototype.onDragStart = function (evt,transferData,action) // enable dragging to file system for (var i=0; i<items.length; i++) { if (items[i].isAttachment() && - items[i].getAttachmentLinkMode() != Zotero.Attachments.LINK_MODE_LINKED_URL + items[i].attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL && items[i].getFile()) { transferData.data.addDataForFlavour("application/x-moz-file-promise", new Zotero.ItemTreeView.fileDragDataProvider(), 0, Components.interfaces.nsISupports); @@ -1542,7 +1613,7 @@ Zotero.ItemTreeView.fileDragDataProvider.prototype = { for (var i=0; i<items.length; i++) { // TODO create URL? if (!items[i].isAttachment() || - items[i].getAttachmentLinkMode() == Zotero.Attachments.LINK_MODE_LINKED_URL) { + items[i].attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { continue; } @@ -1550,7 +1621,7 @@ Zotero.ItemTreeView.fileDragDataProvider.prototype = { // Determine if we need to copy multiple files for this item // (web page snapshots) - if (items[i].getAttachmentLinkMode() != Zotero.Attachments.LINK_MODE_LINKED_FILE) { + if (items[i].attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) { var parentDir = file.parent; var files = parentDir.directoryEntries; var numFiles = 0; @@ -1565,7 +1636,7 @@ Zotero.ItemTreeView.fileDragDataProvider.prototype = { // Create folder if multiple files if (numFiles > 1) { - var dirName = Zotero.Attachments.getFileBaseNameFromItem(items[i].getID()); + var dirName = Zotero.Attachments.getFileBaseNameFromItem(items[i].id); try { if (useTemp) { var copiedFile = destDir.clone(); @@ -1597,7 +1668,7 @@ Zotero.ItemTreeView.fileDragDataProvider.prototype = { catch (e) { if (e.name == 'NS_ERROR_FILE_ALREADY_EXISTS') { // Keep track of items that already existed - existingItems.push(items[i].getID()); + existingItems.push(items[i].id); existingFileNames.push(dirName); } else { @@ -1637,7 +1708,7 @@ Zotero.ItemTreeView.fileDragDataProvider.prototype = { } catch (e) { if (e.name == 'NS_ERROR_FILE_ALREADY_EXISTS') { - existingItems.push(items[i].getID()); + existingItems.push(items[i].id); existingFileNames.push(items[i].getFile().leafName); } else { @@ -1813,7 +1884,7 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient) // Only allow dragging of notes and attachments // that aren't already children of the item - if (item.getSource() != rowItem.getID()) { + if (item.getSource() != rowItem.id) { canDrop = true; } } @@ -1893,7 +1964,8 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient) for each(var id in ids) { var item = Zotero.Items.get(id); - item.setSource(rowItem.getID()); + item.setSource(rowItem.id); + item.save(); } } @@ -1909,6 +1981,7 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient) if (!item.isRegularItem()) { item.setSource(); + item.save() } } } @@ -1923,6 +1996,7 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient) // Top-level item if (source) { item.setSource(); + item.save() } this._itemGroup.ref.addItem(id); } @@ -1934,10 +2008,10 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient) var parentCollectionID = false; if (orient == 0) { - sourceItemID = this._getItemAtRow(row).ref.getID() + sourceItemID = this._getItemAtRow(row).ref.id } else if (this._itemGroup.isCollection()) { - var parentCollectionID = this._itemGroup.ref.getID(); + var parentCollectionID = this._itemGroup.ref.id; } var unlock = Zotero.Notifier.begin(true); @@ -2019,7 +2093,7 @@ Zotero.ItemTreeView.prototype.getColumnProperties = function(col, prop) { } /* Mark items not matching search as context rows, displayed in gray */ Zotero.ItemTreeView.prototype.getCellProperties = function(row, col, prop) { - if (this._searchMode && !this._searchItemIDs[this._getItemAtRow(row).ref.getID()]) { + if (this._searchMode && !this._searchItemIDs[this._getItemAtRow(row).ref.id]) { var aServ = Components.classes["@mozilla.org/atom-service;1"]. getService(Components.interfaces.nsIAtomService); prop.AppendElement(aServ.getAtom("contextRow")); @@ -2033,34 +2107,14 @@ Zotero.ItemTreeView.TreeRow = function(ref, level, isOpen) this.isOpen = isOpen; } -Zotero.ItemTreeView.TreeRow.prototype.isNote = function() -{ - return this.ref.isNote(); -} - -Zotero.ItemTreeView.TreeRow.prototype.isAttachment = function() -{ - return this.ref.isAttachment(); -} - -Zotero.ItemTreeView.TreeRow.prototype.isRegularItem = function() -{ - return this.ref.isRegularItem(); -} - Zotero.ItemTreeView.TreeRow.prototype.getField = function(field, unformatted) { return this.ref.getField(field, unformatted, true); } -Zotero.ItemTreeView.TreeRow.prototype.getType = function() -{ - return this.ref.getType(); -} - Zotero.ItemTreeView.TreeRow.prototype.numChildren = function() { - if(this.isRegularItem()) + if(this.ref.isRegularItem()) return this.ref.numChildren(); else return 0; @@ -2068,7 +2122,7 @@ Zotero.ItemTreeView.TreeRow.prototype.numChildren = function() Zotero.ItemTreeView.TreeRow.prototype.numNotes = function() { - if(this.isRegularItem()) + if(this.ref.isRegularItem()) return this.ref.numNotes(); else return 0; @@ -2076,7 +2130,7 @@ Zotero.ItemTreeView.TreeRow.prototype.numNotes = function() Zotero.ItemTreeView.TreeRow.prototype.numAttachments = function() { - if(this.isRegularItem()) + if(this.ref.isRegularItem()) return this.ref.numAttachments(); else return 0; diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js @@ -23,7 +23,10 @@ Zotero.Notifier = new function(){ var _observers = new Zotero.Hash(); var _disabled = false; - var _types = ['collection', 'search', 'item', 'collection-item', 'item-tag', 'tag']; + var _types = [ + 'collection', 'creator', 'search', 'item', + 'collection-item', 'item-tag', 'tag' + ]; var _inTransaction; var _locked = false; var _queue = []; @@ -31,6 +34,7 @@ Zotero.Notifier = new function(){ this.registerObserver = registerObserver; this.unregisterObserver = unregisterObserver; this.trigger = trigger; + this.untrigger = untrigger; this.begin = begin; this.commit = commit; this.reset = reset; @@ -108,9 +112,6 @@ Zotero.Notifier = new function(){ if (!extraData) { throw ("Extra data must be supplied with Notifier type '" + type + "'"); } - if (extraData.constructor.name != 'Array') { - extraData = [extraData]; - } } ids = Zotero.flattenArguments(ids); @@ -120,6 +121,7 @@ Zotero.Notifier = new function(){ 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] = []; @@ -129,11 +131,18 @@ Zotero.Notifier = new function(){ } if (!_queue[type][event].ids) { _queue[type][event].ids = []; - _queue[type][event].data = []; + _queue[type][event].data = {}; } + // Merge ids _queue[type][event].ids = _queue[type][event].ids.concat(ids); - _queue[type][event].data = _queue[type][event].data.concat(extraData); + + // Merge extraData keys + if (extraData) { + for (var dataID in extraData) { + _queue[type][event].data[dataID] = extraData[dataID]; + } + } return true; } @@ -148,6 +157,7 @@ Zotero.Notifier = new function(){ _observers.get(i).ref.notify(event, type, ids, extraData); } catch (e) { + Zotero.debug(e); Components.utils.reportError(e); } } @@ -157,6 +167,26 @@ Zotero.Notifier = new function(){ } + 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) * @@ -219,13 +249,13 @@ Zotero.Notifier = new function(){ for (var event in _queue[type]) { runQueue[type][event] = { ids: [], - data: [] + 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[i]; + var data = _queue[type][event].data[id]; // Don't send modify on nonexistent items or tags if (event == 'modify') { @@ -239,7 +269,7 @@ Zotero.Notifier = new function(){ if (runQueue[type][event].ids.indexOf(id) == -1) { runQueue[type][event].ids.push(id); - runQueue[type][event].data.push(data); + runQueue[type][event].data[id] = data; } } diff --git a/chrome/content/zotero/xpcom/progressWindow.js b/chrome/content/zotero/xpcom/progressWindow.js @@ -290,6 +290,9 @@ Zotero.ProgressWindow = function(_window){ } function _move() { + // sizeToContent() fails in FF3 with multiple lines + // if we don't change the height + _progressWindow.outerHeight = _progressWindow.outerHeight + 1; _progressWindow.sizeToContent(); Zotero.ProgressWindowSet.tile(_progressWindow); } diff --git a/chrome/content/zotero/xpcom/quickCopy.js b/chrome/content/zotero/xpcom/quickCopy.js @@ -136,7 +136,6 @@ Zotero.QuickCopy = new function() { if (mode == 'export') { var translation = new Zotero.Translate("export"); - Zotero.debug(items); translation.setItems(items); translation.setTranslator(format); translation.setHandler("done", callback); diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js @@ -121,7 +121,8 @@ Zotero.Schema = new function(){ var up1 = _migrateUserDataSchema(dbVersion); var up2 = _updateSchema('system'); - var up3 = _updateSchema('scrapers'); + var up3 = _updateSchema('triggers'); + var up4 = _updateSchema('scrapers'); Zotero.DB.commitTransaction(); } @@ -149,7 +150,7 @@ Zotero.Schema = new function(){ } } - if (up2 || up3) { + if (up2 || up3 || up4) { // Run a manual scraper update if upgraded and pref set if (Zotero.Prefs.get('automaticScraperUpdates')){ this.updateScrapersRemote(2); @@ -331,11 +332,6 @@ Zotero.Schema = new function(){ * Retrieve the DB schema version */ function _getDBVersion(schema){ - // Default to schema.sql - if (!schema){ - schema = 'schema'; - } - if (_dbVersions[schema]){ return _dbVersions[schema]; } @@ -487,39 +483,44 @@ Zotero.Schema = new function(){ Zotero.DB.beginTransaction(); try { // Enable auto-vacuuming + Zotero.DB.query("PRAGMA page_size = 4096"); + Zotero.DB.query("PRAGMA encoding = 'UTF-8'"); Zotero.DB.query("PRAGMA auto_vacuum = 1"); + Zotero.DB.query(_getSchemaSQL('system')); Zotero.DB.query(_getSchemaSQL('userdata')); - _updateFailsafeSchema(); - _updateDBVersion('userdata', _getSchemaSQLVersion('userdata')); + Zotero.DB.query(_getSchemaSQL('triggers')); + Zotero.DB.query(_getSchemaSQL('scrapers')); - Zotero.DB.query(_getSchemaSQL('system')); _updateDBVersion('system', _getSchemaSQLVersion('system')); - - Zotero.DB.query(_getSchemaSQL('scrapers')); + _updateDBVersion('userdata', _getSchemaSQLVersion('userdata')); + _updateDBVersion('triggers', _getSchemaSQLVersion('triggers')); _updateDBVersion('scrapers', _getSchemaSQLVersion('scrapers')); - var sql = "INSERT INTO items VALUES(123456789, 14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"; + /* + TODO: uncomment for release + var sql = "INSERT INTO items VALUES(1, 14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'AJ4PT6IT')"; Zotero.DB.query(sql); - var sql = "INSERT INTO itemAttachments VALUES(123456789, NULL, 3, 'text/html', 25, NULL, NULL)"; + var sql = "INSERT INTO itemAttachments VALUES (1, NULL, 3, 'text/html', 25, NULL, NULL)"; Zotero.DB.query(sql); var sql = "INSERT INTO itemDataValues VALUES (?, ?)"; Zotero.DB.query(sql, [1, "Zotero - " + Zotero.getString('install.quickStartGuide')]); - var sql = "INSERT INTO itemData VALUES(123456789, 110, 1)"; + var sql = "INSERT INTO itemData VALUES (1, 110, 1)"; Zotero.DB.query(sql); var sql = "INSERT INTO itemDataValues VALUES (2, 'http://www.zotero.org/documentation/quick_start_guide')"; Zotero.DB.query(sql); - var sql = "INSERT INTO itemData VALUES(123456789, 1, 2)"; + var sql = "INSERT INTO itemData VALUES (1, 1, 2)"; Zotero.DB.query(sql); var sql = "INSERT INTO itemDataValues VALUES (3, CURRENT_TIMESTAMP)"; Zotero.DB.query(sql); - var sql = "INSERT INTO itemData VALUES(123456789, 27, 3)"; + var sql = "INSERT INTO itemData VALUES (1, 27, 3)"; Zotero.DB.query(sql); - var sql = "INSERT INTO itemNotes (itemID, sourceItemID, note) VALUES(123456789, NULL, ?)"; + var sql = "INSERT INTO itemNotes (itemID, sourceItemID, note) VALUES (1, NULL, ?)"; var msg = Zotero.getString('install.quickStartGuide.message.welcome') + " " + Zotero.getString('install.quickStartGuide.message.clickViewPage') + "\n\n" + Zotero.getString('install.quickStartGuide.message.thanks'); Zotero.DB.query(sql, msg); + */ Zotero.DB.commitTransaction(); self.dbInitialized = true; @@ -788,6 +789,8 @@ Zotero.Schema = new function(){ Zotero.debug('Updating user data tables from version ' + fromVersion + ' to ' + toVersion); + var ZU = new Zotero.Utilities; + Zotero.DB.beginTransaction(); try { @@ -1222,10 +1225,214 @@ Zotero.Schema = new function(){ Zotero.DB.query("ALTER TABLE fulltextItems ADD totalChars INT"); Zotero.DB.query("DELETE FROM version WHERE schema='fulltext'"); } + + // 1.5 + + if (i==37) { + // Some data cleanup from the pre-FK-trigger days + Zotero.DB.query("DELETE FROM annotations WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM collectionItems WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM fulltextItems WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM fulltextItemWords WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM highlights WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemAttachments WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemCreators WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemData WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemNotes WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemNoteTitles WHERE itemID NOT IN (SELECT itemID FROM itemNotes)"); + Zotero.DB.query("DELETE FROM itemSeeAlso WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemSeeAlso WHERE linkedItemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemTags WHERE itemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("DELETE FROM itemTags WHERE tagID NOT IN (SELECT tagID FROM tags)"); + Zotero.DB.query("DELETE FROM savedSearchConditions WHERE savedSearchID NOT IN (select savedSearchID FROM savedSearches)"); + + Zotero.DB.query("DELETE FROM itemData WHERE valueID NOT IN (SELECT valueID FROM itemDataValues)"); + Zotero.DB.query("DELETE FROM fulltextItemWords WHERE wordID NOT IN (SELECT wordID FROM fulltextWords)"); + Zotero.DB.query("DELETE FROM collectionItems WHERE collectionID NOT IN (SELECT collectionID FROM collections)"); + Zotero.DB.query("DELETE FROM itemCreators WHERE creatorID NOT IN (SELECT creatorID FROM creators)"); + Zotero.DB.query("DELETE FROM itemTags WHERE tagID NOT IN (SELECT tagID FROM tags)"); + Zotero.DB.query("DELETE FROM itemData WHERE fieldID NOT IN (SELECT fieldID FROM fields)"); + Zotero.DB.query("DELETE FROM itemData WHERE valueID NOT IN (SELECT valueID FROM itemDataValues)"); + + Zotero.DB.query("DROP TABLE IF EXISTS userFieldMask"); + Zotero.DB.query("DROP TABLE IF EXISTS userItemTypes"); + Zotero.DB.query("DROP TABLE IF EXISTS userItemTypeMask"); + Zotero.DB.query("DROP TABLE IF EXISTS userFields"); + Zotero.DB.query("DROP TABLE IF EXISTS userItemTypeFields"); + + var wordIDs = Zotero.DB.columnQuery("SELECT GROUP_CONCAT(wordID) AS wordIDs FROM fulltextWords GROUP BY word HAVING COUNT(*)>1"); + if (wordIDs.length) { + Zotero.DB.query("CREATE TEMPORARY TABLE deleteWordIDs (wordID INTEGER PRIMARY KEY)"); + for (var j=0, len=wordIDs.length; j<len; j++) { + var ids = wordIDs[j].split(','); + for (var k=1; k<ids.length; k++) { + Zotero.DB.query("INSERT INTO deleteWordIDs VALUES (?)", ids[k]); + } + } + Zotero.DB.query("DELETE FROM fulltextWords WHERE wordID IN (SELECT wordID FROM deleteWordIDs)"); + Zotero.DB.query("DROP TABLE deleteWordIDs"); + } + + Zotero.DB.query("REINDEX"); + Zotero.DB.transactionVacuum = true; + + // Set page cache size to 8MB + var pageSize = Zotero.DB.valueQuery("PRAGMA page_size"); + var cacheSize = 8192000 / pageSize; + Zotero.DB.query("PRAGMA default_cache_size=" + cacheSize); + + Zotero.DB.query("UPDATE itemAttachments SET sourceItemID=NULL WHERE sourceItemID NOT IN (SELECT itemID FROM items)"); + Zotero.DB.query("UPDATE itemNotes SET sourceItemID=NULL WHERE sourceItemID NOT IN (SELECT itemID FROM items)"); + + Zotero.DB.query("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n objectID INT NOT NULL,\n key TEXT NOT NULL,\n timestamp INT NOT NULL,\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)\n);"); + Zotero.DB.query("CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp);"); + + // Note titles + Zotero.DB.query("ALTER TABLE itemNotes ADD COLUMN title TEXT"); + var notes = Zotero.DB.query("SELECT itemID, title FROM itemNoteTitles"); + if (notes) { + var statement = Zotero.DB.getStatement("UPDATE itemNotes SET title=? WHERE itemID=?"); + for (var j=0, len=notes.length; j<len; j++) { + statement.bindUTF8StringParameter(0, notes[j].title); + statement.bindInt32Parameter(1, notes[j].itemID); + try { + statement.execute(); + } + catch (e) { + throw (Zotero.DB.getLastErrorString()); + } + } + statement.reset(); + } + Zotero.DB.query("DROP TABLE itemNoteTitles"); + + // Creator data + Zotero.DB.query("CREATE TABLE creatorData (\n creatorDataID INTEGER PRIMARY KEY,\n firstName TEXT,\n lastName TEXT,\n shortName TEXT,\n fieldMode INT,\n birthYear INT\n)"); + Zotero.DB.query("INSERT INTO creatorData SELECT NULL, firstName, lastName, NULL, fieldMode, NULL FROM creators WHERE creatorID IN (SELECT creatorID FROM itemCreators)"); + var creatorsOld = Zotero.DB.query("SELECT * FROM creators"); + Zotero.DB.query("DROP TABLE creators"); + Zotero.DB.query("CREATE TABLE creators (\n creatorID INTEGER PRIMARY KEY,\n creatorDataID INT,\n dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,\n key TEXT NOT NULL,\n FOREIGN KEY (creatorDataID) REFERENCES creatorData(creatorDataID)\n);"); + + var data = Zotero.DB.query("SELECT * FROM creatorData"); + if (data) { + var oldCreatorIDHash = {}; + for (var j=0, len=creatorsOld.length; j<len; j++) { + oldCreatorIDHash[ + ZU.md5( + creatorsOld[j].firstName + '_' + + creatorsOld[j].lastName + '_' + + creatorsOld[j].fieldMode + ) + ] = creatorsOld[j].creatorID; + } + + var updatedIDs = {}; + var insertStatement = Zotero.DB.getStatement("INSERT INTO creators (creatorID, creatorDataID, key) VALUES (?, ?, ?)"); + var updateStatement = Zotero.DB.getStatement("UPDATE itemCreators SET creatorID=? WHERE creatorID=?"); + for (var j=0, len=data.length; j<len; j++) { + insertStatement.bindInt32Parameter(0, data[j].creatorDataID); + insertStatement.bindInt32Parameter(1, data[j].creatorDataID); + var key = Zotero.ID.getKey(); + insertStatement.bindStringParameter(2, key); + + var oldCreatorID = oldCreatorIDHash[ + ZU.md5( + data[j].firstName + '_' + + data[j].lastName + '_' + + data[j].fieldMode + ) + ]; + + if (updatedIDs[oldCreatorID]) { + continue; + } + updatedIDs[oldCreatorID] = true; + + updateStatement.bindInt32Parameter(0, data[j].creatorDataID); + updateStatement.bindInt32Parameter(1, oldCreatorID); + + try { + insertStatement.execute(); + updateStatement.execute(); + } + catch (e) { + throw (Zotero.DB.getLastErrorString()); + } + } + insertStatement.reset(); + updateStatement.reset(); + } + + Zotero.DB.query("CREATE INDEX creators_creatorDataID ON creators(creatorDataID)"); + + // Items + Zotero.DB.query("ALTER TABLE items ADD COLUMN key TEXT"); + var items = Zotero.DB.query("SELECT itemID, itemTypeID, dateAdded FROM items"); + var titles = Zotero.DB.query("SELECT itemID, value FROM itemData NATURAL JOIN itemDataValues WHERE fieldID BETWEEN 110 AND 112"); + var statement = Zotero.DB.getStatement("UPDATE items SET key=? WHERE itemID=?"); + for (var j=0, len=items.length; j<len; j++) { + var key = Zotero.ID.getKey(); + statement.bindStringParameter(0, key); + statement.bindInt32Parameter(1, items[j].itemID); + try { + statement.execute(); + } + catch (e) { + throw (Zotero.DB.getLastErrorString()); + } + } + statement.reset(); + Zotero.DB.query("CREATE UNIQUE INDEX items_key ON items(key)"); + + // Collections + var collections = Zotero.DB.query("SELECT * FROM collections"); + Zotero.DB.query("DROP TABLE collections"); + Zotero.DB.query("CREATE TABLE collections (\n collectionID INTEGER PRIMARY KEY,\n collectionName TEXT,\n parentCollectionID INT,\n dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,\n key TEXT NOT NULL UNIQUE,\n FOREIGN KEY (parentCollectionID) REFERENCES collections(collectionID)\n);"); + var statement = Zotero.DB.getStatement("INSERT INTO collections (collectionID, collectionName, parentCollectionID, key) VALUES (?,?,?,?)"); + for (var j=0, len=collections.length; j<len; j++) { + statement.bindInt32Parameter(0, collections[j].collectionID); + statement.bindUTF8StringParameter(1, collections[j].collectionName); + if (collections[j].parentCollectionID) { + statement.bindInt32Parameter(2, collections[j].parentCollectionID); + } + else { + statement.bindNullParameter(2); + } + var key = Zotero.ID.getKey(); + statement.bindStringParameter(3, key); + + try { + statement.execute(); + } + catch (e) { + throw (Zotero.DB.getLastErrorString()); + } + } + statement.reset(); + + // Saved searches + var searches = Zotero.DB.query("SELECT * FROM savedSearches"); + Zotero.DB.query("DROP TABLE savedSearches"); + Zotero.DB.query("CREATE TABLE savedSearches (\n savedSearchID INTEGER PRIMARY KEY,\n savedSearchName TEXT,\n dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,\n key TEXT NOT NULL UNIQUE\n);"); + var statement = Zotero.DB.getStatement("INSERT INTO savedSearches (savedSearchID, savedSearchName, key) VALUES (?,?,?)"); + for (var j=0, len=searches.length; j<len; j++) { + statement.bindInt32Parameter(0, searches[j].savedSearchID); + statement.bindUTF8StringParameter(1, searches[j].savedSearchName); + var key = Zotero.ID.getKey(); + statement.bindStringParameter(2, key); + + try { + statement.execute(); + } + catch (e) { + throw (Zotero.DB.getLastErrorString()); + } + } + statement.reset(); + } } - _updateSchema('userdata'); - _updateFailsafeSchema(); + _updateDBVersion('userdata', toVersion); Zotero.DB.commitTransaction(); } @@ -1236,41 +1443,4 @@ Zotero.Schema = new function(){ return true; } - - - function _updateFailsafeSchema(){ - // This is super-annoying, but SQLite didn't have IF [NOT] EXISTS - // on trigger statements until 3.3.8, which didn't make it into - // Firefox 2.0, so we just throw the triggers at the DB on every - // userdata update and catch errors individually - // - - try { Zotero.DB.query("DROP TRIGGER insert_date_field"); } catch (e) {} - try { Zotero.DB.query("DROP TRIGGER update_date_field"); } catch (e) {} - - var itemDataTrigger = " FOR EACH ROW WHEN NEW.fieldID IN (14, 27, 52, 96, 100)\n" - + " BEGIN\n" - + " SELECT CASE\n" - + " CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 1, 4) AS INT) BETWEEN 0 AND 9999 AND\n" - + " SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 5, 1) = '-' AND\n" - + " CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 6, 2) AS INT) BETWEEN 0 AND 12 AND\n" - + " SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 8, 1) = '-' AND\n" - + " CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 9, 2) AS INT) BETWEEN 0 AND 31\n" - + " WHEN 0 THEN RAISE (ABORT, 'Date field must begin with SQL date') END;\n" - + " END;\n"; - - try { - var sql = "CREATE TRIGGER insert_date_field BEFORE INSERT ON itemData\n" - + itemDataTrigger; - Zotero.DB.query(sql); - } - catch (e){} - - try { - var sql = "CREATE TRIGGER update_date_field BEFORE UPDATE ON itemData\n" - + itemDataTrigger; - Zotero.DB.query(sql); - } - catch (e){} - } } diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js @@ -99,14 +99,19 @@ Zotero.Search.prototype.load = function(savedSearchID){ Zotero.Search.prototype.getID = function(){ + Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.id'); return this._savedSearchID; } +Zotero.Search.prototype.__defineGetter__('id', function () { return this._savedSearchID; }); -Zotero.Search.prototype.getName = function(){ + +Zotero.Search.prototype.getName = function() { + Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.name'); return this._savedSearchName; } +Zotero.Search.prototype.__defineGetter__('name', function () { return this._savedSearchName; }); /* * Save the search to the DB and return a savedSearchID @@ -634,11 +639,11 @@ Zotero.Search.prototype.search = function(asTempTable){ //Zotero.debug('Final result set'); //Zotero.debug(ids); + if (!ids || !ids.length) { + return false; + } + if (asTempTable) { - if (!ids) { - return false; - } - return this._idsToTempTable(ids); } @@ -947,7 +952,7 @@ Zotero.Search.prototype._buildQuery = function(){ case 'creator': condSQL += "creatorID IN (SELECT creatorID FROM creators " - + "WHERE "; + + "NATURAL JOIN creatorData WHERE "; openParens++; break; @@ -1329,6 +1334,7 @@ Zotero.Searches = new function(){ } + Zotero.SearchConditions = new function(){ this.get = get; this.getStandardConditions = getStandardConditions; @@ -1338,7 +1344,7 @@ Zotero.SearchConditions = new function(){ this.parseCondition = parseCondition; var _initialized = false; - var _conditions = []; + var _conditions = {}; var _standardConditions = []; var self = this; @@ -1375,7 +1381,7 @@ Zotero.SearchConditions = new function(){ * - template */ function _init(){ - _conditions = [ + var conditions = [ // // Special conditions // @@ -1658,19 +1664,17 @@ Zotero.SearchConditions = new function(){ }, special: false } - ]; // Index conditions by name and aliases - for (var i in _conditions){ - _conditions[_conditions[i]['name']] = _conditions[i]; - if (_conditions[i]['aliases']){ - for (var j in _conditions[i]['aliases']){ - _conditions[_conditions[i]['aliases'][j]] = _conditions[i]; + for (var i in conditions) { + _conditions[conditions[i]['name']] = conditions[i]; + if (conditions[i]['aliases']) { + for (var j in conditions[i]['aliases']) { + _conditions[conditions[i]['aliases'][j]] = conditions[i]; } } - _conditions[_conditions[i]['name']] = _conditions[i]; - delete _conditions[i]; + _conditions[conditions[i]['name']] = conditions[i]; } var sortKeys = []; diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js @@ -0,0 +1,1852 @@ +Zotero.Sync = new function() { + this.init = init; + this.getObjectTypeID = getObjectTypeID; + this.getObjectTypeName = getObjectTypeName; + this.buildUploadIDs = buildUploadIDs; + this.getUpdatedObjects = getUpdatedObjects; + this.addToUpdated = addToUpdated; + this.getDeletedObjects = getDeletedObjects; + this.purgeDeletedObjects = purgeDeletedObjects; + this.removeFromDeleted = removeFromDeleted; + + this.__defineGetter__('syncObjects', function () { + return ['Creator', 'Item', 'Collection']; + }); + + default xml namespace = ''; + + var _typesLoaded = false; + var _objectTypeIDs = {}; + var _objectTypeNames = {}; + + var _deleteLogDays = 30; + + + function init() { + var sql = "SELECT version FROM version WHERE schema='syncdeletelog'"; + if (!Zotero.DB.valueQuery(sql)) { + sql = "SELECT COUNT(*) FROM syncDeleteLog"; + if (Zotero.DB.valueQuery(sql)) { + throw ('syncDeleteLog not empty and no timestamp in Zotero.Sync.delete()'); + } + sql = "INSERT INTO version VALUES ('syncdeletelog', ?)"; + Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp()); + } + + this.EventListener.init(); + } + + + function getObjectTypeID(type) { + if (!_typesLoaded) { + _loadObjectTypes(); + } + + var id = _objectTypeIDs[type]; + return id ? id : false; + } + + + function getObjectTypeName(typeID) { + if (!_typesLoaded) { + _loadObjectTypes(); + } + + var name = _objectTypeNames[typeID]; + return name ? name : false; + } + + + function buildUploadIDs() { + var uploadIDs = {}; + + uploadIDs.updated = {}; + uploadIDs.changed = {}; + uploadIDs.deleted = {}; + + for each(var Type in Zotero.Sync.syncObjects) { + var Types = Type + 's'; // 'Items' + var type = Type.toLowerCase(); // 'item' + var types = type + 's'; // 'items' + + uploadIDs.updated[types] = []; + uploadIDs.changed[types] = {}; + uploadIDs.deleted[types] = []; + } + + return uploadIDs; + } + + + /** + * @param object lastSyncDate JS Date object + * @return object { items: [123, 234, ...], creators: [321, 432, ...], ... } + */ + function getUpdatedObjects(lastSyncDate) { + if (lastSyncDate && lastSyncDate.constructor.name != 'Date') { + throw ('lastSyncDate must be a Date or FALSE in ' + + 'Zotero.Sync.getDeletedObjects()') + } + + var updatedIDs = {}; + for each(var Type in this.syncObjects) { + var Types = Type + 's'; // 'Items' + var type = Type.toLowerCase(); // 'item' + var types = type + 's'; // 'items' + + Zotero.debug("Getting updated local " + types); + + updatedIDs[types] = Zotero[Types].getUpdated(lastSyncDate); + if (!updatedIDs[types]) { + updatedIDs[types] = []; + } + } + return updatedIDs; + } + + + function addToUpdated(updated, ids) { + ids = Zotero.flattenArguments(ids); + for each(var id in ids) { + if (updated.indexOf(id) == -1) { + updated.push(id); + } + } + } + + + /** + * @param object lastSyncDate JS Date object + * @return mixed Returns object with deleted ids + * { + * items: [ { id: 123, key: ABCD1234 }, ... ] + * creators: [ { id: 123, key: ABCD1234 }, ... ], + * ... + * } + * or FALSE if none or -1 if last sync time is before start of log + */ + function getDeletedObjects(lastSyncDate) { + if (lastSyncDate && lastSyncDate.constructor.name != 'Date') { + throw ('lastSyncDate must be a Date or FALSE in ' + + 'Zotero.Sync.getDeletedObjects()') + } + + var sql = "SELECT version FROM version WHERE schema='syncdeletelog'"; + var syncLogStart = Zotero.DB.valueQuery(sql); + if (!syncLogStart) { + throw ('syncLogStart not found in Zotero.Sync.getDeletedObjects()'); + } + + // Last sync time is before start of log + if (lastSyncDate && new Date(syncLogStart * 1000) > lastSyncDate) { + return -1; + } + + var param = false; + var sql = "SELECT syncObjectTypeID, objectID, key FROM syncDeleteLog"; + if (lastSyncDate) { + param = Zotero.Date.toUnixTimestamp(lastSyncDate); + sql += " WHERE timestamp>?"; + } + sql += " ORDER BY timestamp"; + var rows = Zotero.DB.query(sql, param); + + if (!rows) { + return false; + } + + var deletedIDs = {}; + for each(var Type in this.syncObjects) { + deletedIDs[Type.toLowerCase() + 's'] = []; + } + + for each(var row in rows) { + deletedIDs[this.getObjectTypeName(row.syncObjectTypeID) + 's'].push({ + id: row.objectID, + key: row.key + }); + } + return deletedIDs; + } + + + /** + * @param int deleteOlderThan Unix timestamp + */ + function purgeDeletedObjects(deleteOlderThan) { + if (isNaN(parseInt(deleteOlderThan))) { + throw ("Invalid timestamp '" + deleteOlderThan + + "' in Zotero.Sync.purgeDeletedObjects"); + } + var sql = "DELETE FROM syncDeleteLog WHERE timestamp<?"; + Zotero.DB.query(sql, { int: deleteOlderThan }); + } + + + function removeFromDeleted(deleted, id, key) { + for (var i=0; i<deleted.length; i++) { + if (deleted[i].id == id && deleted[i].key == key) { + deleted.splice(i, 1); + i--; + } + } + } + + + function _loadObjectTypes() { + var sql = "SELECT * FROM syncObjectTypes"; + var types = Zotero.DB.query(sql); + for each(var type in types) { + _objectTypeNames[type.syncObjectTypeID] = type.name; + _objectTypeIDs[type.name] = type.syncObjectTypeID; + } + _typesLoaded = true; + } +} + + + +/** + * Notifier observer to add deleted objects to syncDeleteLog + * plus related methods + */ +Zotero.Sync.EventListener = new function () { + default xml namespace = ''; + + this.init = init; + this.ignoreDeletions = ignoreDeletions; + this.unignoreDeletions = unignoreDeletions; + this.notify = notify; + + var _notifierObserver = false; + var _shutdown = false; + var _deleteBlacklist = {}; + + + function init() { + // Initialize delete log listener + _notifierObserver = Zotero.Notifier.registerObserver(this); + + // Register shutdown handler + var observerService = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + observerService.addObserver(this, "xpcom-shutdown", false); + observerService = null; + } + + + /** + * Blacklist objects from going into the sync delete log + */ + function ignoreDeletions(type, ids) { + var cap = type[0].toUpperCase() + type.substr(1); + if (Zotero.Sync.syncObjects.indexOf(cap) == -1) { + throw ("Invalid type '" + type + + "' in Zotero.Sync.EventListener.ignoreDeletions()"); + } + + if (!_deleteBlacklist[type]) { + _deleteBlacklist[type] = {}; + } + + ids = Zotero.flattenArguments(ids); + for each(var id in ids) { + _deleteBlacklist[type][id] = true; + } + } + + + /** + * Remove objects blacklisted from the sync delete log + */ + function unignoreDeletions(type, ids) { + var cap = type[0].toUpperCase() + type.substr(1); + if (Zotero.Sync.syncObjects.indexOf(cap) == -1) { + throw ("Invalid type '" + type + + "' in Zotero.Sync.EventListener.ignoreDeletions()"); + } + + ids = Zotero.flattenArguments(ids); + for each(var id in ids) { + if (_deleteBlacklist[type][id]) { + delete _deleteBlacklist[type][id]; + } + } + } + + + function notify(event, type, ids, extraData) { + var objectTypeID = Zotero.Sync.getObjectTypeID(type); + if (!objectTypeID) { + return; + } + + var ZU = new Zotero.Utilities; + + Zotero.DB.beginTransaction(); + + if (event == 'delete') { + var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?, ?)"; + var statement = Zotero.DB.getStatement(sql); + + var ts = Zotero.Date.getUnixTimestamp(); + + for(var i=0, len=ids.length; i<len; i++) { + if (_deleteBlacklist[ids[i]]) { + Zotero.debug("Not logging blacklisted '" + + type + "' id " + ids[i] + + " in Zotero.Sync.EventListener.notify()", 4); + continue; + } + + var key = extraData[ids[i]].old.primary.key; + + statement.bindInt32Parameter(0, objectTypeID); + statement.bindInt32Parameter(1, ids[i]); + statement.bindStringParameter(2, key); + statement.bindInt32Parameter(3, ts); + + try { + statement.execute(); + } + catch(e) { + statement.reset(); + Zotero.DB.rollbackTransaction(); + throw(Zotero.DB.getLastErrorString()); + } + } + + statement.reset(); + } + + Zotero.DB.commitTransaction(); + } + + /* + * Shutdown observer -- implements nsIObserver + */ + function observe(subject, topic, data) { + switch (topic) { + case 'xpcom-shutdown': + if (_shutdown) { + Zotero.debug('returning'); + return; + } + + Zotero.debug('Shutting down sync system'); + Zotero.Notifier.unregisterObserver(_notifierObserver); + _shutdown = true; + break; + } + } +} + + + +/** + * Methods for syncing with the Zotero Server + */ +Zotero.Sync.Server = new function () { + this.login = login; + this.sync = sync; + this.lock = lock; + this.unlock = unlock; + this.clear = clear; + this.resetServer = resetServer; + this.resetClient = resetClient; + this.logout = logout; + + this.__defineGetter__('username', function () { + return Zotero.Prefs.get('sync.server.username'); + }); + + this.__defineGetter__('password', function () { + if (!this.username) { + Zotero.debug('Username not set before setting Zotero.Sync.Server.password'); + return ''; + } + + Zotero.debug('Getting Zotero sync password'); + var loginManager = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + var logins = loginManager.findLogins({}, _loginManagerHost, _loginManagerURL, null); + + // Find user from returned array of nsILoginInfo objects + for (var i = 0; i < logins.length; i++) { + if (logins[i].username == this.username) { + return logins[i].password; + } + } + + return ''; + }); + + this.__defineSetter__('password', function (password) { + _sessionID = null; + + if (!this.username) { + Zotero.debug('Username not set before setting Zotero.Sync.Server.password'); + return; + } + + if (!password) { + Zotero.debug('Password empty setting Zotero.Sync.Server.password'); + return; + } + + var loginManager = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + + var logins = loginManager.findLogins({}, _loginManagerHost, _loginManagerURL, null); + + for (var i = 0; i < logins.length; i++) { + Zotero.debug('Clearing Zotero sync passwords'); + loginManager.removeLogin(logins[i]); + break; + } + + if (password) { + var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1", + Components.interfaces.nsILoginInfo, "init"); + + Zotero.debug('Setting Zotero sync password'); + var loginInfo = new nsLoginInfo(_loginManagerHost, _loginManagerURL, + null, this.username, password, "", ""); + loginManager.addLogin(loginInfo); + } + }); + + this.__defineGetter__("sessionIDComponent", function () { + return 'sessionid=' + _sessionID; + }); + this.__defineGetter__("lastRemoteSyncTime", function () { + return Zotero.DB.valueQuery("SELECT version FROM version WHERE schema='lastremotesync'"); + }); + this.__defineSetter__("lastRemoteSyncTime", function (val) { + Zotero.DB.query("REPLACE INTO version VALUES ('lastremotesync', ?)", { int: val }); + }); + this.__defineGetter__("lastLocalSyncTime", function () { + return Zotero.DB.valueQuery("SELECT version FROM version WHERE schema='lastlocalsync'"); + }); + this.__defineSetter__("lastLocalSyncTime", function (val) { + Zotero.DB.query("REPLACE INTO version VALUES ('lastlocalsync', ?)", { int: val }); + }); + + this.nextLocalSyncDate = false; + this.apiVersion = 1; + + default xml namespace = ''; + + var _loginManagerHost = 'chrome://zotero'; + var _loginManagerURL = 'Zotero Sync Server'; + + var _serverURL = "https://syncdev.zotero.org/"; + + var _maxAttempts = 3; + var _attempts = _maxAttempts; + var _syncInProgress; + + var _apiVersionComponent = "version=" + this.apiVersion; + var _sessionID; + var _sessionLock; + + + function login(callback) { + var url = _serverURL + "login"; + + var username = Zotero.Sync.Server.username; + + if (!username) { + _error("Username not set in Zotero.Sync.Server.login()"); + } + else if (!username.match(/^\w+$/)) { + _error("Invalid username '" + username + "' in Zotero.Sync.Server.login()"); + } + + var password = encodeURIComponent(Zotero.Sync.Server.password); + var body = _apiVersionComponent + + "&username=" + username + + "&password=" + password; + + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + _checkResponse(xmlhttp); + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + if (response.firstChild.getAttribute('type') == 'forbidden' + && response.firstChild.getAttribute('code') == 'INVALID_LOGIN') { + _error('Invalid login/pass'); + } + _error(response.firstChild.firstChild.nodeValue); + } + + if (_sessionID) { + _error("Session ID already set in Zotero.Sync.Server.login()") + } + + // <response><sessionID>[abcdefg0-9]{32}</sessionID></response> + _sessionID = response.firstChild.firstChild.nodeValue; + + var re = /^[abcdefg0-9]{32}$/; + if (!re.test(_sessionID)) { + _sessionID = null; + _error('Invalid session ID received from server'); + } + + + Zotero.debug('Got session ID ' + _sessionID + ' from server'); + + if (callback) { + callback(); + } + }); + } + + + function sync() { + if (_attempts < 0) { + _error('Too many attempts in Zotero.Sync.Server.sync()'); + } + + if (!_sessionID) { + Zotero.debug("Session ID not available -- logging in"); + this.login(Zotero.Sync.Server.sync); + return; + } + + if (!_sessionLock) { + Zotero.Sync.Server.lock(Zotero.Sync.Server.sync); + return; + } + + if (_syncInProgress) { + Zotero.log("Sync operation already in progress", 'error'); + return; + + } + + _syncInProgress = true; + + // Get updated data + var url = _serverURL + 'updated'; + var lastsync = Zotero.Sync.Server.lastRemoteSyncTime; + // TODO: use full sync instead? or make this full sync? + if (!lastsync) { + lastsync = 1; + } + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent + + '&lastsync=' + lastsync; + + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + Zotero.debug(xmlhttp.responseText); + + _checkResponse(xmlhttp); + if (_invalidSession(xmlhttp)) { + Zotero.debug("Invalid session ID -- logging in"); + _sessionID = false; + _syncInProgress = false; + Zotero.Sync.Server.login(Zotero.Sync.Server.sync); + return; + } + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + // handle error + Zotero.debug(xmlhttp.responseText); + _error(response.firstChild.firstChild.nodeValue); + } + + // Strip XML declaration + var xml = new XML(xmlhttp.responseText.replace(/<\?xml.*\?>/, '')); + + Zotero.DB.beginTransaction(); + + try { + Zotero.UnresponsiveScriptIndicator.disable(); + + var lastLocalSyncTime = Zotero.Sync.Server.lastLocalSyncTime; + var lastLocalSyncDate = lastLocalSyncTime ? + new Date(lastLocalSyncTime * 1000) : false; + + var uploadIDs = Zotero.Sync.buildUploadIDs(); + uploadIDs.updated = Zotero.Sync.getUpdatedObjects(lastLocalSyncDate); + var deleted = Zotero.Sync.getDeletedObjects(lastLocalSyncDate); + if (deleted == -1) { + _error('Sync delete log starts after last sync date in Zotero.Sync.Server.sync()'); + } + if (deleted) { + uploadIDs.deleted = deleted; + } + + var nextLocalSyncDate = Zotero.DB.transactionDate; + var nextLocalSyncTime = Zotero.Date.toUnixTimestamp(nextLocalSyncDate); + Zotero.Sync.Server.nextLocalSyncDate = nextLocalSyncDate; + + // Reconcile and save updated data from server and + // prepare local data to upload + var xmlstr = Zotero.Sync.Server.Data.processUpdatedXML( + xml.updated, lastLocalSyncDate, uploadIDs + ); + + if (xmlstr === false) { + Zotero.debug("Sync cancelled"); + Zotero.DB.rollbackTransaction(); + Zotero.Sync.Server.unlock(); + Zotero.reloadDataObjects(); + _syncInProgress = false; + return; + } + + if (xmlstr) { + Zotero.debug(xmlstr); + } + + //throw('break1'); + + Zotero.Sync.Server.lastRemoteSyncTime = response.getAttribute('timestamp'); + + if (!xmlstr) { + Zotero.debug("Nothing to upload to server"); + Zotero.Sync.Server.lastLocalSyncTime = nextLocalSyncTime; + Zotero.Sync.Server.nextLocalSyncDate = false; + Zotero.DB.commitTransaction(); + Zotero.Sync.Server.unlock(); + _syncInProgress = false; + return; + } + + Zotero.DB.commitTransaction(); + + var url = _serverURL + 'upload'; + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent + + '&data=' + encodeURIComponent(xmlstr); + + //var file = Zotero.getZoteroDirectory(); + //file.append('lastupload.txt'); + //Zotero.File.putContents(file, body); + + var uploadCallback = function (xmlhttp) { + _checkResponse(xmlhttp); + + //var ZU = new Zotero.Utilities; + //Zotero.debug(ZU.unescapeHTML(xmlhttp.responseText)); + Zotero.debug(xmlhttp.responseText); + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + // handle error + Zotero.debug(xmlhttp.responseText); + _error(response.firstChild.firstChild.nodeValue); + } + + Zotero.DB.beginTransaction(); + Zotero.Sync.purgeDeletedObjects(nextLocalSyncTime); + Zotero.Sync.Server.lastLocalSyncTime = nextLocalSyncTime; + Zotero.Sync.Server.nextLocalSyncDate = false; + Zotero.Sync.Server.lastRemoteSyncTime = response.getAttribute('timestamp'); + + //throw('break2'); + + Zotero.DB.commitTransaction(); + Zotero.Sync.Server.unlock(); + _syncInProgress = false; + } + + var compress = Zotero.Prefs.get('sync.server.compressData'); + // Compress upload data + if (compress) { + // Callback when compressed data is available + var bufferUploader = function (data) { + var gzurl = url + '?gzip=1'; + + var oldLen = body.length; + var newLen = data.length; + var savings = Math.round(((oldLen - newLen) / oldLen) * 100) + Zotero.debug("HTTP POST " + newLen + " bytes to " + gzurl + + " (gzipped from " + oldLen + " bytes; " + + savings + "% savings)"); + + if (Zotero.Utilities.HTTP.browserIsOffline()) { + Zotero.debug('Browser is offline'); + return false; + } + + var req = + Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(); + req.open('POST', gzurl, true); + req.setRequestHeader('Content-Type', "application/octet-stream"); + req.setRequestHeader('Content-Encoding', 'gzip'); + + req.onreadystatechange = function () { + if (req.readyState == 4) { + uploadCallback(req); + } + }; + try { + req.sendAsBinary(data); + } + catch (e) { + _error(e); + } + } + + // Get input stream from POST data + var unicodeConverter = + Components.classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + unicodeConverter.charset = "UTF-8"; + var bodyStream = unicodeConverter.convertToInputStream(body); + + // Get listener for when compression is done + var listener = new Zotero.BufferedInputListener(bufferUploader); + + // Initialize stream converter + var converter = + Components.classes["@mozilla.org/streamconv;1?from=uncompressed&to=gzip"] + .createInstance(Components.interfaces.nsIStreamConverter); + converter.asyncConvertData("uncompressed", "gzip", listener, null); + + // Send input stream to stream converter + var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"]. + createInstance(Components.interfaces.nsIInputStreamPump); + pump.init(bodyStream, -1, -1, 0, 0, true); + pump.asyncRead(converter, null); + } + + // Don't compress upload data + else { + Zotero.Utilities.HTTP.doPost(url, body, uploadCallback); + } + } + catch (e) { + _error(e); + } + finally { + Zotero.UnresponsiveScriptIndicator.enable(); + } + + _resetAttempts(); + }); + + return; + } + + + function lock(callback) { + Zotero.debug("Getting session lock"); + + if (_attempts < 0) { + _error('Too many attempts in Zotero.Sync.Server.lock()', 2); + } + + if (!_sessionID) { + _error('No session available in Zotero.Sync.Server.lock()', 2); + } + + if (_sessionLock) { + _error('Session already locked in Zotero.Sync.Server.lock()', 2); + } + + var url = _serverURL + "lock"; + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent; + + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + if (_invalidSession(xmlhttp)) { + Zotero.debug("Invalid session ID -- logging in"); + _sessionID = false; + Zotero.Sync.Server.login(callback); + return; + } + + _checkResponse(xmlhttp); + + Zotero.debug(xmlhttp.responseText); + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + _error(response.firstChild.firstChild.nodeValue); + } + + if (response.firstChild.tagName != 'locked') { + _error('Invalid response from server'); + } + + _sessionLock = true; + + if (callback) { + callback(); + } + }); + } + + + function unlock(callback) { + Zotero.debug("Releasing session lock"); + + if (_attempts < 0) { + _error('Too many attempts in Zotero.Sync.Server.unlock()'); + } + + if (!_sessionID) { + _error('No session available in Zotero.Sync.Server.unlock()'); + } + + var url = _serverURL + "unlock"; + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent; + + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + _checkResponse(xmlhttp); + + Zotero.debug(xmlhttp.responseText); + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + _error(response.firstChild.firstChild.nodeValue); + } + + if (response.firstChild.tagName != 'unlocked') { + _error('Invalid response from server'); + } + + _sessionLock = null; + + if (callback) { + callback(); + } + }); + } + + + function clear() { + if (_attempts < 0) { + _error('Too many attempts in Zotero.Sync.Server.clear()'); + } + + if (!_sessionID) { + Zotero.debug("Session ID not available -- logging in"); + this.login(Zotero.Sync.Server.clear); + return; + } + + var url = _serverURL + "clear"; + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent; + + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + if (_invalidSession(xmlhttp)) { + Zotero.debug("Invalid session ID -- logging in"); + _sessionID = false; + Zotero.Sync.Server.login(Zotero.Sync.Server.clear); + return; + } + + _checkResponse(xmlhttp); + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + _error(response.firstChild.firstChild.nodeValue); + } + + if (response.firstChild.tagName != 'cleared') { + _error('Invalid response from server'); + } + + Zotero.Sync.Server.resetClient(); + }); + + _resetAttempts(); + } + + + /** + * Clear session lock on server + */ + function resetServer() { + if (_attempts < 0) { + _error('Too many attempts in Zotero.Sync.Server.resetServer()'); + } + + if (!_sessionID) { + Zotero.debug("Session ID not available -- logging in"); + this.login(Zotero.Sync.Server.resetServer); + return; + } + + var url = _serverURL + "reset"; + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent; + + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + if (_invalidSession(xmlhttp)) { + Zotero.debug("Invalid session ID -- logging in"); + _sessionID = false; + Zotero.Sync.Server.login(Zotero.Sync.Server.reset); + return; + } + + _checkResponse(xmlhttp); + + Zotero.debug(xmlhttp.responseText); + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + _error(response.firstChild.firstChild.nodeValue); + } + + if (response.firstChild.tagName != 'reset') { + _error('Invalid response from server'); + } + + _syncInProgress = false; + }); + + _resetAttempts(); + } + + + function resetClient() { + Zotero.DB.beginTransaction(); + + var sql = "DELETE FROM version WHERE schema IN " + + "('lastlocalsync', 'lastremotesync', 'syncdeletelog')"; + Zotero.DB.query(sql); + + Zotero.DB.query("DELETE FROM syncDeleteLog"); + + sql = "INSERT INTO version VALUES ('syncdeletelog', ?)"; + Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp()); + + Zotero.DB.commitTransaction(); + } + + + function logout(callback) { + var url = _serverURL + "logout"; + var body = _apiVersionComponent + + '&' + Zotero.Sync.Server.sessionIDComponent; + + _sessionID = null; + + Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) { + _checkResponse(xmlhttp); + Zotero.debug(xmlhttp.responseText); + + var response = xmlhttp.responseXML.childNodes[0]; + + if (response.firstChild.tagName == 'error') { + _error(response.firstChild.firstChild.nodeValue); + } + + if (response.firstChild.tagName != 'loggedout') { + _error('Invalid response from server'); + } + + if (callback) { + callback(); + } + }); + } + + + function _checkResponse(xmlhttp) { + if (!xmlhttp.responseXML || + !xmlhttp.responseXML.childNodes[0] || + xmlhttp.responseXML.childNodes[0].tagName != 'response') { + Zotero.debug(xmlhttp.responseText); + _error('Invalid response from server'); + } + + if (!xmlhttp.responseXML.childNodes[0].firstChild) { + _error('Empty response from server'); + } + } + + + function _invalidSession(xmlhttp) { + if (xmlhttp.responseXML.childNodes[0].firstChild.tagName != 'error') { + return false; + } + + var code = xmlhttp.responseXML.childNodes[0].firstChild.getAttribute('code'); + return (code == 'INVALID_SESSION_ID') || (code == 'SESSION_TIMED_OUT'); + } + + + function _resetAttempts() { + _attempts = _maxAttempts; + } + + + function _error(e) { + _resetAttempts(); + Zotero.DB.rollbackAllTransactions(); + throw(e); + } +} + + + + +Zotero.BufferedInputListener = function (callback) { + this._callback = callback; +} + +Zotero.BufferedInputListener.prototype = { + binaryInputStream: null, + size: 0, + data: '', + + onStartRequest: function(request, context) {}, + + onStopRequest: function(request, context, status) { + this.binaryInputStream.close(); + delete this.binaryInputStream; + + this._callback(this.data); + }, + + onDataAvailable: function(request, context, inputStream, offset, count) { + this.size += count; + + this.binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"] + .createInstance(Components.interfaces.nsIBinaryInputStream) + this.binaryInputStream.setInputStream(inputStream); + this.data += this.binaryInputStream.readBytes(this.binaryInputStream.available()); + }, + + QueryInterface: function (iid) { + if (iid.equals(Components.interfaces.nsISupports) + || iid.equals(Components.interfaces.nsIStreamListener)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + + + + +Zotero.Sync.Server.Data = new function() { + this.processUpdatedXML = processUpdatedXML; + this.buildUploadXML = buildUploadXML; + this.itemToXML = itemToXML; + this.xmlToItem = xmlToItem; + this.collectionToXML = collectionToXML; + this.xmlToCollection = xmlToCollection; + this.creatorToXML = creatorToXML; + this.xmlToCreator = xmlToCreator; + + default xml namespace = ''; + + + function processUpdatedXML(xml, lastLocalSyncDate, uploadIDs) { + if (xml.children().length() == 0) { + Zotero.debug('No changes received from server'); + return Zotero.Sync.Server.Data.buildUploadXML(uploadIDs); + } + + var remoteCreatorStore = {}; + + Zotero.DB.beginTransaction(); + + for each(var Type in Zotero.Sync.syncObjects) { + var Types = Type + 's'; // 'Items' + var type = Type.toLowerCase(); // 'item' + var types = type + 's'; // 'items' + + if (!xml[types]) { + continue; + } + + Zotero.debug("Processing remotely changed " + types); + + var toSaveParents = []; + var toSaveChildren = []; + var toDeleteParents = []; + var toDeleteChildren = []; + var toReconcile = []; + + typeloop: + for each(var xmlNode in xml[types][type]) { + // Get local object with same id + var obj = Zotero[Types].get(parseInt(xmlNode.@id)); + + // TODO: check local deleted items for possible conflict + + if (obj) { + // Key match -- same item + if (obj.key == xmlNode.@key.toString()) { + var objDate = Zotero.Date.sqlToDate(obj.dateModified, true); + + // Local object has been modified since last sync + if ((objDate > lastLocalSyncDate && + objDate < Zotero.Sync.Server.nextLocalSyncDate) + // Check for object in updated array, since it might + // have been modified during sync process, making its + // date equal to Zotero.Sync.Server.nextLocalSyncDate + // and therefore excluded above (example: an item + // linked to a creator whose id changed) + || uploadIDs.updated[types].indexOf(obj.id) != -1) { + + var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); + + /* + // For now, show item conflicts even if only + // dateModified changed, since we need to handle + // creator conflicts there + if (type != 'item') { + // Skip if only dateModified changed + var diff = obj.diff(remoteObj, false, true); + if (!diff) { + continue; + } + } + */ + + // Will be handled by item CR for now + if (type == 'creator') { + remoteCreatorStore[remoteObj.id] = remoteObj; + continue; + } + + if (type != 'item') { + alert('Reconciliation unimplemented for ' + types); + _error('Reconciliation unimplemented for ' + types); + } + + // TODO: order reconcile by parent/child? + + toReconcile.push([ + obj, + remoteObj + ]); + + continue; + } + // Local object hasn't been modified -- overwrite + else { + obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj); + } + } + + // Key mismatch -- different objects with same id, + // so change id of local object + else { + var oldID = parseInt(xmlNode.@id); + + // Don't use assigned-but-unsaved ids for the new id + var skip = []; + for each(var o in toSaveParents) { + skip.push(o.id); + } + for each(var o in toSaveChildren) { + skip.push(o.id); + } + var newID = Zotero.ID.get(types, true, skip); + + Zotero.debug("Changing " + type + " " + oldID + " id to " + newID); + + // Save changed object now to update other linked objects + switch (type) { + case 'item': + obj.setField('itemID', newID); + break; + + default: + obj[type + 'ID'] = newID; + } + obj.save(); + + // Update id in local updates array + var index = uploadIDs.updated[types].indexOf(oldID); + if (index == -1) { + _error("Local " + type + " " + oldID + " not in " + + "update array when changing id"); + } + uploadIDs.updated[types][index] = newID; + + // Update id in local deletions array + for (var i in uploadIDs.deleted[types]) { + if (uploadIDs.deleted[types][i].id == oldID) { + uploadIDs.deleted[types][i] = newID; + } + } + + // Add items linked to creators to updated array, + // since their timestamps will be set to the + // transaction timestamp + if (type == 'creator') { + var linkedItems = obj.getLinkedItems(); + if (linkedItems) { + Zotero.Sync.addToUpdated(uploadIDs.updated.items, linkedItems); + } + } + + + // Note: Don't need to change collection children + // since they're stored as objects + + uploadIDs.changed[types][oldID] = { + oldID: oldID, + newID: newID + }; + + // Process new item + obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); + } + } + // Object doesn't exist + else { + // Reconcile locally deleted objects + for each(var pair in uploadIDs.deleted[types]) { + if (pair.id != parseInt(xmlNode.@id) || + pair.key != xmlNode.@key.toString()) { + continue; + } + + var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); + if (type != 'item') { + alert('Reconciliation unimplemented for ' + types); + _error('Reconciliation unimplemented for ' + types); + } + + // TODO: order reconcile by parent/child? + + toReconcile.push([ + 'deleted', + remoteObj + ]); + + break typeloop; + } + + // Create locally + obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); + } + + // Child items have to be saved after parent items + if (type == 'item' && obj.getSource()) { + toSaveChildren.push(obj); + } + else { + toSaveParents.push(obj); + } + } + + // Handle deleted objects + if (xml.deleted && xml.deleted[types]) { + Zotero.debug("Processing remotely deleted " + types); + + for each(var xmlNode in xml.deleted[types][type]) { + var id = parseInt(xmlNode.@id); + var obj = Zotero[Types].get(id); + // Object can't be found + if (!obj || obj.key != xmlNode.@key) { + continue; + } + + // Local object has been modified since last sync -- reconcile + var now = Zotero.Date.sqlToDate(obj.dateModified, true); + if (now >= lastLocalSyncDate) { + // TODO: order reconcile by parent/child + toReconcile.push([obj, 'deleted']); + } + // Local object hasn't been modified -- delete + else { + if (type == 'item' && obj.getSource()) { + toDeleteChildren.push(id); + } + else { + toDeleteParents.push(id); + } + } + } + } + + // Reconcile objects that have changed locally and remotely + if (toReconcile.length) { + var io = { + dataIn: { + captions: [ + // TODO: localize + 'Local Item', + 'Remote Item', + 'Merged Item' + ], + objects: toReconcile + } + }; + + if (type == 'item') { + io.dataIn.changedCreators = remoteCreatorStore; + } + + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var lastWin = wm.getMostRecentWindow("navigator:browser"); + lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io); + + if (io.dataOut) { + for each(var obj in io.dataOut) { + // TODO: do we need to make sure item isn't already being saved? + + if (obj.ref == 'deleted') { + // Deleted item was remote + if (obj.left != 'deleted') { + if (type == 'item' && obj.left.getSource()) { + toDeleteParents.push(obj.id); + } + else { + toDeleteChildren.push(obj.id); + } + + uploadIDs.deleted[types].push({ + id: obj.id, + key: obj.left.key + }); + } + continue; + } + + if (type == 'item' && obj.ref.getSource()) { + toSaveParents.push(obj.ref); + } + else { + toSaveChildren.push(obj.ref); + } + + // Item had been deleted locally, so remove from + // deleted array + if (obj.left == 'deleted') { + Zotero.Sync.removeFromDeleted(uploadIDs.deleted[types], obj.id, obj.ref.key); + } + + // TODO: only upload if the local item was chosen + // or remote item was changed + + Zotero.Sync.addToUpdated(uploadIDs.updated[types], obj.id); + } + } + else { + Zotero.DB.rollbackTransaction(); + return false; + } + } + + if (type == 'collection') { + var collections = []; + + // Sort collections in order of parent collections, + // so referenced parent collections always exist when saving + var cmp = function (a, b) { + var pA = a.parent; + var pB = b.parent; + if (pA == pB) { + return 0; + } + return (pA < pB) ? -1 : 1; + }; + toSaveParents.sort(cmp); + } + + Zotero.debug('Saving merged ' + types); + for each(var obj in toSaveParents) { + // If collection, temporarily clear subcollections before + // saving since referenced collections may not exist yet + if (type == 'collection') { + var childCollections = obj.getChildCollections(true); + if (childCollections) { + obj.childCollections = []; + } + } + + var id = obj.save(); + + // Store subcollections + if (type == 'collection') { + collections.push({ + obj: obj, + childCollections: childCollections + }); + } + } + for each(var obj in toSaveChildren) { + obj.save(); + } + + // Set subcollections + if (type == 'collection') { + for each(var collection in collections) { + if (collection.collections) { + collection.obj.childCollections = collection.collections; + collection.obj.save(); + } + } + } + + + // Delete + Zotero.debug('Deleting merged ' + types); + if (toDeleteChildren.length) { + Zotero.Sync.EventListener.ignoreDeletions(type, toDeleteChildren); + Zotero[Types].erase(toDeleteChildren); + Zotero.Sync.EventListener.unignoreDeletions(type, toDeleteChildren); + } + if (toDeleteParents.length) { + Zotero.Sync.EventListener.ignoreDeletions(type, toDeleteParents); + Zotero[Types].erase(toDeleteParents); + Zotero.Sync.EventListener.unignoreDeletions(type, toDeleteParents); + } + } + + var xmlstr = Zotero.Sync.Server.Data.buildUploadXML(uploadIDs); + + Zotero.DB.commitTransaction(); + + return xmlstr; + } + + + /** + * ids = { + * items: [123, 234, 345, 456], + * creators: [321, 432, 543, 654], + * changed: { + * items: { + * oldID: { oldID: 1234, newID: 5678 }, ... + * }, + * creators: { + * oldID: { oldID: 1234, newID: 5678 }, ... + * } + * }, + * deleted: { + * items: [ + * { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ... + * ], + * creators: [ + * { id: 1234, key: ABCDEFGHIJKMNPQRSTUVWXYZ23456789 }, ... + * ] + * } + * }; + */ + function buildUploadXML(ids) { + var xml = <data/> + + // Add API version attribute + xml.@version = Zotero.Sync.Server.apiVersion; + + + // Updates + for each(var Type in Zotero.Sync.syncObjects) { + var Types = Type + 's'; // 'Items' + var type = Type.toLowerCase(); // 'item' + var types = type + 's'; // 'items' + + if (!ids.updated[types]) { + continue; + } + + Zotero.debug("Processing locally changed " + types); + + switch (type) { + // Items.get() can take multiple ids, + // so we handle it differently + case 'item': + var objs = Zotero[Types].get(ids.updated[types]); + for each(var obj in objs) { + xml[types][type] += this[type + 'ToXML'](obj); + } + break; + + default: + for each(var id in ids.updated[types]) { + var obj = Zotero[Types].get(id); + xml[types][type] += this[type + 'ToXML'](obj); + } + } + } + + // TODO: handle changed ids + + // Deletions + for each(var Type in Zotero.Sync.syncObjects) { + var Types = Type + 's'; // 'Items' + var type = Type.toLowerCase(); // 'item' + var types = type + 's'; // 'items' + + if (!ids.deleted[types]) { + continue; + } + + Zotero.debug('Processing locally deleted ' + types); + + for each(var obj in ids.deleted[types]) { + var deletexml = new XML('<' + type + '/>'); + deletexml.@id = obj.id; + deletexml.@key = obj.key; + xml.deleted[types][type] += deletexml; + } + } + + var xmlstr = xml.toXMLString(); + if (xmlstr.match('<data version="[0-9]+"/>')) { + return ''; + } + + return xmlstr; + } + + + /** + * Converts a Zotero.Item object to an E4X <item> object + */ + function itemToXML(item) { + var xml = <item/>; + var item = item.serialize(); + + // Primary fields + for (var field in item.primary) { + switch (field) { + case 'itemID': + var attr = 'id'; + break; + + default: + var attr = field; + } + xml['@' + attr] = item.primary[field]; + } + + // Item data + for (var field in item.fields) { + if (!item.fields[field]) { + continue; + } + var newField = <field>{item.fields[field]}</field>; + newField.@name = field; + xml.field += newField; + } + + if (item.primary.itemType == 'note' || item.primary.itemType == 'attachment') { + if (item.sourceItemID) { + xml.@sourceItemID = item.sourceItemID; + } + } + + // Note + if (item.primary.itemType == 'note') { + var note = <note>{item.note}</note>; + xml.note += note; + } + + // Attachment + if (item.primary.itemType == 'attachment') { + xml.@linkMode = item.attachment.linkMode; + xml.@mimeType = item.attachment.mimeType; + xml.@charset = item.attachment.charset; + + // Don't include paths for links + if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { + var path = <path>{item.attachment.path}</path>; + xml.path += path; + } + + if (item.note) { + var note = <note>{item.note}</note>; + xml.note += note; + } + } + + // Creators + for (var index in item.creators) { + var newCreator = <creator/>; + newCreator.@id = item.creators[index].creatorID; + newCreator.@creatorType = item.creators[index].creatorType; + newCreator.@index = index; + xml.creator += newCreator; + } + + return xml; + } + + + /** + * Convert E4X <item> object into an unsaved Zotero.Item + * + * @param object xmlItem E4X XML node with item data + * @param object item (Optional) Existing Zotero.Item to update + * @param bool newID (Optional) Ignore passed itemID and choose new one + */ + function xmlToItem(xmlItem, item, newID) { + if (!item) { + if (newID) { + item = new Zotero.Item(null); + } + else { + item = new Zotero.Item(parseInt(xmlItem.@id)); + /* + if (item.exists()) { + _error("Item specified in XML node already exists " + + "in Zotero.Sync.Server.Data.xmlToItem()"); + } + */ + } + } + else if (newID) { + _error("Cannot use new id with existing item in " + + "Zotero.Sync.Server.Data.xmlToItem()"); + } + + // TODO: add custom item types + + var data = { + itemTypeID: Zotero.ItemTypes.getID(xmlItem.@itemType.toString()), + dateAdded: xmlItem.@dateAdded.toString(), + dateModified: xmlItem.@dateModified.toString(), + key: xmlItem.@key.toString() + }; + + var changedFields = {}; + + // Primary data + for (var field in data) { + item.setField(field, data[field]); + changedFields[field] = true; + } + + // Item data + for each(var field in xmlItem.field) { + var fieldName = field.@name.toString(); + item.setField(fieldName, field.toString()); + changedFields[fieldName] = true; + } + var previousFields = item.getUsedFields(true); + for each(var field in previousFields) { + if (!changedFields[field] && + // If not valid, it'll already have been cleared by the + // type change + Zotero.ItemFields.isValidForType( + Zotero.ItemFields.getID(field), data.itemTypeID + )) { + item.setField(field, false); + } + } + + // Item creators + var i = 0; + for each(var creator in xmlItem.creator) { + var pos = parseInt(creator.@index); + if (pos != i) { + _error('No creator in position ' + i); + } + + item.setCreator( + pos, + Zotero.Creators.get(parseInt(creator.@id)), + creator.@creatorType.toString() + ); + i++; + } + + // Remove item's remaining creators not in XML + var numCreators = item.numCreators(); + var rem = numCreators - i; + for (var j=0; j<rem; j++) { + // Keep removing last creator + item.removeCreator(i); + } + + // Both notes and attachments might have parents and notes + if (item.isNote() || item.isAttachment()) { + var sourceItemID = parseInt(xmlItem.@sourceItemID); + item.setSource(sourceItemID ? sourceItemID : false); + item.setNote(xmlItem.note.toString()); + } + + // Attachment metadata + if (item.isAttachment()) { + item.attachmentLinkMode = parseInt(xmlItem.@linkMode); + item.attachmentMIMEType = xmlItem.@mimeType; + item.attachmentCharset = parseInt(xmlItem.@charsetID); + if (item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { + item.attachmentPath = xmlItem.path.toString(); + } + } + + return item; + } + + + function collectionToXML(collection) { + var xml = <collection/>; + + xml.@id = collection.id; + xml.@name = collection.name; + xml.@dateModified = collection.dateModified; + xml.@key = collection.key; + if (collection.parent) { + xml.@parent = collection.parent; + } + + var children = collection.getChildren(); + if (children) { + xml.collections = ''; + xml.items = ''; + for each(var child in children) { + if (child.type == 'collection') { + xml.collections = xml.collections ? + xml.collections + ' ' + child.id : child.id; + } + else if (child.type == 'item') { + xml.items = xml.items ? + xml.items + ' ' + child.id : child.id; + } + } + if (xml.collections == '') { + delete xml.collections; + } + if (xml.items == '') { + delete xml.items; + } + } + + return xml; + } + + + /** + * Convert E4X <collection> object into an unsaved Zotero.Collection + * + * @param object xmlCollection E4X XML node with collection data + * @param object item (Optional) Existing Zotero.Collection to update + * @param bool newID (Optional) Ignore passed collectionID and choose new one + */ + function xmlToCollection(xmlCollection, collection, newID) { + if (!collection) { + if (newID) { + collection = new Zotero.Collection(null); + } + else { + collection = new Zotero.Collection(parseInt(xmlCollection.@id)); + /* + if (collection.exists()) { + throw ("Collection specified in XML node already exists " + + "in Zotero.Sync.Server.Data.xmlToCollection()"); + } + */ + } + } + else if (newID) { + _error("Cannot use new id with existing collection in " + + "Zotero.Sync.Server.Data.xmlToCollection()"); + } + + collection.name = xmlCollection.@name.toString(); + collection.parent = xmlCollection.@parent.toString() ? + parseInt(xmlCollection.@parent) : false; + collection.dateModified = xmlCollection.@dateModified.toString(); + collection.key = xmlCollection.@key.toString(); + + // Subcollections + var str = xmlCollection.collections.toString(); + collection.childCollections = str == '' ? [] : str.split(' '); + + // Child items + var str = xmlCollection.items.toString(); + collection.childItems = str == '' ? [] : str.split(' '); + + return collection; + } + + + /** + * Converts a Zotero.Creator object to an E4X <creator> object + */ + function creatorToXML(creator) { + var xml = <creator/>; + var creator = creator.serialize(); + for (var field in creator.primary) { + switch (field) { + case 'creatorID': + var attr = 'id'; + break; + + default: + var attr = field; + } + xml['@' + attr] = creator.primary[field]; + } + + var allowEmpty = ['firstName', 'lastName', 'name']; + + for (var field in creator.fields) { + if (!creator.fields[field] && allowEmpty.indexOf(field) == -1) { + continue; + } + xml[field] = creator.fields[field]; + } + return xml; + } + + + /** + * Convert E4X <creator> object into an unsaved Zotero.Creator + * + * @param object xmlCreator E4X XML node with creator data + * @param object item (Optional) Existing Zotero.Creator to update + * @param bool newID (Optional) Ignore passed creatorID and choose new one + */ + function xmlToCreator(xmlCreator, creator, newID) { + if (!creator) { + if (newID) { + creator = new Zotero.Creator(null); + } + else { + creator = new Zotero.Creator(parseInt(xmlCreator.@id)); + /* + if (creator.exists()) { + throw ("Creator specified in XML node already exists " + + "in Zotero.Sync.Server.Data.xmlToCreator()"); + } + */ + } + } + else if (newID) { + _error("Cannot use new id with existing creator in " + + "Zotero.Sync.Server.Data.xmlToCreator()"); + } + + var data = { + dateModified: xmlCreator.@dateModified.toString(), + key: xmlCreator.@key.toString(), + birthYear: xmlCreator.birthYear.toString() + }; + + if (xmlCreator.fieldMode == 1) { + data.firstName = ''; + data.lastName = xmlCreator.name.toString(); + data.fieldMode = 1; + } + else { + data.firstName = xmlCreator.firstName.toString(); + data.lastName = xmlCreator.lastName.toString(); + data.fieldMode = 0; + } + + creator.setFields(data); + + return creator; + } +} diff --git a/chrome/content/zotero/xpcom/translate.js b/chrome/content/zotero/xpcom/translate.js @@ -1011,7 +1011,7 @@ Zotero.Translate.prototype._closeStreams = function() { Zotero.Translate.prototype._itemTagsAndSeeAlso = function(item, newItem) { // add to ID map if(item.itemID) { - this._IDMap[item.itemID] = newItem.getID(); + this._IDMap[item.itemID] = newItem.id; } // add see alsos if(item.seeAlso) { @@ -1087,7 +1087,10 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { var type = (item.itemType ? item.itemType : "webpage"); if(type == "note") { // handle notes differently - var myID = Zotero.Notes.add(item.note); + var item = new Zotero.Item(false, 'note'); + item.setNote(item.note); + var myID = item.save(); + // re-retrieve the item var newItem = Zotero.Items.get(myID); } else { @@ -1173,11 +1176,11 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { // add note if necessary if(item.note) { - newItem.updateNote(item.note); + newItem.setNote(item.note); } } else { var typeID = Zotero.ItemTypes.getID(type); - var newItem = new Zotero.Item(typeID); + var newItem = new Zotero.Item(false, typeID); } // makes looping through easier @@ -1196,17 +1199,32 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { if(data) { // if field has content if(field == "creators") { // creators are a special case for(var j in data) { - var creatorType = 1; // try to assign correct creator type if(data[j].creatorType) { - try { - var creatorType = Zotero.CreatorTypes.getID(data[j].creatorType); - } catch(e) { - Zotero.debug("Translate: invalid creator type "+data[j].creatorType+" for creator index "+j); - } + var creatorTypeID = Zotero.CreatorTypes.getID(data[j].creatorType); + } + if(!creatorTypeID) { + var creatorTypeID = 1; + } + + var fields = { + firstName: data[j].firstName, + lastName: data[j].lastName + }; + + var creatorDataID = Zotero.Creators.getDataID(fields); + if(creatorDataID) { + var linkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID); + // TODO: support identical creators via popup? ugh... + var creatorID = linkedCreators[0]; + var creator = Zotero.Creators.get(creatorID); + } else { + var creator = new Zotero.Creator; + creator.setFields(fields); + var creatorID = creator.save(); } - newItem.setCreator(j, data[j].firstName, data[j].lastName, creatorType); + newItem.setCreator(j, creator, creatorTypeID); } } else if(field == "seeAlso") { newItem.translateSeeAlso = data; @@ -1270,14 +1288,19 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { } else { var myID = newItem.save(); if(myID == true || !myID) { - myID = newItem.getID(); + myID = newItem.id; } } // handle notes if(item.notes) { for each(var note in item.notes) { - var noteID = Zotero.Notes.add(note.note, myID); + var item = new Zotero.Item(false, 'note'); + item.setNote(note.note); + if (myID) { + item.setSource(myID); + } + var noteID = item.save(); // handle see also var myNote = Zotero.Items.get(noteID); @@ -1420,7 +1443,11 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { } } - if(!attachedTo) this.runHandler("itemDone", newItem); + if(!attachedTo) { + // Re-retrieve item before passing to handler + newItem = Zotero.Items.get(newItem.id); + this.runHandler("itemDone", newItem); + } delete item; } @@ -1439,7 +1466,7 @@ Zotero.Translate.prototype._collectionDone = function(collection) { */ Zotero.Translate.prototype._processCollection = function(collection, parentID) { var newCollection = Zotero.Collections.add(collection.name, parentID); - var myID = newCollection.getID(); + var myID = newCollection.id; this.newCollections.push(myID); @@ -1756,7 +1783,7 @@ Zotero.Translate.prototype._export = function() { if(this.configOptions.getCollections) { // get child collections - this._collectionsLeft = Zotero.getCollections(this.collection.getID(), true); + this._collectionsLeft = Zotero.getCollections(this.collection.id, true); // get items in child collections for each(var collection in this._collectionsLeft) { this._itemsLeft = this._itemsLeft.concat(collection.getChildItems()); @@ -1976,7 +2003,7 @@ Zotero.Translate.prototype._exportToArray = function(returnItem) { returnItemArray.uniqueFields = new Object(); // get base fields, not just the type-specific ones - var itemTypeID = returnItem.getType(); + var itemTypeID = returnItem.itemTypeID; var allFields = Zotero.ItemFields.getItemTypeFields(itemTypeID); for each(var field in allFields) { var fieldName = Zotero.ItemFields.getName(field); diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js @@ -253,6 +253,56 @@ Zotero.Utilities.prototype.isInt = function(x) { return false; } + +/** + * Determine the necessary data type for SQLite parameter binding + * + * @return int 0 for string, 32 for int32, 64 for int64 + */ +Zotero.Utilities.prototype.getSQLDataType = function(value) { + var strVal = value + ''; + if (strVal.match(/^[1-9]+[0-9]*$/)) { + // These upper bounds also specified in Zotero.DB + // + // Store as 32-bit signed integer + if (value <= 2147483647) { + return 32; + } + // Store as 64-bit signed integer + // 2^53 is JS's upper-bound for decimal integers + else if (value < 9007199254740992) { + return 64; + } + } + return 0; +} + + +/* + * From http://developer.mozilla.org/en/docs/nsICryptoHash#Computing_the_Hash_of_a_String + */ +Zotero.Utilities.prototype.md5 = function(str) { + var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var result = {}; + var data = converter.convertToByteArray(str, result); + var ch = Components.classes["@mozilla.org/security/hash;1"] + .createInstance(Components.interfaces.nsICryptoHash); + ch.init(ch.MD5); + ch.update(data, data.length); + var hash = ch.finish(false); + + // return the two-digit hexadecimal code for a byte + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + // convert the binary hash data to a hex string. + return [toHexString(hash.charCodeAt(i)) for (i in hash)].join(""); +} + + /* * Get current zotero version */ @@ -562,8 +612,10 @@ Zotero.Utilities.HTTP = new function() { * * doGet can be called as: * Zotero.Utilities.HTTP.doGet(url, onDone) + * + * Returns the XMLHTTPRequest object **/ - function doGet(url, onDone, onError, responseCharset) { + function doGet(url, onDone, responseCharset) { Zotero.debug("HTTP GET "+url); if (this.browserIsOffline()){ return false; @@ -580,7 +632,7 @@ Zotero.Utilities.HTTP = new function() { xmlhttp.send(null); - return true; + return xmlhttp; } @@ -591,9 +643,19 @@ Zotero.Utilities.HTTP = new function() { * * doPost can be called as: * Zotero.Utilities.HTTP.doPost(url, body, onDone) + * + * Returns the XMLHTTPRequest object **/ function doPost(url, body, onDone, requestContentType, responseCharset) { - Zotero.debug("HTTP POST "+body+" to "+url); + var bodyStart = body.substr(0, 1024); + // Don't display password in console + bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********'); + + Zotero.debug("HTTP POST " + + (body.length > 1024 ? + bodyStart + '... (' + body.length + ' chars)' : bodyStart) + + " to " + url); + if (this.browserIsOffline()){ return false; } @@ -610,7 +672,7 @@ Zotero.Utilities.HTTP = new function() { xmlhttp.send(body); - return true; + return xmlhttp; } @@ -631,7 +693,7 @@ Zotero.Utilities.HTTP = new function() { xmlhttp.send(null); - return true; + return xmlhttp; } @@ -641,8 +703,7 @@ Zotero.Utilities.HTTP = new function() { * doOptions can be called as: * Zotero.Utilities.HTTP.doOptions(url, body, onDone) * - * The status handler, which doesn't really serve a very noticeable purpose - * in our code, is required for compatiblity with the Piggy Bank project + * Returns the XMLHTTPRequest object **/ function doOptions(url, body, onDone) { Zotero.debug("HTTP OPTIONS "+url); @@ -661,7 +722,7 @@ Zotero.Utilities.HTTP = new function() { xmlhttp.send(body); - return true; + return xmlhttp; } diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -60,6 +60,7 @@ var Zotero = new function(){ this.hasValues = hasValues; this.randomString = randomString; this.moveToUnique = moveToUnique; + this.reloadDataObjects = reloadDataObjects; // Public properties this.initialized = false; @@ -251,6 +252,8 @@ var Zotero = new function(){ Zotero.Integration.SOAP.init(); Zotero.Integration.init(); + Zotero.Sync.init(); + this.initialized = true; return true; @@ -467,8 +470,7 @@ var Zotero = new function(){ * |type| is a string with one of the flag types in nsIScriptError: * 'error', 'warning', 'exception', 'strict' */ - function log(message, type, sourceName, sourceLine, lineNumber, - columnNumber, category) { + function log(message, type, sourceName, sourceLine, lineNumber, columnNumber) { var consoleService = Components.classes["@mozilla.org/consoleservice;1"] .getService(Components.interfaces.nsIConsoleService); var scriptError = Components.classes["@mozilla.org/scripterror;1"] @@ -486,7 +488,7 @@ var Zotero = new function(){ lineNumber != undefined ? lineNumber : null, columnNumber != undefined ? columnNumber : null, flags, - category + 'XUL javascript' // DEBUG: this doesn't seem to work ); consoleService.logMessage(scriptError); } @@ -798,9 +800,11 @@ var Zotero = new function(){ /** * Generate a random string of length 'len' (defaults to 8) **/ - function randomString(len) { - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; - if (!len){ + function randomString(len, chars) { + if (!chars) { + chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"; + } + if (!len) { len = 8; } var randomstring = ''; @@ -821,6 +825,13 @@ var Zotero = new function(){ file.moveTo(newFile.parent, newName); return file; } + + + function reloadDataObjects() { + Zotero.Collections.reloadAll(); + Zotero.Creators.reloadAll(); + Zotero.Items.reloadAll(); + } }; @@ -1163,6 +1174,8 @@ Zotero.Date = new function(){ this.sqlHasYear = sqlHasYear; this.sqlHasMonth = sqlHasMonth; this.sqlHasDay = sqlHasDay; + this.getUnixTimestamp = getUnixTimestamp; + this.toUnixTimestamp = toUnixTimestamp; this.getFileDateString = getFileDateString; this.getFileTimeString = getFileTimeString; this.getLocaleDateOrder = getLocaleDateOrder; @@ -1223,12 +1236,7 @@ Zotero.Date = new function(){ var seconds = date.getUTCSeconds(); } else { - var year = date.getFullYear(); - var month = date.getMonth(); - var day = date.getDate(); - var hours = date.getHours(); - var minutes = date.getMinutes(); - var seconds = date.getSeconds(); + return date.toLocaleFormat('%Y-%m-%d %T'); } var utils = new Zotero.Utilities(); @@ -1483,9 +1491,12 @@ Zotero.Date = new function(){ } // Regexes for multipart and SQL dates - var _multipartRE = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2} /; - var _sqldateRE = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2}/; - var _sqldatetimeRE = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2} ([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-5][0-9])/; + // Allow zeroes in multipart dates + var _multipartRE = /^\-?[0-9]{4}\-[0-9]{2}\-[0-9]{2} /; + //var _sqldateRE = /^\-?[0-9]{4}\-[0-9]{2}\-[0-9]{2}/; + //var _sqldatetimeRE = /^\-?[0-9]{4}\-[0-9]{2}\-[0-9]{2} ([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-5][0-9])/; + var _sqldateRE = /^\-?[0-9]{4}\-(0[1-9]|10|11|12)\-(0[1-9]|[1-2][0-9]|30|31)$/; + var _sqldatetimeRE = /^\-?[0-9]{4}\-(0[1-9]|10|11|12)\-(0[1-9]|[1-2][0-9]|30|31) ([0-1][0-9]|[2][0-3]):([0-5][0-9]):([0-5][0-9])$/; /** * Tests if a string is a multipart date string @@ -1555,6 +1566,20 @@ Zotero.Date = new function(){ } + function getUnixTimestamp() { + return Math.round(Date.now() / 1000); + } + + + function toUnixTimestamp(date) { + if (date === null || typeof date != 'object' || + date.constructor.name != 'Date') { + throw ('Not a valid date in Zotero.Date.toUnixTimestamp()'); + } + return Math.round(date.getTime() / 1000); + } + + function getFileDateString(file){ var date = new Date(); date.setTime(file.lastModifiedTime); diff --git a/chrome/skin/default/zotero/bindings/itembox.css b/chrome/skin/default/zotero/bindings/itembox.css @@ -0,0 +1,104 @@ +scrollbox +{ + overflow: visible; +} + +row, tagsbox row +{ + margin: 0 0 1px; + min-height: 1em; +} + +textbox, tagsbox textbox +{ + margin-top: 0; + margin-bottom: -1px; +} + +#go-buttons button +{ + list-style-image: url('chrome://zotero/skin/toolbar-go-arrow.png'); + -moz-box-direction: reverse; + -moz-box-flex: 1; +} + +#go-buttons button[disabled=true] +{ + list-style-image: url('chrome://zotero/skin/toolbar-go-arrow-disabled.png'); +} + + +/* DEBUG: this doesn't seem to work, unfortunately +label[singleField=false]:after +{ + content:","; +} +*/ + +/* metadata field names */ +row > label:first-child, +row > toolbarbutton .toolbarbutton-text /* creator type menu */ +{ + text-align: right; + font-weight: bold; +} + +row label:first-child[isButton=true]:hover +{ + cursor: pointer; +} + +row label +{ + -moz-user-focus: ignore; +} + + /* creator type menu */ +row > toolbarbutton +{ + margin: 0 2px 0 0; + padding: 0 0 0 5px; + list-style-image: url("chrome://browser/skin/dropmark-nav.png"); + -moz-image-region: rect(3px, 14px, 19px, 0px); +} +row > toolbarbutton .toolbarbutton-text +{ + margin-top: -1px; +} +row > toolbarbutton .toolbarbutton-icon, +row > toolbarbutton .toolbarbutton-menu-dropmarker +{ + margin: 0; + padding: 0; +} + + +/* no space between last name and comma */ +row hbox label:first-child +{ + margin-right: 1px; +} + +row hbox label.comma +{ + +} + +row vbox[fieldname=abstractNote], +row vbox[fieldname=extra] +{ + margin-top: 1px; + margin-left: 6px; +} + +hbox.zotero-date-field-status +{ + margin-right: 5px; +} + +hbox.zotero-date-field-status label +{ + font-weight: bold; + color: #666; + margin: 0 0 0 1px; +} diff --git a/chrome/skin/default/zotero/bindings/noteeditor.css b/chrome/skin/default/zotero/bindings/noteeditor.css @@ -1,12 +1,8 @@ /* Don't collapse blank note parent labels, since it prevents access to parent */ -#citeLabel -{ - min-height: 1.25em; -} - -#citeLabel:hover +#citeLabel[onclick]:hover { cursor: pointer !important; + min-height: 1.25em; } #tagsPopup { diff --git a/chrome/skin/default/zotero/itemPane.css b/chrome/skin/default/zotero/itemPane.css @@ -1,85 +0,0 @@ -#zotero-editpane-dynamic-fields row, tagsbox row -{ - margin: 0 0 1px; -} - -#zotero-editpane-dynamic-fields textbox, tagsbox textbox -{ - margin-top: 0; - margin-bottom: -1px; -} - -/* DEBUG: this doesn't seem to work, unfortunately -#zotero-editpane-dynamic-fields label[singleField=false]:after -{ - content:","; -} -*/ - -/* metadata field names */ -#zotero-editpane-dynamic-fields row > label:first-child, -#zotero-editpane-dynamic-fields row > toolbarbutton .toolbarbutton-text /* creator type menu */ -{ - text-align: right; - font-weight: bold; -} - -#zotero-editpane-dynamic-fields row label:first-child[isButton=true]:hover -{ - cursor: pointer; -} - -#zotero-editpane-dynamic-fields row label -{ - -moz-user-focus: ignore; -} - - /* creator type menu */ -#zotero-editpane-dynamic-fields row > toolbarbutton -{ - margin: 0 2px 0 0; - padding: 0 0 0 5px; - list-style-image: url("chrome://browser/skin/dropmark-nav.png"); - -moz-image-region: rect(3px, 14px, 19px, 0px); -} -#zotero-editpane-dynamic-fields row > toolbarbutton .toolbarbutton-text -{ - margin-top: -1px; -} -#zotero-editpane-dynamic-fields row > toolbarbutton .toolbarbutton-icon, -#zotero-editpane-dynamic-fields row > toolbarbutton .toolbarbutton-menu-dropmarker -{ - margin: 0; - padding: 0; -} - - -/* no space between last name and comma */ -#zotero-editpane-dynamic-fields row hbox label:first-child -{ - margin-right: 1px; -} - -#zotero-editpane-dynamic-fields row hbox label.comma -{ - margin-left: 0; -} - -#zotero-editpane-dynamic-fields row vbox[fieldname=abstractNote], -#zotero-editpane-dynamic-fields row vbox[fieldname=extra] -{ - margin-top: 1px; - margin-left: 6px; -} - -#zotero-editpane-dynamic-fields hbox.zotero-date-field-status -{ - margin-right: 5px; -} - -#zotero-editpane-dynamic-fields hbox.zotero-date-field-status label -{ - font-weight: bold; - color: #666; - margin: 0 0 0 1px; -} diff --git a/chrome/skin/default/zotero/merge.css b/chrome/skin/default/zotero/merge.css @@ -0,0 +1,97 @@ +/* merge.xul */ +wizard { + padding-left: 10px; + padding-right: 10px; +} + +wizard { + height: 550px; +} + +wizardpage { + min-width: 770px; + min-height: 300px; +} + +wizard[zoterowidescreen=true] { + height: 718px; + width: 974px; +} + +wizard .wizard-header label.wizard-header-label { + margin-left: 0; +} + +/* different order on windows */ +wizard > hbox button[dlgtype=cancel] { + margin-left: 0; +} + +wizard > hbox button[dlgtype=next][disabled=false], +wizard > hbox button[dlgtype=next]:not([disabled]) { + margin-right: 0; +} + +wizard > hbox button:last-child { + margin-right: 0; +} + + +wizard > deck { + margin: 0; + padding: 0; +} + +#zotero-step-count { + -moz-box-pack: end; +} + +#zotero-step-count label:first-child { + margin-left: 0; + font-weight: bold; +} + +#zotero-step-count label { + font-size: 1.1em; +} + +#zotero-step-count label:last-child { + margin-right: 1em; + font-weight: bold; +} + + + +/* merge.xml */ +zoteromergegroup { + margin: 0; + padding: 0; + overflow-y: auto; +} + +zoteromergepane #delete-box { + min-width: 15em; + -moz-box-align: center; + -moz-box-pack: center; + font-weight: bold; +} + +zoteromergepane[selected=true] groupbox caption { + color: red; + font-weight: bold; +} + +zoteromergepane[id=leftpane]:not([selected=true]):hover groupbox caption, +zoteromergepane[id=rightpane]:not([selected=true]):hover groupbox caption { + /* font-weight: bold; */ +} + +hbox:not([mergetype=note]) zoteromergepane:active[id=leftpane] groupbox caption, +hbox:not([mergetype=note]) zoteromergepane:active[id=rightpane] groupbox caption { + color: red; + font-weight: bold; +} + +zoteromergepane[id=mergepane] { + min-width: 30em; +} diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css @@ -229,7 +229,6 @@ list-style-image: url('chrome://zotero/skin/search-cancel-active.png'); } -#zotero-go-to-url, #zotero-openurl, #zotero-attachment-view, #zotero-attachment-show { list-style-image: url('chrome://zotero/skin/toolbar-go-arrow.png'); @@ -258,11 +257,6 @@ } -#zotero-go-to-url[disabled=true], #zotero-openurl[disabled=true] -{ - list-style-image: url('chrome://zotero/skin/toolbar-go-arrow-disabled.png'); -} - #zotero-view-item > vbox { overflow: auto; diff --git a/chrome/skin/default/zotero/prefs-sync.png b/chrome/skin/default/zotero/prefs-sync.png Binary files differ. diff --git a/chrome/skin/default/zotero/zotero.css b/chrome/skin/default/zotero/zotero.css @@ -68,7 +68,7 @@ textbox[type="styled"] -moz-binding: url('chrome://zotero/content/bindings/styled-textbox.xml#styled-textbox'); } -noteeditor +zoteronoteeditor { -moz-binding: url('chrome://zotero/content/bindings/noteeditor.xml#note-editor'); } @@ -121,6 +121,18 @@ zoterosearchagefield -moz-binding: url('chrome://zotero/content/bindings/zoterosearch.xml#search-in-the-last'); } +zoteroitembox { + -moz-binding: url('chrome://zotero/content/bindings/itembox.xml#item-box'); +} + +zoteromergegroup { + -moz-binding: url('chrome://zotero/content/bindings/merge.xml#merge-group'); +} + +zoteromergepane { + -moz-binding: url('chrome://zotero/content/bindings/merge.xml#merge-pane'); +} + .zotero-clicky { -moz-border-radius: 6px; diff --git a/components/zotero-autocomplete.js b/components/zotero-autocomplete.js @@ -63,6 +63,11 @@ ZoteroAutoCompleteResult.prototype.getCommentAt = function(index){ } +ZoteroAutoCompleteResult.prototype.getImageAt = function(index) { + return null; +} + + ZoteroAutoCompleteResult.prototype.getStyleAt = function(index){ return null; } @@ -151,7 +156,7 @@ ZoteroAutoComplete.prototype.startSearch = function(searchString, searchParam, { var sql = "SELECT DISTINCT CASE fieldMode WHEN 1 THEN lastName " + "WHEN 0 THEN firstName || ' ' || lastName END AS name " - + "FROM creators WHERE CASE fieldMode " + + "FROM creators NATURAL JOIN creatorData WHERE CASE fieldMode " + "WHEN 1 THEN lastName " + "WHEN 0 THEN firstName || ' ' || lastName END " + "LIKE ? ORDER BY name"; @@ -179,9 +184,10 @@ ZoteroAutoComplete.prototype.startSearch = function(searchString, searchParam, + "ELSE 2 END AS creatorID"; } - var fromSQL = " FROM creators WHERE " + searchParts[2] - + " LIKE ?1 " + "AND fieldMode=?2"; - var sqlParams = [searchString + '%', parseInt(fieldMode)]; + var fromSQL = " FROM creators NATURAL JOIN creatorData " + + "WHERE " + searchParts[2] + " LIKE ?1 " + "AND fieldMode=?2"; + var sqlParams = [searchString + '%', + fieldMode ? parseInt(fieldMode) : 0]; if (itemID){ fromSQL += " AND creatorID NOT IN (SELECT creatorID FROM " + "itemCreators WHERE itemID=?3)"; diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -14,101 +14,19 @@ var ZoteroWrapped = this; * Include the core objects to be stored within XPCOM *********************************************************************/ -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/zotero.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/db.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/schema.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/data_access.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/attachments.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/notifier.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/history.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/search.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/ingester.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/translate.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/cite.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/cite_compat.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/quickCopy.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/report.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/timeline.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/utilities.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/integration.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/file.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/fulltext.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/mime.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/itemTreeView.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/collectionTreeView.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/progressWindow.js"); - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/annotate.js"); +var xpcomFiles = [ 'zotero', + 'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView', + 'data_access', 'data/item', 'data/items', 'data/collection', 'data/collections', + 'data/cachedTypes', 'data/creator', 'data/creators', 'data/itemFields', + 'data/notes', 'data/tags', 'db', 'file', 'fulltext', 'id', 'ingester', 'integration', + 'itemTreeView', 'mime', 'notifier', 'progressWindow', 'quickCopy', 'report', + 'schema', 'search', 'sync', 'timeline', 'translate', 'utilities']; + +for (var i=0; i<xpcomFiles.length; i++) { + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://zotero/content/xpcom/" + xpcomFiles[i] + ".js"); +} Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader) diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js @@ -69,4 +69,8 @@ pref("extensions.zotero.export.quickCopy.setting", 'bibliography=http://www.zote pref("extensions.zotero.integration.autoRegenerate", -1); // -1 = ask; 0 = no; 1 = yes // Annotation settings -pref("extensions.zotero.annotations.warnOnClose", true); -\ No newline at end of file +pref("extensions.zotero.annotations.warnOnClose", true); + +// Server +pref("extensions.zotero.sync.server.username", ''); +pref("extensions.zotero.sync.server.compressData", true); +\ No newline at end of file diff --git a/system.sql b/system.sql @@ -1,4 +1,4 @@ --- 20 +-- 21 -- This file creates system tables that can be safely wiped and reinitialized -- at any time, as long as existing ids are preserved. @@ -101,6 +101,13 @@ CREATE TABLE itemTypeCreatorTypes ( FOREIGN KEY (creatorTypeID) REFERENCES creatorTypes(creatorTypeID) ); +DROP TABLE IF EXISTS syncObjectTypes; +CREATE TABLE syncObjectTypes ( + syncObjectTypeID INTEGER PRIMARY KEY, + name TEXT +); +CREATE INDEX syncObjectTypes_name ON syncObjectTypes(name); + DROP TABLE IF EXISTS transactionSets; CREATE TABLE transactionSets ( transactionSetID INTEGER PRIMARY KEY, @@ -1237,3 +1244,8 @@ INSERT INTO "charsets" VALUES(165, 'x-unicode-2-0-utf-7'); INSERT INTO "charsets" VALUES(166, 'x-x-big5'); INSERT INTO "charsets" VALUES(167, 'x0201'); INSERT INTO "charsets" VALUES(168, 'x0212'); + +INSERT INTO "syncObjectTypes" VALUES(1, 'collection'); +INSERT INTO "syncObjectTypes" VALUES(2, 'creator'); +INSERT INTO "syncObjectTypes" VALUES(3, 'item'); +INSERT INTO "syncObjectTypes" VALUES(4, 'savedSearch'); diff --git a/triggers.sql b/triggers.sql @@ -0,0 +1,659 @@ +-- 1 + +-- Triggers to validate date field +DROP TRIGGER IF EXISTS insert_date_field; +CREATE TRIGGER insert_date_field BEFORE INSERT ON itemData + FOR EACH ROW WHEN NEW.fieldID IN (14, 27, 52, 96, 100) + BEGIN + SELECT CASE + CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 1, 4) AS INT) BETWEEN 0 AND 9999 AND + SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 5, 1) = '-' AND + CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 6, 2) AS INT) BETWEEN 0 AND 12 AND + SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 8, 1) = '-' AND + CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 9, 2) AS INT) BETWEEN 0 AND 31 + WHEN 0 THEN RAISE (ABORT, 'Date field must begin with SQL date') END; + END; + +DROP TRIGGER IF EXISTS update_date_field; +CREATE TRIGGER update_date_field BEFORE UPDATE ON itemData + FOR EACH ROW WHEN NEW.fieldID IN (14, 27, 52, 96, 100) + BEGIN + SELECT CASE + CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 1, 4) AS INT) BETWEEN 0 AND 9999 AND + SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 5, 1) = '-' AND + CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 6, 2) AS INT) BETWEEN 0 AND 12 AND + SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 8, 1) = '-' AND + CAST(SUBSTR((SELECT value FROM itemDataValues WHERE valueID=NEW.valueID), 9, 2) AS INT) BETWEEN 0 AND 31 + WHEN 0 THEN RAISE (ABORT, 'Date field must begin with SQL date') END; + END; + + +-- +-- Fake foreign key constraint checks using triggers +-- + +-- annotations/itemID +DROP TRIGGER IF EXISTS fki_annotations_itemID_itemAttachments_itemID; +CREATE TRIGGER fki_annotations_itemID_itemAttachments_itemID + BEFORE INSERT ON annotations + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "annotations" violates foreign key constraint "fki_annotations_itemID_itemAttachments_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM itemAttachments WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_annotations_itemID_itemAttachments_itemID; +CREATE TRIGGER fku_annotations_itemID_itemAttachments_itemID + BEFORE UPDATE OF itemID ON annotations + FOR EACH ROW +BEGIN + SELECT RAISE(ABORT, 'update on table "annotations" violates foreign key constraint "fku_annotations_itemID_itemAttachments_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM itemAttachments WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_annotations_itemID_itemAttachments_itemID; +CREATE TRIGGER fkd_annotations_itemID_itemAttachments_itemID + BEFORE DELETE ON itemAttachments + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "itemAttachments" violates foreign key constraint "fkd_annotations_itemID_itemAttachments_itemID"') + WHERE (SELECT COUNT(*) FROM annotations WHERE itemID = OLD.itemID) > 0; + END; + +-- collections/parentCollectionID +DROP TRIGGER IF EXISTS fki_collections_parentCollectionID_collections_collectionID; +CREATE TRIGGER fki_collections_parentCollectionID_collections_collectionID + BEFORE INSERT ON collections + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "collections" violates foreign key constraint "fki_collections_parentCollectionID_collections_collectionID"') + WHERE NEW.parentCollectionID IS NOT NULL AND (SELECT COUNT(*) FROM collections WHERE collectionID = NEW.parentCollectionID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_collections_parentCollectionID_collections_collectionID; +CREATE TRIGGER fku_collections_parentCollectionID_collections_collectionID + BEFORE UPDATE OF parentCollectionID ON collections + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "collections" violates foreign key constraint "fku_collections_parentCollectionID_collections_collectionID"') + WHERE NEW.parentCollectionID IS NOT NULL AND (SELECT COUNT(*) FROM collections WHERE collectionID = NEW.parentCollectionID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_collections_parentCollectionID_collections_collectionID; +CREATE TRIGGER fkd_collections_parentCollectionID_collections_collectionID + BEFORE DELETE ON collections + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "collections" violates foreign key constraint "fkd_collections_parentCollectionID_collections_collectionID"') + WHERE (SELECT COUNT(*) FROM collections WHERE parentCollectionID = OLD.collectionID) > 0; + END; + +-- collectionItems/collectionID +DROP TRIGGER IF EXISTS fki_collectionItems_collectionID_collections_collectionID; +CREATE TRIGGER fki_collectionItems_collectionID_collections_collectionID + BEFORE INSERT ON collectionItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "collectionItems" violates foreign key constraint "fki_collectionItems_collectionID_collections_collectionID"') + WHERE NEW.collectionID IS NOT NULL AND (SELECT COUNT(*) FROM collections WHERE collectionID = NEW.collectionID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_collectionItems_collectionID_collections_collectionID; +CREATE TRIGGER fku_collectionItems_collectionID_collections_collectionID + BEFORE UPDATE OF collectionID ON collectionItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "collectionItems" violates foreign key constraint "fku_collectionItems_collectionID_collections_collectionID"') + WHERE NEW.collectionID IS NOT NULL AND (SELECT COUNT(*) FROM collections WHERE collectionID = NEW.collectionID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_collectionItems_collectionID_collections_collectionID; +CREATE TRIGGER fkd_collectionItems_collectionID_collections_collectionID + BEFORE DELETE ON collections + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "collections" violates foreign key constraint "fkd_collectionItems_collectionID_collections_collectionID"') + WHERE (SELECT COUNT(*) FROM collectionItems WHERE collectionID = OLD.collectionID) > 0; + END; + +-- collectionItems/itemID +DROP TRIGGER IF EXISTS fki_collectionItems_itemID_items_itemID; +CREATE TRIGGER fki_collectionItems_itemID_items_itemID + BEFORE INSERT ON collectionItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "collectionItems" violates foreign key constraint "fki_collectionItems_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_collectionItems_itemID_items_itemID; +CREATE TRIGGER fku_collectionItems_itemID_items_itemID + BEFORE UPDATE OF itemID ON collectionItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "collectionItems" violates foreign key constraint "fku_collectionItems_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_collectionItems_itemID_items_itemID; +CREATE TRIGGER fkd_collectionItems_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_collectionItems_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM collectionItems WHERE itemID = OLD.itemID) > 0; + END; + +-- creators/creatorDataID +DROP TRIGGER IF EXISTS fki_creators_creatorDataID_creatorData_creatorDataID; +CREATE TRIGGER fki_creators_creatorDataID_creatorData_creatorDataID + BEFORE INSERT ON creators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "creators" violates foreign key constraint "fki_creators_creatorDataID_creatorData_creatorDataID"') + WHERE NEW.creatorDataID IS NOT NULL AND (SELECT COUNT(*) FROM creatorData WHERE creatorDataID = NEW.creatorDataID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_creators_creatorDataID_creatorData_creatorDataID; +CREATE TRIGGER fku_creators_creatorDataID_creatorData_creatorDataID + BEFORE UPDATE OF creatorDataID ON creators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "creators" violates foreign key constraint "fku_creators_creatorDataID_creatorData_creatorDataID"') + WHERE NEW.creatorDataID IS NOT NULL AND (SELECT COUNT(*) FROM creatorData WHERE creatorDataID = NEW.creatorDataID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_creators_creatorDataID_creatorData_creatorDataID; +CREATE TRIGGER fkd_creators_creatorDataID_creatorData_creatorDataID + BEFORE DELETE ON creatorData + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "creatorData" violates foreign key constraint "fkd_creators_creatorDataID_creatorData_creatorDataID"') + WHERE (SELECT COUNT(*) FROM creators WHERE creatorDataID = OLD.creatorDataID) > 0; + END; + +-- fulltextItems/itemID +DROP TRIGGER IF EXISTS fki_fulltextItems_itemID_items_itemID; +CREATE TRIGGER fki_fulltextItems_itemID_items_itemID + BEFORE INSERT ON fulltextItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "fulltextItems" violates foreign key constraint "fki_fulltextItems_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_fulltextItems_itemID_items_itemID; +CREATE TRIGGER fku_fulltextItems_itemID_items_itemID + BEFORE UPDATE OF itemID ON fulltextItems + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "fulltextItems" violates foreign key constraint "fku_fulltextItems_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_fulltextItems_itemID_items_itemID; +CREATE TRIGGER fkd_fulltextItems_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_fulltextItems_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM fulltextItems WHERE itemID = OLD.itemID) > 0; + END; + +-- fulltextItemWords/wordID +DROP TRIGGER IF EXISTS fki_fulltextItemWords_wordID_fulltextWords_wordID; +CREATE TRIGGER fki_fulltextItemWords_wordID_fulltextWords_wordID + BEFORE INSERT ON fulltextItemWords + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "fulltextItemWords" violates foreign key constraint "fki_fulltextItemWords_wordID_fulltextWords_wordID"') + WHERE NEW.wordID IS NOT NULL AND (SELECT COUNT(*) FROM fulltextWords WHERE wordID = NEW.wordID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_fulltextItemWords_wordID_fulltextWords_wordID; +CREATE TRIGGER fku_fulltextItemWords_wordID_fulltextWords_wordID + BEFORE UPDATE OF wordID ON fulltextItemWords + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "fulltextItemWords" violates foreign key constraint "fku_fulltextItemWords_wordID_fulltextWords_wordID"') + WHERE NEW.wordID IS NOT NULL AND (SELECT COUNT(*) FROM fulltextWords WHERE wordID = NEW.wordID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_fulltextItemWords_wordID_fulltextWords_wordID; +CREATE TRIGGER fkd_fulltextItemWords_wordID_fulltextWords_wordID + BEFORE DELETE ON fulltextWords + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "fulltextWords" violates foreign key constraint "fkd_fulltextItemWords_wordID_fulltextWords_wordID"') + WHERE (SELECT COUNT(*) FROM fulltextItemWords WHERE wordID = OLD.wordID) > 0; + END; + +-- fulltextItemWords/itemID +DROP TRIGGER IF EXISTS fki_fulltextItemWords_itemID_items_itemID; +CREATE TRIGGER fki_fulltextItemWords_itemID_items_itemID + BEFORE INSERT ON fulltextItemWords + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "fulltextItemWords" violates foreign key constraint "fki_fulltextItemWords_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_fulltextItemWords_itemID_items_itemID; +CREATE TRIGGER fku_fulltextItemWords_itemID_items_itemID + BEFORE UPDATE OF itemID ON fulltextItemWords + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "fulltextItemWords" violates foreign key constraint "fku_fulltextItemWords_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_fulltextItemWords_itemID_items_itemID; +CREATE TRIGGER fkd_fulltextItemWords_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_fulltextItemWords_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM fulltextItemWords WHERE itemID = OLD.itemID) > 0; + END; + +-- highlights/itemID +DROP TRIGGER IF EXISTS fki_highlights_itemID_itemAttachments_itemID; +CREATE TRIGGER fki_highlights_itemID_itemAttachments_itemID + BEFORE INSERT ON highlights + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "highlights" violates foreign key constraint "fki_highlights_itemID_itemAttachments_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM itemAttachments WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_highlights_itemID_itemAttachments_itemID; +CREATE TRIGGER fku_highlights_itemID_itemAttachments_itemID + BEFORE UPDATE OF itemID ON highlights + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "highlights" violates foreign key constraint "fku_highlights_itemID_itemAttachments_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM itemAttachments WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_highlights_itemID_itemAttachments_itemID; +CREATE TRIGGER fkd_highlights_itemID_itemAttachments_itemID + BEFORE DELETE ON itemAttachments + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "itemAttachments" violates foreign key constraint "fkd_highlights_itemID_itemAttachments_itemID"') + WHERE (SELECT COUNT(*) FROM highlights WHERE itemID = OLD.itemID) > 0; + END; + +-- itemAttachments/itemID +DROP TRIGGER IF EXISTS fki_itemAttachments_itemID_items_itemID; +CREATE TRIGGER fki_itemAttachments_itemID_items_itemID + BEFORE INSERT ON itemAttachments + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemAttachments" violates foreign key constraint "fki_itemAttachments_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemAttachments_itemID_items_itemID; +CREATE TRIGGER fku_itemAttachments_itemID_items_itemID + BEFORE UPDATE OF itemID ON itemAttachments + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemAttachments" violates foreign key constraint "fku_itemAttachments_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemAttachments_itemID_items_itemID; +CREATE TRIGGER fkd_itemAttachments_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemAttachments_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemAttachments WHERE itemID = OLD.itemID) > 0; + END; + +-- itemAttachments/sourceItemID +DROP TRIGGER IF EXISTS fki_itemAttachments_sourceItemID_items_itemID; +CREATE TRIGGER fki_itemAttachments_sourceItemID_items_itemID + BEFORE INSERT ON itemAttachments + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemAttachments" violates foreign key constraint "fki_itemAttachments_sourceItemID_items_sourceItemID"') + WHERE NEW.sourceItemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.sourceItemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemAttachments_sourceItemID_items_itemID; +CREATE TRIGGER fku_itemAttachments_sourceItemID_items_itemID + BEFORE UPDATE OF sourceItemID ON itemAttachments + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemAttachments" violates foreign key constraint "fku_itemAttachments_sourceItemID_items_sourceItemID"') + WHERE NEW.sourceItemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.sourceItemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemAttachments_sourceItemID_items_itemID; +CREATE TRIGGER fkd_itemAttachments_sourceItemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemAttachments_sourceItemID_items_sourceItemID"') + WHERE (SELECT COUNT(*) FROM itemAttachments WHERE sourceItemID = OLD.itemID) > 0; + END; + +-- itemCreators/itemID +DROP TRIGGER IF EXISTS fki_itemCreators_itemID_items_itemID; +CREATE TRIGGER fki_itemCreators_itemID_items_itemID + BEFORE INSERT ON itemCreators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemCreators" violates foreign key constraint "fki_itemCreators_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemCreators_itemID_items_itemID; +CREATE TRIGGER fku_itemCreators_itemID_items_itemID + BEFORE UPDATE OF itemID ON itemCreators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemCreators" violates foreign key constraint "fku_itemCreators_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemCreators_itemID_items_itemID; +CREATE TRIGGER fkd_itemCreators_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemCreators_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemCreators WHERE itemID = OLD.itemID) > 0; + END; + +-- itemCreators/creatorID +DROP TRIGGER IF EXISTS fki_itemCreators_creatorID_creators_creatorID; +CREATE TRIGGER fki_itemCreators_creatorID_creators_creatorID + BEFORE INSERT ON itemCreators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemCreators" violates foreign key constraint "fki_itemCreators_creatorID_creators_creatorID"') + WHERE NEW.creatorID IS NOT NULL AND (SELECT COUNT(*) FROM creators WHERE creatorID = NEW.creatorID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemCreators_creatorID_creators_creatorID; +CREATE TRIGGER fku_itemCreators_creatorID_creators_creatorID + BEFORE UPDATE OF creatorID ON itemCreators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemCreators" violates foreign key constraint "fku_itemCreators_creatorID_creators_creatorID"') + WHERE NEW.creatorID IS NOT NULL AND (SELECT COUNT(*) FROM creators WHERE creatorID = NEW.creatorID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemCreators_creatorID_creators_creatorID; +CREATE TRIGGER fkd_itemCreators_creatorID_creators_creatorID + BEFORE DELETE ON creators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "creators" violates foreign key constraint "fkd_itemCreators_creatorID_creators_creatorID"') + WHERE (SELECT COUNT(*) FROM itemCreators WHERE creatorID = OLD.creatorID) > 0; + END; + +-- itemCreators/creatorTypeID +DROP TRIGGER IF EXISTS fki_itemCreators_creatorTypeID_creatorTypes_creatorTypeID; +CREATE TRIGGER fki_itemCreators_creatorTypeID_creatorTypes_creatorTypeID + BEFORE INSERT ON itemCreators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemCreators" violates foreign key constraint "fki_itemCreators_creatorTypeID_creatorTypes_creatorTypeID"') + WHERE NEW.creatorTypeID IS NOT NULL AND (SELECT COUNT(*) FROM creatorTypes WHERE creatorTypeID = NEW.creatorTypeID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemCreators_creatorTypeID_creatorTypes_creatorTypeID; +CREATE TRIGGER fku_itemCreators_creatorTypeID_creatorTypes_creatorTypeID + BEFORE UPDATE OF creatorTypeID ON itemCreators + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemCreators" violates foreign key constraint "fku_itemCreators_creatorTypeID_creatorTypes_creatorTypeID"') + WHERE NEW.creatorTypeID IS NOT NULL AND (SELECT COUNT(*) FROM creatorTypes WHERE creatorTypeID = NEW.creatorTypeID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemCreators_creatorTypeID_creatorTypes_creatorTypeID; +CREATE TRIGGER fkd_itemCreators_creatorTypeID_creatorTypes_creatorTypeID + BEFORE DELETE ON creatorTypes + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "creatorTypes" violates foreign key constraint "fkd_itemCreators_creatorTypeID_creatorTypes_creatorTypeID"') + WHERE (SELECT COUNT(*) FROM itemCreators WHERE creatorTypeID = OLD.creatorTypeID) > 0; + END; + +-- itemData/itemID +DROP TRIGGER IF EXISTS fki_itemData_itemID_items_itemID; +CREATE TRIGGER fki_itemData_itemID_items_itemID + BEFORE INSERT ON itemData + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemData" violates foreign key constraint "fki_itemData_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemData_itemID_items_itemID; +CREATE TRIGGER fku_itemData_itemID_items_itemID + BEFORE UPDATE OF itemID ON itemData + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemData" violates foreign key constraint "fku_itemData_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemData_itemID_items_itemID; +CREATE TRIGGER fkd_itemData_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemData_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemData WHERE itemID = OLD.itemID) > 0; + END; + +-- itemData/fieldID +DROP TRIGGER IF EXISTS fki_itemData_fieldID_fields_fieldID; +CREATE TRIGGER fki_itemData_fieldID_fields_fieldID + BEFORE INSERT ON itemData + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemData" violates foreign key constraint "fki_itemData_fieldID_fields_fieldID"') + WHERE NEW.fieldID IS NOT NULL AND (SELECT COUNT(*) FROM fields WHERE fieldID = NEW.fieldID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemData_fieldID_fields_fieldID; +CREATE TRIGGER fku_itemData_fieldID_fields_fieldID + BEFORE UPDATE OF fieldID ON itemData + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemData" violates foreign key constraint "fku_itemData_fieldID_fields_fieldID"') + WHERE NEW.fieldID IS NOT NULL AND (SELECT COUNT(*) FROM fields WHERE fieldID = NEW.fieldID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemData_fieldID_fields_fieldID; +CREATE TRIGGER fkd_itemData_fieldID_fields_fieldID + BEFORE DELETE ON FIELDS + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "fields" violates foreign key constraint "fkd_itemData_fieldID_fields_fieldID"') + WHERE (SELECT COUNT(*) FROM itemData WHERE fieldID = OLD.fieldID) > 0; + END; + +-- itemData/valueID +DROP TRIGGER IF EXISTS fki_itemData_valueID_itemDataValues_valueID; +CREATE TRIGGER fki_itemData_valueID_itemDataValues_valueID + BEFORE INSERT ON itemData + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemData" violates foreign key constraint "fki_itemData_valueID_itemDataValues_valueID"') + WHERE NEW.valueID IS NOT NULL AND (SELECT COUNT(*) FROM itemDataValues WHERE valueID = NEW.valueID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemData_valueID_itemDataValues_valueID; +CREATE TRIGGER fku_itemData_valueID_itemDataValues_valueID + BEFORE UPDATE OF valueID ON itemData + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemData" violates foreign key constraint "fku_itemData_valueID_itemDataValues_valueID"') + WHERE NEW.valueID IS NOT NULL AND (SELECT COUNT(*) FROM itemDataValues WHERE valueID = NEW.valueID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemData_valueID_itemDataValues_valueID; +CREATE TRIGGER fkd_itemData_valueID_itemDataValues_valueID + BEFORE DELETE ON itemDataValues + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "itemDataValues" violates foreign key constraint "fkd_itemData_valueID_itemDataValues_valueID"') + WHERE (SELECT COUNT(*) FROM itemData WHERE valueID = OLD.valueID) > 0; + END; + +-- itemNotes/itemID +DROP TRIGGER IF EXISTS fki_itemNotes_itemID_items_itemID; +CREATE TRIGGER fki_itemNotes_itemID_items_itemID + BEFORE INSERT ON itemNotes + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemNotes" violates foreign key constraint "fki_itemNotes_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemNotes_itemID_items_itemID; +CREATE TRIGGER fku_itemNotes_itemID_items_itemID + BEFORE UPDATE OF itemID ON itemNotes + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemNotes" violates foreign key constraint "fku_itemNotes_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemNotes_itemID_items_itemID; +CREATE TRIGGER fkd_itemNotes_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemNotes_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemNotes WHERE itemID = OLD.itemID) > 0; + END; + +-- itemNotes/sourceItemID +DROP TRIGGER IF EXISTS fki_itemNotes_sourceItemID_items_itemID; +CREATE TRIGGER fki_itemNotes_sourceItemID_items_itemID + BEFORE INSERT ON itemNotes + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemNotes" violates foreign key constraint "fki_itemNotes_sourceItemID_items_itemID"') + WHERE NEW.sourceItemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.sourceItemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemNotes_sourceItemID_items_itemID; +CREATE TRIGGER fku_itemNotes_sourceItemID_items_itemID + BEFORE UPDATE OF sourceItemID ON itemNotes + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemNotes" violates foreign key constraint "fku_itemNotes_sourceItemID_items_itemID"') + WHERE NEW.sourceItemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.sourceItemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemNotes_sourceItemID_items_itemID; +CREATE TRIGGER fkd_itemNotes_sourceItemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemNotes_sourceItemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemNotes WHERE sourceItemID = OLD.itemID) > 0; + END; + +-- itemSeeAlso/itemID +DROP TRIGGER IF EXISTS fki_itemSeeAlso_itemID_items_itemID; +CREATE TRIGGER fki_itemSeeAlso_itemID_items_itemID + BEFORE INSERT ON itemSeeAlso + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemSeeAlso" violates foreign key constraint "fki_itemSeeAlso_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemSeeAlso_itemID_items_itemID; +CREATE TRIGGER fku_itemSeeAlso_itemID_items_itemID + BEFORE UPDATE OF itemID ON itemSeeAlso + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemSeeAlso" violates foreign key constraint "fku_itemSeeAlso_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemSeeAlso_itemID_items_itemID; +CREATE TRIGGER fkd_itemSeeAlso_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemSeeAlso_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemSeeAlso WHERE itemID = OLD.itemID) > 0; + END; + +-- itemSeeAlso/linkedItemID +DROP TRIGGER IF EXISTS fki_itemSeeAlso_linkedItemID_items_itemID; +CREATE TRIGGER fki_itemSeeAlso_linkedItemID_items_itemID + BEFORE INSERT ON itemSeeAlso + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemSeeAlso" violates foreign key constraint "fki_itemSeeAlso_linkedItemID_items_itemID"') + WHERE NEW.linkedItemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.linkedItemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemSeeAlso_linkedItemID_items_itemID; +CREATE TRIGGER fku_itemSeeAlso_linkedItemID_items_itemID + BEFORE UPDATE OF linkedItemID ON itemSeeAlso + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemSeeAlso" violates foreign key constraint "fku_itemSeeAlso_linkedItemID_items_itemID"') + WHERE NEW.linkedItemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.linkedItemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemSeeAlso_linkedItemID_items_itemID; +CREATE TRIGGER fkd_itemSeeAlso_linkedItemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemSeeAlso_linkedItemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemSeeAlso WHERE linkedItemID = OLD.itemID) > 0; + END; + +-- itemTags/itemID +DROP TRIGGER IF EXISTS fki_itemTags_itemID_items_itemID; +CREATE TRIGGER fki_itemTags_itemID_items_itemID + BEFORE INSERT ON itemTags + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemTags" violates foreign key constraint "fki_itemTags_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemTags_itemID_items_itemID; +CREATE TRIGGER fku_itemTags_itemID_items_itemID + BEFORE UPDATE OF itemID ON itemTags + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemTags" violates foreign key constraint "fku_itemTags_itemID_items_itemID"') + WHERE NEW.itemID IS NOT NULL AND (SELECT COUNT(*) FROM items WHERE itemID = NEW.itemID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemTags_itemID_items_itemID; +CREATE TRIGGER fkd_itemTags_itemID_items_itemID + BEFORE DELETE ON items + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "items" violates foreign key constraint "fkd_itemTags_itemID_items_itemID"') + WHERE (SELECT COUNT(*) FROM itemTags WHERE itemID = OLD.itemID) > 0; + END; + +-- itemTags/tagID +DROP TRIGGER IF EXISTS fki_itemTags_tagID_tags_tagID; +CREATE TRIGGER fki_itemTags_tagID_tags_tagID + BEFORE INSERT ON itemTags + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "itemTags" violates foreign key constraint "fki_itemTags_tagID_tags_tagID"') + WHERE NEW.tagID IS NOT NULL AND (SELECT COUNT(*) FROM tags WHERE tagID = NEW.tagID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_itemTags_tagID_tags_tagID; +CREATE TRIGGER fku_itemTags_tagID_tags_tagID + BEFORE UPDATE OF tagID ON itemTags + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "itemTags" violates foreign key constraint "fku_itemTags_tagID_tags_tagID"') + WHERE NEW.tagID IS NOT NULL AND (SELECT COUNT(*) FROM tags WHERE tagID = NEW.tagID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_itemTags_tagID_tags_tagID; +CREATE TRIGGER fkd_itemTags_tagID_tags_tagID + BEFORE DELETE ON tags + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "tags" violates foreign key constraint "fkd_itemTags_tagID_tags_tagID"') + WHERE (SELECT COUNT(*) FROM itemTags WHERE tagID = OLD.tagID) > 0; + END; + +-- savedSearchConditions/searchConditionID +DROP TRIGGER IF EXISTS fki_savedSearchConditions_searchConditionID_savedSearches_savedSearchID; +CREATE TRIGGER fki_savedSearchConditions_searchConditionID_savedSearches_savedSearchID + BEFORE INSERT ON savedSearchConditions + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "savedSearchConditions" violates foreign key constraint "fki_savedSearchConditions_searchConditionID_savedSearches_savedSearchID"') + WHERE NEW.searchConditionID IS NOT NULL AND (SELECT COUNT(*) FROM savedSearches WHERE savedSearchID = NEW.searchConditionID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_savedSearchConditions_searchConditionID_savedSearches_savedSearchID; +CREATE TRIGGER fku_savedSearchConditions_searchConditionID_savedSearches_savedSearchID + BEFORE UPDATE OF searchConditionID ON savedSearchConditions + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "savedSearchConditions" violates foreign key constraint "fku_savedSearchConditions_searchConditionID_savedSearches_savedSearchID"') + WHERE NEW.searchConditionID IS NOT NULL AND (SELECT COUNT(*) FROM savedSearches WHERE savedSearchID = NEW.searchConditionID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_savedSearchConditions_searchConditionID_savedSearches_savedSearchID; +CREATE TRIGGER fkd_savedSearchConditions_searchConditionID_savedSearches_savedSearchID + BEFORE DELETE ON savedSearches + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "savedSearches" violates foreign key constraint "fkd_savedSearchConditions_searchConditionID_savedSearches_savedSearchID"') + WHERE (SELECT COUNT(*) FROM savedSearchConditions WHERE searchConditionID = OLD.savedSearchID) > 0; + END; + +-- syncDeleteLog/syncObjectTypeID +DROP TRIGGER IF EXISTS fki_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID; +CREATE TRIGGER fki_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID + BEFORE INSERT ON syncDeleteLog + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'insert on table "syncDeleteLog" violates foreign key constraint "fki_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID"') + WHERE (SELECT COUNT(*) FROM syncObjectTypes WHERE syncObjectTypeID = NEW.syncObjectTypeID) = 0; + END; + +DROP TRIGGER IF EXISTS fku_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID; +CREATE TRIGGER fku_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID + BEFORE UPDATE OF syncObjectTypeID ON syncDeleteLog + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'update on table "syncDeleteLog" violates foreign key constraint "fku_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID"') + WHERE (SELECT COUNT(*) FROM syncObjectTypes WHERE syncObjectTypeID = NEW.syncObjectTypeID) = 0; + END; + +DROP TRIGGER IF EXISTS fkd_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID; +CREATE TRIGGER fkd_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID + BEFORE DELETE ON syncObjectTypes + FOR EACH ROW BEGIN + SELECT RAISE(ABORT, 'delete on table "syncObjectTypes" violates foreign key constraint "fkd_syncDeleteLog_syncObjectTypeID_syncObjectTypes_syncObjectTypeID"') + WHERE (SELECT COUNT(*) FROM syncDeleteLog WHERE syncObjectTypeID = OLD.syncObjectTypeID) > 0; + END; diff --git a/userdata.sql b/userdata.sql @@ -1,78 +1,38 @@ --- 36 +-- 37 --- This file creates tables containing user-specific data -- any changes --- to existing tables made here must be mirrored in transition steps in --- schema.js::_migrateSchema() +-- This file creates tables containing user-specific data -- any changes made +-- here must be mirrored in transition steps in schema.js::_migrateSchema() -CREATE TABLE IF NOT EXISTS version ( +CREATE TABLE version ( schema TEXT PRIMARY KEY, version INT NOT NULL ); -CREATE INDEX IF NOT EXISTS schema ON version(schema); +CREATE INDEX schema ON version(schema); -CREATE TABLE IF NOT EXISTS settings ( +CREATE TABLE settings ( setting TEXT, key TEXT, value, PRIMARY KEY (setting, key) ); --- Show or hide pre-mapped fields for system item types -CREATE TABLE IF NOT EXISTS userFieldMask ( - itemTypeID INT, - fieldID INT, - hide INT, - PRIMARY KEY (itemTypeID, fieldID), - FOREIGN KEY (itemTypeID, fieldID) REFERENCES itemTypeFields(itemTypeID, fieldID) -); - --- User-defined item types -- itemTypeIDs must be >= 1000 -CREATE TABLE IF NOT EXISTS userItemTypes ( - itemTypeID INTEGER PRIMARY KEY, - typeName TEXT, - templateItemTypeID INT -); - --- Control visibility and placement of system and user item types -CREATE TABLE IF NOT EXISTS userItemTypeMask ( - itemTypeID INTEGER PRIMARY KEY, - display INT, -- 0 == hide, 1 == show, 2 == primary - FOREIGN KEY (itemTypeID) REFERENCES userItemTypes(itemTypeID) -); - --- User-defined fields -CREATE TABLE IF NOT EXISTS userFields ( - userFieldID INTEGER PRIMARY KEY, - fieldName TEXT -); - --- Map custom fields to system and custom item types -CREATE TABLE IF NOT EXISTS userItemTypeFields ( - itemTypeID INT, - userFieldID INT, - orderIndex INT, - PRIMARY KEY (itemTypeID, userFieldID), - FOREIGN KEY (userFieldID) REFERENCES userFields(userFieldID) -); - -- The foundational table; every item collected has a unique record here -CREATE TABLE IF NOT EXISTS items ( +CREATE TABLE items ( itemID INTEGER PRIMARY KEY, itemTypeID INT, dateAdded DATETIME DEFAULT CURRENT_TIMESTAMP, - dateModified DATETIME DEFAULT CURRENT_TIMESTAMP + dateModified DATETIME DEFAULT CURRENT_TIMESTAMP, + key TEXT NOT NULL UNIQUE ); -CREATE TABLE IF NOT EXISTS itemDataValues ( +CREATE TABLE itemDataValues ( valueID INTEGER PRIMARY KEY, value ); -- Type-specific data for individual items --- --- Triggers specified in schema.js due to lack of trigger IF [NOT] EXISTS in Firefox 2.0 -CREATE TABLE IF NOT EXISTS itemData ( +CREATE TABLE itemData ( itemID INT, fieldID INT, valueID, @@ -83,23 +43,18 @@ CREATE TABLE IF NOT EXISTS itemData ( ); -- Note data for note items -CREATE TABLE IF NOT EXISTS itemNotes ( +CREATE TABLE itemNotes ( itemID INTEGER PRIMARY KEY, sourceItemID INT, note TEXT, + title TEXT, FOREIGN KEY (itemID) REFERENCES items(itemID), FOREIGN KEY (sourceItemID) REFERENCES items(itemID) ); -CREATE INDEX IF NOT EXISTS itemNotes_sourceItemID ON itemNotes(sourceItemID); - -CREATE TABLE IF NOT EXISTS itemNoteTitles ( - itemID INTEGER PRIMARY KEY, - title TEXT, - FOREIGN KEY (itemID) REFERENCES itemNotes(itemID) -); +CREATE INDEX itemNotes_sourceItemID ON itemNotes(sourceItemID); -- Metadata for attachment items -CREATE TABLE IF NOT EXISTS itemAttachments ( +CREATE TABLE itemAttachments ( itemID INTEGER PRIMARY KEY, sourceItemID INT, linkMode INT, @@ -110,11 +65,11 @@ CREATE TABLE IF NOT EXISTS itemAttachments ( FOREIGN KEY (itemID) REFERENCES items(itemID), FOREIGN KEY (sourceItemID) REFERENCES items(sourceItemID) ); -CREATE INDEX IF NOT EXISTS itemAttachments_sourceItemID ON itemAttachments(sourceItemID); -CREATE INDEX IF NOT EXISTS itemAttachments_mimeType ON itemAttachments(mimeType); +CREATE INDEX itemAttachments_sourceItemID ON itemAttachments(sourceItemID); +CREATE INDEX itemAttachments_mimeType ON itemAttachments(mimeType); -- Individual entries for each tag -CREATE TABLE IF NOT EXISTS tags ( +CREATE TABLE tags ( tagID INTEGER PRIMARY KEY, tag TEXT, tagType INT, @@ -122,34 +77,46 @@ CREATE TABLE IF NOT EXISTS tags ( ); -- Associates items with keywords -CREATE TABLE IF NOT EXISTS itemTags ( +CREATE TABLE itemTags ( itemID INT, tagID INT, PRIMARY KEY (itemID, tagID), FOREIGN KEY (itemID) REFERENCES items(itemID), FOREIGN KEY (tagID) REFERENCES tags(tagID) ); -CREATE INDEX IF NOT EXISTS itemTags_tagID ON itemTags(tagID); +CREATE INDEX itemTags_tagID ON itemTags(tagID); -CREATE TABLE IF NOT EXISTS itemSeeAlso ( +CREATE TABLE itemSeeAlso ( itemID INT, linkedItemID INT, PRIMARY KEY (itemID, linkedItemID), FOREIGN KEY (itemID) REFERENCES items(itemID), FOREIGN KEY (linkedItemID) REFERENCES items(itemID) ); -CREATE INDEX IF NOT EXISTS itemSeeAlso_linkedItemID ON itemSeeAlso(linkedItemID); +CREATE INDEX itemSeeAlso_linkedItemID ON itemSeeAlso(linkedItemID); --- Names of each individual "creator" (inc. authors, editors, etc.) -CREATE TABLE IF NOT EXISTS creators ( + +CREATE TABLE creators ( creatorID INTEGER PRIMARY KEY, + creatorDataID INT, + dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL, + key TEXT NOT NULL UNIQUE, + FOREIGN KEY (creatorDataID) REFERENCES creatorData(creatorDataID) +); +CREATE INDEX creators_creatorDataID ON creators(creatorDataID); + +-- Each individual creator +CREATE TABLE creatorData ( + creatorDataID INTEGER PRIMARY KEY, firstName TEXT, lastName TEXT, - fieldMode INT + shortName TEXT, + fieldMode INT, + birthYear INT ); -- Associates single or multiple creators to items -CREATE TABLE IF NOT EXISTS itemCreators ( +CREATE TABLE itemCreators ( itemID INT, creatorID INT, creatorTypeID INT DEFAULT 1, @@ -161,15 +128,17 @@ CREATE TABLE IF NOT EXISTS itemCreators ( ); -- Collections for holding items -CREATE TABLE IF NOT EXISTS collections ( +CREATE TABLE collections ( collectionID INTEGER PRIMARY KEY, collectionName TEXT, parentCollectionID INT, + dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL, + key TEXT NOT NULL UNIQUE, FOREIGN KEY (parentCollectionID) REFERENCES collections(collectionID) ); -- Associates items with the various collections they belong to -CREATE TABLE IF NOT EXISTS collectionItems ( +CREATE TABLE collectionItems ( collectionID INT, itemID INT, orderIndex INT DEFAULT 0, @@ -177,14 +146,16 @@ CREATE TABLE IF NOT EXISTS collectionItems ( FOREIGN KEY (collectionID) REFERENCES collections(collectionID), FOREIGN KEY (itemID) REFERENCES items(itemID) ); -CREATE INDEX IF NOT EXISTS itemID ON collectionItems(itemID); +CREATE INDEX itemID ON collectionItems(itemID); -CREATE TABLE IF NOT EXISTS savedSearches ( +CREATE TABLE savedSearches ( savedSearchID INTEGER PRIMARY KEY, - savedSearchName TEXT + savedSearchName TEXT, + dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL, + key TEXT NOT NULL UNIQUE ); -CREATE TABLE IF NOT EXISTS savedSearchConditions ( +CREATE TABLE savedSearchConditions ( savedSearchID INT, searchConditionID INT, condition TEXT, @@ -195,7 +166,7 @@ CREATE TABLE IF NOT EXISTS savedSearchConditions ( FOREIGN KEY (savedSearchID) REFERENCES savedSearches(savedSearchID) ); -CREATE TABLE IF NOT EXISTS fulltextItems ( +CREATE TABLE fulltextItems ( itemID INTEGER PRIMARY KEY, version INT, indexedPages INT, @@ -204,24 +175,33 @@ CREATE TABLE IF NOT EXISTS fulltextItems ( totalChars INT, FOREIGN KEY (itemID) REFERENCES items(itemID) ); -CREATE INDEX IF NOT EXISTS fulltextItems_version ON fulltextItems(version); +CREATE INDEX fulltextItems_version ON fulltextItems(version); -CREATE TABLE IF NOT EXISTS fulltextWords ( +CREATE TABLE fulltextWords ( wordID INTEGER PRIMARY KEY, word TEXT UNIQUE ); -CREATE INDEX IF NOT EXISTS fulltextWords_word ON fulltextWords(word); +CREATE INDEX fulltextWords_word ON fulltextWords(word); -CREATE TABLE IF NOT EXISTS fulltextItemWords ( +CREATE TABLE fulltextItemWords ( wordID INT, itemID INT, PRIMARY KEY (wordID, itemID), FOREIGN KEY (wordID) REFERENCES fulltextWords(wordID), FOREIGN KEY (itemID) REFERENCES items(itemID) ); -CREATE INDEX IF NOT EXISTS fulltextItemWords_itemID ON fulltextItemWords(itemID); +CREATE INDEX fulltextItemWords_itemID ON fulltextItemWords(itemID); + +CREATE TABLE syncDeleteLog ( + syncObjectTypeID INT NOT NULL, + objectID INT NOT NULL, + key TEXT NOT NULL UNIQUE, + timestamp INT NOT NULL, + FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID) +); +CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp); -CREATE TABLE IF NOT EXISTS translators ( +CREATE TABLE translators ( translatorID TEXT PRIMARY KEY, minVersion TEXT, maxVersion TEXT, @@ -235,16 +215,16 @@ CREATE TABLE IF NOT EXISTS translators ( detectCode TEXT, code TEXT ); -CREATE INDEX IF NOT EXISTS translators_type ON translators(translatorType); +CREATE INDEX translators_type ON translators(translatorType); -CREATE TABLE IF NOT EXISTS csl ( +CREATE TABLE csl ( cslID TEXT PRIMARY KEY, updated DATETIME, title TEXT, csl TEXT ); -CREATE TABLE IF NOT EXISTS annotations ( +CREATE TABLE annotations ( annotationID INTEGER PRIMARY KEY, itemID INT, parent TEXT, @@ -259,9 +239,9 @@ CREATE TABLE IF NOT EXISTS annotations ( dateModified DATE, FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ); -CREATE INDEX IF NOT EXISTS annotations_itemID ON annotations(itemID); +CREATE INDEX annotations_itemID ON annotations(itemID); -CREATE TABLE IF NOT EXISTS highlights ( +CREATE TABLE highlights ( highlightID INTEGER PRIMARY KEY, itemID INTEGER, startParent TEXT, @@ -273,4 +253,4 @@ CREATE TABLE IF NOT EXISTS highlights ( dateModified DATE, FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ); -CREATE INDEX IF NOT EXISTS highlights_itemID ON highlights(itemID); -\ No newline at end of file +CREATE INDEX highlights_itemID ON highlights(itemID); +\ No newline at end of file