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