- Fixes tag editing

- Adds tag syncing
- Fixes a few other things

No tag CR yet
Requires new 1.0 DB upgrade
This commit is contained in:
Dan Stillman 2008-06-16 05:46:10 +00:00
parent eb134e6fe4
commit c52604883f
19 changed files with 1355 additions and 334 deletions

View File

@ -28,16 +28,49 @@
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="tags-box"> <binding id="tags-box">
<implementation> <implementation>
<field name="itemRef"/> <field name="clickHandler"/>
<property name="item" onget="return this.itemRef;">
<!-- Modes are predefined settings groups for particular tasks -->
<field name="_mode">"view"</field>
<property name="mode" onget="return this._mode;">
<setter> <setter>
<![CDATA[ <![CDATA[
this.itemRef = val; this.clickable = false;
this.editable = false;
switch (val) {
case 'view':
break;
case 'edit':
this.clickable = true;
this.editable = true;
this.clickHandler = this.showEditor;
this.blurHandler = this.hideEditor;
break;
default:
throw ("Invalid mode '" + val + "' in tagsbox.xml");
}
this._mode = val;
document.getAnonymousNodes(this)[0].setAttribute('mode', val);
]]>
</setter>
</property>
<field name="_item"/>
<property name="item" onget="return this._item;">
<setter>
<![CDATA[
this._item = val;
this.reload(); this.reload();
]]> ]]>
</setter> </setter>
</property> </property>
<property name="count"/> <property name="count"/>
<property name="summary"> <property name="summary">
<getter> <getter>
<![CDATA[ <![CDATA[
@ -50,7 +83,7 @@
{ {
for(var i = 0; i < tags.length; i++) for(var i = 0; i < tags.length; i++)
{ {
r = r + tags[i].tag + ", "; r = r + tags[i].name + ", ";
} }
r = r.substr(0,r.length-2); r = r.substr(0,r.length-2);
} }
@ -60,10 +93,13 @@
]]> ]]>
</getter> </getter>
</property> </property>
<method name="reload"> <method name="reload">
<body> <body>
<![CDATA[ <![CDATA[
//Zotero.debug('Reloading tags'); Zotero.debug('Reloading tags');
var rows = this.id('tagRows'); var rows = this.id('tagRows');
while(rows.hasChildNodes()) while(rows.hasChildNodes())
@ -88,6 +124,8 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="addDynamicRow"> <method name="addDynamicRow">
<parameter name="tagObj"/> <parameter name="tagObj"/>
<parameter name="tabindex"/> <parameter name="tabindex"/>
@ -95,11 +133,11 @@
<![CDATA[ <![CDATA[
if (tagObj) { if (tagObj) {
var tagID = tagObj.id; var tagID = tagObj.id;
var tag = tagObj.tag; var name = tagObj.name;
var type = tagObj.type; var type = tagObj.type;
} }
if (!tag) { if (!name) {
tag = ''; name = '';
} }
if (!tabindex) if (!tabindex)
@ -128,7 +166,7 @@
// DEBUG: Why won't just this.nextSibling.blur() work? // DEBUG: Why won't just this.nextSibling.blur() work?
icon.setAttribute('onclick','if (this.nextSibling.inputField){ this.nextSibling.inputField.blur() }'); icon.setAttribute('onclick','if (this.nextSibling.inputField){ this.nextSibling.inputField.blur() }');
var label = ZoteroItemPane.createValueElement(tag, 'tag', tabindex); var label = this.createValueElement(name, tabindex);
var remove = document.createElement("label"); var remove = document.createElement("label");
remove.setAttribute('value','-'); remove.setAttribute('value','-');
@ -159,6 +197,284 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="createValueElement">
<parameter name="valueText"/>
<parameter name="tabindex"/>
<body>
<![CDATA[
var valueElement = document.createElement("label");
valueElement.setAttribute('fieldname', 'tag');
valueElement.setAttribute('flex', 1);
if (this.clickable) {
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';
}
this._tabIndexMaxTagsFields = Math.max(this._tabIndexMaxTagsFields, tabindex);
var firstSpace;
if (typeof valueText == 'string') {
firstSpace = valueText.indexOf(" ");
}
// 29 == arbitrary length at which to chop uninterrupted text
if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29) {
valueElement.setAttribute('crop', 'end');
valueElement.setAttribute('value',valueText);
}
else {
// Wrap to multiple lines
valueElement.appendChild(document.createTextNode(valueText));
}
return valueElement;
]]>
</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 = 'tag';
var tabindex = elem.getAttribute('ztabindex');
var tagID = elem.parentNode.getAttribute('id').split('-')[1];
var value = tagID ? Zotero.Tags.getName(tagID) : '';
var itemID = Zotero.getAncestorByTagName(elem, 'tagsbox').item.id;
var t = document.createElement("textbox");
t.setAttribute('value', value);
t.setAttribute('fieldname', fieldName);
t.setAttribute('ztabindex', tabindex);
t.setAttribute('flex', '1');
// Add auto-complete
t.setAttribute('type', 'autocomplete');
t.setAttribute('autocompletesearch', 'zotero');
var suffix = itemID ? itemID : '';
t.setAttribute('autocompletesearchparam', fieldName + '/' + suffix);
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>
<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 = 'tag';
// Prevent blur on containing textbox
// DEBUG: what happens if this isn't present?
event.preventDefault();
// If last tag row, create new one
var row = target.parentNode.parentNode;
if (row == row.parentNode.lastChild) {
this._tabDirection = 1;
var lastTag = true;
}
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;
var saveChanges = true;
var fieldName = 'tag';
var tabindex = textbox.getAttribute('ztabindex');
//var value = t.value;
var value = textbox.value;
var elem;
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;
var box = textbox.parentNode;
box.replaceChild(elem,textbox);
if (this._tabDirection) {
this._focusNextField(focusBox, this._lastTabIndex, this._tabDirection == -1);
}
]]>
</body>
</method>
<method name="new"> <method name="new">
<body> <body>
<![CDATA[ <![CDATA[
@ -167,18 +483,21 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="add"> <method name="add">
<parameter name="value"/> <parameter name="value"/>
<body> <body>
<![CDATA[ <![CDATA[
if (value) if (value) {
{
return this.item.addTag(value); return this.item.addTag(value);
} }
return false; return false;
]]> ]]>
</body> </body>
</method> </method>
<method name="replace"> <method name="replace">
<parameter name="oldTagID"/> <parameter name="oldTagID"/>
<parameter name="newTag"/> <parameter name="newTag"/>
@ -196,6 +515,8 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="remove"> <method name="remove">
<parameter name="id"/> <parameter name="id"/>
<body> <body>
@ -204,6 +525,8 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="updateCount"> <method name="updateCount">
<parameter name="count"/> <parameter name="count"/>
<body> <body>
@ -235,14 +558,8 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="id">
<parameter name="id"/>
<body>
<![CDATA[
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
]]>
</body>
</method>
<method name="fixPopup"> <method name="fixPopup">
<body> <body>
<![CDATA[ <![CDATA[
@ -262,6 +579,8 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="closePopup"> <method name="closePopup">
<body> <body>
<![CDATA[ <![CDATA[
@ -271,10 +590,78 @@
]]> ]]>
</body> </body>
</method> </method>
<method name="getScrollBox">
<!--
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="box"/>
<parameter name="tabindex"/>
<parameter name="back"/>
<body> <body>
<![CDATA[ <![CDATA[
return document.getAnonymousNodes(this)[0]; tabindex = parseInt(tabindex);
if (back) {
switch (tabindex) {
case 1:
return false;
default:
var nextIndex = tabindex - 1;
}
}
else {
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);
var next = document.getAnonymousNodes(box)[0].
getElementsByAttribute('ztabindex', nextIndex);
if (!next[0]) {
next[0] = box.addDynamicRow();
}
next[0].click();
this.ensureElementIsVisible(next[0]);
return true;
]]>
</body>
</method>
<method name="ensureElementIsVisible">
<parameter name="elem"/>
<body>
<![CDATA[
var scrollbox = document.getAnonymousNodes(this)[0];
var sbo = scrollbox.boxObject;
sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject);
sbo.ensureElementIsVisible(elem);
]]>
</body>
</method>
<method name="id">
<parameter name="id"/>
<body>
<![CDATA[
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
]]> ]]>
</body> </body>
</method> </method>

View File

@ -181,7 +181,7 @@
for (var tagID in this._tags) { for (var tagID in this._tags) {
// If the last tag was the same, add this tagID and tagType to it // If the last tag was the same, add this tagID and tagType to it
if (tagsToggleBox.lastChild && if (tagsToggleBox.lastChild &&
tagsToggleBox.lastChild.getAttribute('value') == this._tags[tagID].tag) { tagsToggleBox.lastChild.getAttribute('value') == this._tags[tagID].name) {
tagsToggleBox.lastChild.setAttribute('tagID', tagsToggleBox.lastChild.getAttribute('tagID') + '-' + tagID); tagsToggleBox.lastChild.setAttribute('tagID', tagsToggleBox.lastChild.getAttribute('tagID') + '-' + tagID);
tagsToggleBox.lastChild.setAttribute('tagType', tagsToggleBox.lastChild.getAttribute('tagType') + '-' + this._tags[tagID].type); tagsToggleBox.lastChild.setAttribute('tagType', tagsToggleBox.lastChild.getAttribute('tagType') + '-' + this._tags[tagID].type);
continue; continue;
@ -190,7 +190,7 @@
var label = document.createElement('label'); var label = document.createElement('label');
label.setAttribute('onclick', "this.parentNode.parentNode.parentNode.handleTagClick(event, this)"); label.setAttribute('onclick', "this.parentNode.parentNode.parentNode.handleTagClick(event, this)");
label.className = 'zotero-clicky'; label.className = 'zotero-clicky';
label.setAttribute('value', this._tags[tagID].tag); label.setAttribute('value', this._tags[tagID].name);
label.setAttribute('tagID', tagID); label.setAttribute('tagID', tagID);
label.setAttribute('tagType', this._tags[tagID].type); label.setAttribute('tagType', this._tags[tagID].type);
label.setAttribute('context', 'tag-menu'); label.setAttribute('context', 'tag-menu');

View File

@ -120,16 +120,15 @@ var ZoteroItemPane = new function() {
// Info pane // Info pane
if (index == 0) { if (index == 0) {
var itembox = document.getElementById('zotero-editpane-item-box');
// Hack to allow read-only mode in right pane -- probably a better // Hack to allow read-only mode in right pane -- probably a better
// way to allow access to this // way to allow access to this
if (mode) { if (mode) {
itembox.mode = mode; _itemBox.mode = mode;
} }
else { else {
itembox.mode = 'edit'; _itemBox.mode = 'edit';
} }
itembox.item = _itemBeingEdited; _itemBox.item = _itemBeingEdited;
} }
// Notes pane // Notes pane
@ -147,7 +146,9 @@ var ZoteroItemPane = new function() {
icon.setAttribute('src','chrome://zotero/skin/treeitem-note.png'); icon.setAttribute('src','chrome://zotero/skin/treeitem-note.png');
var label = document.createElement('label'); var label = document.createElement('label');
label.setAttribute('value',_noteToTitle(notes[i].getNote())); var title = Zotero.Notes.noteToTitle(notes[i].getNote());
title = title ? title : Zotero.getString('pane.item.notes.untitled');
label.setAttribute('value', title);
label.setAttribute('flex','1'); //so that the long names will flex smaller label.setAttribute('flex','1'); //so that the long names will flex smaller
label.setAttribute('crop','end'); label.setAttribute('crop','end');
@ -236,6 +237,13 @@ var ZoteroItemPane = new function() {
// Tags pane // Tags pane
else if(index == 3) else if(index == 3)
{ {
if (mode) {
_tagsBox.mode = mode;
}
else {
_tagsBox.mode = 'edit';
}
var focusMode = 'tags'; var focusMode = 'tags';
var focusBox = _tagsBox; var focusBox = _tagsBox;
_tagsBox.item = _itemBeingEdited; _tagsBox.item = _itemBeingEdited;
@ -269,27 +277,6 @@ var ZoteroItemPane = new function() {
ZoteroPane.openNoteWindow(null, null, _itemBeingEdited.id); ZoteroPane.openNoteWindow(null, null, _itemBeingEdited.id);
} }
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() function _updateNoteCount()
{ {
var c = _notesList.childNodes.length; var c = _notesList.childNodes.length;

View File

@ -28,6 +28,7 @@
<overlay <overlay
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="include.js"/>
<script src="itemPane.js"/> <script src="itemPane.js"/>
<deck id="zotero-view-item" flex="1" onselect="if (this.selectedIndex !== '') { ZoteroItemPane.loadPane(this.selectedIndex); }"> <deck id="zotero-view-item" flex="1" onselect="if (this.selectedIndex !== '') { ZoteroItemPane.loadPane(this.selectedIndex); }">
@ -46,6 +47,7 @@
<rows id="zotero-editpane-dynamic-notes" flex="1"/> <rows id="zotero-editpane-dynamic-notes" flex="1"/>
</grid> </grid>
</vbox> </vbox>
<vbox flex="1"> <vbox flex="1">
<hbox align="center"> <hbox align="center">
<label id="zotero-editpane-attachments-label"/> <label id="zotero-editpane-attachments-label"/>
@ -66,7 +68,9 @@
<rows id="zotero-editpane-dynamic-attachments" flex="1"/> <rows id="zotero-editpane-dynamic-attachments" flex="1"/>
</grid> </grid>
</vbox> </vbox>
<tagsbox id="zotero-editpane-tags" flex="1"/> <tagsbox id="zotero-editpane-tags" flex="1"/>
<seealsobox id="zotero-editpane-related" flex="1"/> <seealsobox id="zotero-editpane-related" flex="1"/>
</deck> </deck>
</overlay> </overlay>

View File

@ -785,8 +785,7 @@ var ZoteroPane = new function()
{ {
var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex); var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex);
if(item.ref.isNote()) if(item.ref.isNote()) {
{
var noteEditor = document.getElementById('zotero-note-editor'); var noteEditor = document.getElementById('zotero-note-editor');
if (this.itemsView.readOnly) { if (this.itemsView.readOnly) {
noteEditor.mode = 'view'; noteEditor.mode = 'view';
@ -817,8 +816,8 @@ var ZoteroPane = new function()
} }
document.getElementById('zotero-item-pane-content').selectedIndex = 2; document.getElementById('zotero-item-pane-content').selectedIndex = 2;
} }
else if(item.ref.isAttachment())
{ else if(item.ref.isAttachment()) {
// DEBUG: this is annoying -- we really want to use an abstracted // DEBUG: this is annoying -- we really want to use an abstracted
// version of createValueElement() from itemPane.js // version of createValueElement() from itemPane.js
// (ideally in an XBL binding) // (ideally in an XBL binding)
@ -956,6 +955,8 @@ var ZoteroPane = new function()
document.getElementById('zotero-item-pane-content').selectedIndex = 3; document.getElementById('zotero-item-pane-content').selectedIndex = 3;
} }
// Regular item
else else
{ {
ZoteroItemPane.viewItem(item.ref, this.itemsView.readOnly ? 'view' : false); ZoteroItemPane.viewItem(item.ref, this.itemsView.readOnly ? 'view' : false);

View File

@ -551,12 +551,15 @@ Zotero.Collection.prototype.addItems = function(itemIDs) {
* Remove an item from the collection (does not delete item from library) * Remove an item from the collection (does not delete item from library)
**/ **/
Zotero.Collection.prototype.removeItem = function(itemID) { Zotero.Collection.prototype.removeItem = function(itemID) {
var index = this.getChildItems(true).indexOf(itemID); var childItems = this.getChildItems(true);
if (childItems) {
var index = childItems.indexOf(itemID);
if (index == -1) { if (index == -1) {
Zotero.debug("Item " + itemID + " not a child of collection " Zotero.debug("Item " + itemID + " not a child of collection "
+ this.id + " in Zotero.Collection.removeItem()"); + this.id + " in Zotero.Collection.removeItem()");
return false; return false;
} }
}
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();

View File

@ -353,6 +353,7 @@ Zotero.Creator.prototype.erase = function () {
Zotero.debug("Deleting creator " + this.id); Zotero.debug("Deleting creator " + this.id);
// TODO: notifier
var changedItems = []; var changedItems = [];
var changedItemsNotifierData = {}; var changedItemsNotifierData = {};

View File

@ -50,14 +50,14 @@ Zotero.Creators = new function() {
return _creatorsByID[creatorID]; return _creatorsByID[creatorID];
} }
var sql = 'SELECT * FROM creators WHERE creatorID=?'; var sql = 'SELECT COUNT(*) FROM creators WHERE creatorID=?';
var result = Zotero.DB.rowQuery(sql, creatorID); var result = Zotero.DB.valueQuery(sql, creatorID);
if (!result) { if (!result) {
return false; return false;
} }
_creatorsByID[creatorID] = new Zotero.Creator(result.creatorID); _creatorsByID[creatorID] = new Zotero.Creator(creatorID);
return _creatorsByID[creatorID]; return _creatorsByID[creatorID];
} }

View File

@ -2364,12 +2364,12 @@ Zotero.Item.prototype.getBestSnapshot = function() {
// //
// save() is not required for tag functions // save() is not required for tag functions
// //
Zotero.Item.prototype.addTag = function(tag, type) { Zotero.Item.prototype.addTag = function(name, type) {
if (!this.id) { if (!this.id) {
throw ('Cannot add tag to unsaved item in Item.addTag()'); throw ('Cannot add tag to unsaved item in Item.addTag()');
} }
if (!tag) { if (!name) {
Zotero.debug('Not saving empty tag in Item.addTag()', 2); Zotero.debug('Not saving empty tag in Item.addTag()', 2);
return false; return false;
} }
@ -2378,18 +2378,13 @@ Zotero.Item.prototype.addTag = function(tag, type) {
type = 0; type = 0;
} }
if (type !=0 && type !=1) {
throw ('Invalid tag type in Item.addTag()');
}
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();
var tagID = Zotero.Tags.getID(tag, type);
var existingTypes = Zotero.Tags.getTypes(tag);
var existingTypes = Zotero.Tags.getTypes(name);
if (existingTypes) { if (existingTypes) {
// If existing automatic and adding identical user, remove automatic // If existing automatic and adding identical user, remove automatic
if (type == 0 && existingTypes.indexOf(1) != -1) { if (type == 0 && existingTypes.indexOf(1) != -1) {
this.removeTag(Zotero.Tags.getID(tag, 1)); this.removeTag(Zotero.Tags.getID(name, 1));
} }
// If existing user and adding automatic, skip // If existing user and adding automatic, skip
else if (type == 1 && existingTypes.indexOf(0) != -1) { else if (type == 1 && existingTypes.indexOf(0) != -1) {
@ -2399,8 +2394,12 @@ Zotero.Item.prototype.addTag = function(tag, type) {
} }
} }
var tagID = Zotero.Tags.getID(name, type);
if (!tagID) { if (!tagID) {
var tagID = Zotero.Tags.add(tag, type); var tag = new Zotero.Tag;
tag.name = name;
tag.type = type;
var tagID = tag.save();
} }
try { try {
@ -2433,38 +2432,20 @@ Zotero.Item.prototype.addTags = function (tags, type) {
Zotero.Item.prototype.addTagByID = function(tagID) { Zotero.Item.prototype.addTagByID = function(tagID) {
if (!this.id) { if (!this.id) {
throw ('Cannot add tag to unsaved item in Item.addTagByID()'); throw ('Cannot add tag to unsaved item in Zotero.Item.addTagByID()');
} }
if (!tagID) { if (!tagID) {
Zotero.debug('Not saving nonexistent tag in Item.addTagByID()', 2); throw ('tagID not provided in Zotero.Item.addTagByID()');
return false;
} }
var sql = "SELECT COUNT(*) FROM tags WHERE tagID = ?"; var tag = Zotero.Tags.get(tagID);
var count = !!Zotero.DB.valueQuery(sql, tagID); if (!tag) {
throw ('Cannot add invalid tag ' + tagID + ' in Zotero.Item.addTagByID()');
if (!count) {
throw ('Cannot add invalid tag id ' + tagID + ' in Item.addTagByID()');
} }
Zotero.DB.beginTransaction(); tag.addItem(this.id);
tag.save();
// 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) { Zotero.Item.prototype.hasTag = function(tagID) {
@ -2484,28 +2465,38 @@ Zotero.Item.prototype.hasTags = function(tagIDs) {
return !!Zotero.DB.valueQuery(sql, [this.id].concat(tagIDs)); return !!Zotero.DB.valueQuery(sql, [this.id].concat(tagIDs));
} }
/**
* Returns all tags assigned to an item
*
* @return array Array of Zotero.Tag objects
*/
Zotero.Item.prototype.getTags = function() { Zotero.Item.prototype.getTags = function() {
if (!this.id) { if (!this.id) {
return false; return false;
} }
var sql = "SELECT tagID AS id, tag, tagType AS type FROM tags WHERE tagID IN " var sql = "SELECT tagID, name FROM tags WHERE tagID IN "
+ "(SELECT tagID FROM itemTags WHERE itemID=" + this.id + ")"; + "(SELECT tagID FROM itemTags WHERE itemID=?)";
var tags = Zotero.DB.query(sql, this.id);
var tags = Zotero.DB.query(sql);
if (!tags) { if (!tags) {
return false; return false;
} }
var collation = Zotero.getLocaleCollation(); var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) { tags.sort(function(a, b) {
return collation.compareString(1, a.tag, b.tag); return collation.compareString(1, a.name, b.name);
}); });
return tags;
var tagObjs = [];
for (var i=0; i<tags.length; i++) {
var tag = Zotero.Tags.get(tags[i].tagID, true);
tagObjs.push(tag);
}
return tagObjs;
} }
Zotero.Item.prototype.getTagIDs = function() { Zotero.Item.prototype.getTagIDs = function() {
var sql = "SELECT tagID FROM itemTags WHERE itemID=" + this.id; var sql = "SELECT tagID FROM itemTags WHERE itemID=?";
return Zotero.DB.columnQuery(sql); return Zotero.DB.columnQuery(sql, this.id);
} }
Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) { Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) {
@ -2537,16 +2528,20 @@ Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) {
Zotero.Item.prototype.removeTag = function(tagID) { Zotero.Item.prototype.removeTag = function(tagID) {
if (!this.id) { if (!this.id) {
throw ('Cannot remove tag on unsaved item'); throw ('Cannot remove tag on unsaved item in Zotero.Item.removeTag()');
} }
Zotero.DB.beginTransaction(); if (!tagID) {
var sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=?"; throw ('tagID not provided in Zotero.Item.removeTag()');
Zotero.DB.query(sql, [this.id, { int: tagID }]); }
Zotero.Tags.purge();
Zotero.DB.commitTransaction(); var tag = Zotero.Tags.get(tagID);
Zotero.Notifier.trigger('modify', 'item', this.id); if (!tag) {
Zotero.Notifier.trigger('remove', 'item-tag', this.id + '-' + tagID); throw ('Cannot remove invalid tag ' + tagID + ' in Zotero.Item.removeTag()');
}
tag.removeItem(this.id);
tag.save();
} }
Zotero.Item.prototype.removeAllTags = function() { Zotero.Item.prototype.removeAllTags = function() {
@ -3188,10 +3183,14 @@ Zotero.Item.prototype.toArray = function (mode) {
} }
} }
arr.tags = this.getTags();
if (!arr.tags) {
arr.tags = []; arr.tags = [];
var tags = this.getTags();
if (tags) {
for (var i=0; i<tags.length; i++) {
arr.tags.push(tags[i].serialize());
} }
}
arr.related = this.getSeeAlso(); arr.related = this.getSeeAlso();
if (!arr.related) { if (!arr.related) {
arr.related = []; arr.related = [];
@ -3312,10 +3311,14 @@ Zotero.Item.prototype.serialize = function(mode) {
} }
} }
arr.tags = this.getTags();
if (!arr.tags) {
arr.tags = []; arr.tags = [];
var tags = this.getTags();
if (tags) {
for (var i=0; i<tags.length; i++) {
arr.tags.push(tags[i].serialize());
} }
}
arr.related = this.getSeeAlso(); arr.related = this.getSeeAlso();
if (!arr.related) { if (!arr.related) {
arr.related = []; arr.related = [];

View File

@ -0,0 +1,544 @@
/*
***** 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.Tag = function(tagID) {
this._tagID = tagID ? tagID : null;
this._init();
}
Zotero.Tag.prototype._init = function () {
// Public members for access by public methods -- do not access directly
this._name = null;
this._type = null;
this._dateModified = null;
this._key = null;
this._loaded = false;
this._changed = false;
this._previousData = false;
this._linkedItemsLoaded = false;
this._linkedItems = [];
}
Zotero.Tag.prototype.__defineGetter__('id', function () { return this._tagID; });
Zotero.Tag.prototype.__defineSetter__('tagID', function (val) { this._set('tagID', val); });
Zotero.Tag.prototype.__defineGetter__('name', function () { return this._get('name'); });
Zotero.Tag.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
Zotero.Tag.prototype.__defineGetter__('type', function () { return this._get('type'); });
Zotero.Tag.prototype.__defineSetter__('type', function (val) { this._set('type', val); });
Zotero.Tag.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
Zotero.Tag.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
Zotero.Tag.prototype.__defineGetter__('key', function () { return this._get('key'); });
Zotero.Tag.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
Zotero.Tag.prototype.__defineSetter__('linkedItems', function (arr) { this._setLinkedItems(arr); });
Zotero.Tag.prototype._get = function (field) {
if (this.id && !this._loaded) {
this.load();
}
return this['_' + field];
}
Zotero.Tag.prototype._set = function (field, val) {
switch (field) {
case 'id': // set using constructor
//case 'tagID': // set using constructor
throw ("Invalid field '" + field + "' in Zotero.Tag.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;
}
}
}
/**
* Check if tag exists in the database
*
* @return bool TRUE if the tag exists, FALSE if not
*/
Zotero.Tag.prototype.exists = function() {
if (!this.id) {
throw ('tagID not set in Zotero.Tag.exists()');
}
var sql = "SELECT COUNT(*) FROM tags WHERE tagID=?";
return !!Zotero.DB.valueQuery(sql, this.id);
}
/*
* Build tag from database
*/
Zotero.Tag.prototype.load = function() {
Zotero.debug("Loading data for tag " + this.id + " in Zotero.Tag.load()");
if (!this.id) {
throw ("tagID not set in Zotero.Tag.load()");
}
var sql = "SELECT name, type, dateModified, key FROM tags WHERE tagID=?";
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];
}
}
/**
* Returns items linked to this tag
*
* @param bool asIDs Return as itemIDs
* @return array Array of Zotero.Item instances or itemIDs,
* or FALSE if none
*/
Zotero.Tag.prototype.getLinkedItems = function (asIDs) {
if (!this._linkedItemsLoaded) {
this._loadLinkedItems();
}
if (this._linkedItems.length == 0) {
return false;
}
// Return itemIDs
if (asIDs) {
var ids = [];
for each(var item in this._linkedItems) {
ids.push(item.id);
}
return ids;
}
// Return Zotero.Item objects
var objs = [];
for each(var item in this._linkedItems) {
objs.push(item);
}
return objs;
}
Zotero.Tag.prototype._setLinkedItems = function (itemIDs) {
if (!this._linkedItemsLoaded) {
this._loadLinkedItems();
}
if (itemIDs.constructor.name != 'Array') {
throw ('ids must be an array in Zotero.Tag._setLinkedItems()');
}
var currentIDs = this.getLinkedItems(true);
if (!currentIDs) {
currentIDs = [];
}
var oldIDs = []; // children being kept
var newIDs = []; // new children
if (itemIDs.length == 0) {
if (currentIDs.length == 0) {
Zotero.debug('No linked items added', 4);
return false;
}
}
else {
for (var i in itemIDs) {
var id = parseInt(itemIDs[i]);
if (isNaN(id)) {
throw ("Invalid itemID '" + itemIDs[i]
+ "' in Zotero.Tag._setLinkedItems()");
}
if (currentIDs.indexOf(id) != -1) {
Zotero.debug("Item " + itemIDs[i]
+ " is already linked to tag " + this.id);
oldIDs.push(id);
continue;
}
newIDs.push(id);
}
}
// Mark as changed if new or removed ids
if (newIDs.length > 0 || oldIDs.length != currentIDs.length) {
this._prepFieldChange('linkedItems');
}
else {
Zotero.debug('Linked items not changed in Zotero.Tag._setLinkedItems()', 4);
return false;
}
newIDs = oldIDs.concat(newIDs);
var items = Zotero.Items.get(itemIDs);
this._linkedItems = items ? items : [];
return true;
}
Zotero.Tag.prototype.addItem = function (itemID) {
var current = this.getLinkedItems(true);
if (current && current.indexOf(itemID) != -1) {
Zotero.debug("Item " + itemID + " already has tag "
+ this.id + " in Zotero.Tag.addItem()");
return false;
}
this._prepFieldChange('linkedItems');
var item = Zotero.Items.get(itemID);
if (!item) {
throw ("Can't link invalid item " + itemID + " to tag " + this.id
+ " in Zotero.Tag.addItem()");
}
this._linkedItems.push(item);
return true;
}
Zotero.Tag.prototype.removeItem = function (itemID) {
var current = this.getLinkedItems(true);
if (current) {
var index = current.indexOf(itemID);
}
if (!current || index == -1) {
Zotero.debug("Item " + itemID + " doesn't have tag "
+ this.id + " in Zotero.Tag.removeItem()");
return false;
}
this._prepFieldChange('linkedItems');
this._linkedItems.splice(index, 1);
return true;
}
Zotero.Tag.prototype.save = function () {
// Default to manual tag
if (!this.type) {
this.type = 0;
}
if (this.type != 0 && this.type != 1) {
throw ('Invalid tag type ' + this.type + ' in Zotero.Tag.save()');
}
if (!this.name) {
throw ('Tag name is empty in Zotero.Tag.save()');
}
if (!this._changed) {
Zotero.debug("Tag " + this.id + " has not changed");
return false;
}
Zotero.DB.beginTransaction();
// ID change
if (this._changed.tagID) {
var oldID = this._previousData.primary.tagID;
var params = [this.id, oldID];
Zotero.debug("Changing tagID " + oldID + " to " + this.id);
var row = Zotero.DB.rowQuery("SELECT * FROM tags WHERE tagID=?", oldID);
// Set type on old row to -1, since there's a UNIQUE on name/type
Zotero.DB.query("UPDATE tags SET type=-1 WHERE tagID=?", 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 tags VALUES (?, ?, ?, ?, ?)",
[this.id, row.name, row.type, row.dateModified, 'TEMPKEY']);
Zotero.DB.query("UPDATE itemTags SET tagID=? WHERE tagID=?", params);
Zotero.DB.query("DELETE FROM tags WHERE tagID=?", oldID);
Zotero.DB.query("UPDATE tags SET key=? WHERE tagID=?", [row.key, this.id]);
Zotero.Tags.unload([{ oldID: { name: row.name, type: row.type } }]);
Zotero.Notifier.trigger('id-change', 'tag', oldID + '-' + this.id);
// update caches
}
var isNew = !this.id || !this.exists();
try {
// how to know if date modified changed (in server code too?)
var tagID = this.id ? this.id : Zotero.ID.get('tags');
Zotero.debug("Saving tag " + this.id);
var key = this.key ? this.key : this._generateKey();
var columns = [
'tagID', 'name', 'type', 'dateModified', 'key'
];
var placeholders = ['?', '?', '?', '?', '?'];
var sqlValues = [
tagID ? { int: tagID } : null,
{ string: this.name },
{ int: this.type },
// If date modified hasn't changed, use current timestamp
this._changed.dateModified ?
this.dateModified : Zotero.DB.transactionDateTime,
key
];
var sql = "REPLACE INTO tags (" + columns.join(', ') + ") VALUES ("
+ placeholders.join(', ') + ")";
var insertID = Zotero.DB.query(sql, sqlValues);
if (!tagID) {
tagID = insertID;
}
// Linked items
if (this._changed.linkedItems) {
var removed = [];
var newids = [];
var currentIDs = this.getLinkedItems(true);
if (!currentIDs) {
currentIDs = [];
}
if (this._previousData.linkedItems) {
for each(var id in this._previousData.linkedItems) {
if (currentIDs.indexOf(id) == -1) {
removed.push(id);
}
}
}
for each(var id in currentIDs) {
if (this._previousData.linkedItems &&
this._previousData.linkedItems.indexOf(id) != -1) {
continue;
}
newids.push(id);
}
if (removed.length) {
var sql = "DELETE FROM itemTags WHERE tagID=? "
+ "AND itemID IN ("
+ removed.map(function () '?').join()
+ ")";
Zotero.DB.query(sql, [tagID].concat(removed));
}
if (newids.length) {
var sql = "INSERT INTO itemTags (itemID, tagID) VALUES (?,?)";
var insertStatement = Zotero.DB.getStatement(sql);
for each(var itemID in newids) {
insertStatement.bindInt32Parameter(0, itemID);
insertStatement.bindInt32Parameter(1, tagID);
try {
insertStatement.execute();
}
catch (e) {
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
}
}
}
//Zotero.Notifier.trigger('add', 'tag-item', this.id + '-' + itemID);
// TODO: notify linked items of name changes?
// Zotero.Notifier.trigger('modify', 'item', itemIDs);
}
Zotero.DB.commitTransaction();
}
catch (e) {
Zotero.DB.rollbackTransaction();
throw (e);
}
// If successful, set values in object
if (!this.id) {
this._tagID = tagID;
}
if (!this.key) {
this._key = key;
}
Zotero.Tags.reload(this.id);
if (isNew) {
Zotero.Notifier.trigger('add', 'tag', this.id);
}
else {
Zotero.Notifier.trigger('modify', 'tag', this.id, this._previousData);
}
return this.id;
}
Zotero.Tag.prototype.serialize = function () {
var obj = {
primary: {
tagID: this.id,
dateModified: this.dateModified,
key: this.key
},
name: this.name,
type: this.type,
linkedItems: this.getLinkedItems(true),
};
return obj;
}
/**
* Remove tag from all linked items
*
* Tags.erase() should be used externally instead of this
*
* Actual deletion of tag occurs in Zotero.Tags.purge(),
* which is called by Tags.erase()
*/
Zotero.Tag.prototype.erase = function () {
Zotero.debug('Deleting tag ' + this.id);
if (!this.id) {
return;
}
var linkedItems = [];
var linkedItemsNotifierData = {};
Zotero.DB.beginTransaction();
var deletedTagNotifierData = {};
deletedTagNotifierData[this.id] = { old: this.serialize() };
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
var linkedItemIDs = Zotero.DB.columnQuery(sql, this.id);
if (!linkedItemIDs) {
Zotero.DB.commitTransaction();
return;
}
var sql = "DELETE FROM itemTags WHERE tagID=?";
Zotero.DB.query(sql, this.id);
var itemTags = [];
for each(var itemID in linkedItemIDs) {
var item = Zotero.Items.get(itemID)
if (!item) {
throw ('Linked item not found in Zotero.Tag.erase()');
}
linkedItems.push(itemID);
linkedItemsNotifierData[itemID] = { old: item.serialize() };
itemTags.push(itemID + '-' + this.id);
}
Zotero.Notifier.trigger('remove', 'item-tag', itemTags);
// Send notification of linked items
if (linkedItems.length) {
Zotero.Notifier.trigger('modify', 'item', linkedItems, linkedItemsNotifierData);
}
Zotero.Notifier.trigger('delete', 'tag', this.id, deletedTagNotifierData);
Zotero.DB.commitTransaction();
return;
}
Zotero.Tag.prototype._loadLinkedItems = function() {
if (!this._loaded) {
this.load();
}
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
var ids = Zotero.DB.columnQuery(sql, this.id);
this._linkedItems = [];
if (ids) {
for each(var id in ids) {
this._linkedItems.push(Zotero.Items.get(id));
}
}
this._linkedItemsLoaded = true;
}
Zotero.Tag.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 tag exists
if (this.id && this.exists() && !this._previousData) {
this._previousData = this.serialize();
}
}
Zotero.Tag.prototype._generateKey = function () {
return Zotero.ID.getKey();
}

View File

@ -33,37 +33,37 @@ Zotero.Tags = new function() {
this.getID = getID; this.getID = getID;
this.getIDs = getIDs; this.getIDs = getIDs;
this.getTypes = getTypes; this.getTypes = getTypes;
this.getUpdated = getUpdated;
this.getAll = getAll; this.getAll = getAll;
this.getAllWithinSearch = getAllWithinSearch; this.getAllWithinSearch = getAllWithinSearch;
this.getTagItems = getTagItems; this.getTagItems = getTagItems;
this.search = search; this.search = search;
this.add = add;
this.rename = rename; this.rename = rename;
this.remove = remove; this.reload = reload;
this.erase = erase;
this.purge = purge; this.purge = purge;
this.toArray = toArray; this.unload = unload;
/* /*
* Returns a tag and type for a given tagID * Returns a tag and type for a given tagID
*/ */
function get(tagID) { function get(tagID, skipCheck) {
if (_tagsByID[tagID]) { if (_tagsByID[tagID]) {
return _tagsByID[tagID]; return _tagsByID[tagID];
} }
var sql = 'SELECT tag, tagType FROM tags WHERE tagID=?'; if (!skipCheck) {
var result = Zotero.DB.rowQuery(sql, tagID); var sql = 'SELECT COUNT(*) FROM tags WHERE tagID=?';
var result = Zotero.DB.valueQuery(sql, tagID);
if (!result) { if (!result) {
return false; return false;
} }
}
_tagsByID[tagID] = { _tagsByID[tagID] = new Zotero.Tag(tagID);
tag: result.tag, return _tagsByID[tagID];
type: result.tagType
};
return result;
} }
@ -72,31 +72,31 @@ Zotero.Tags = new function() {
*/ */
function getName(tagID) { function getName(tagID) {
if (_tagsByID[tagID]) { if (_tagsByID[tagID]) {
return _tagsByID[tagID].tag; return _tagsByID[tagID].name;
} }
var tag = this.get(tagID); var tag = this.get(tagID);
return _tagsByID[tagID] ? _tagsByID[tagID].tag : false; return _tagsByID[tagID] ? _tagsByID[tagID].name : false;
} }
/* /*
* Returns the tagID matching given tag and type * Returns the tagID matching given tag and type
*/ */
function getID(tag, type) { function getID(name, type) {
if (_tags[type] && _tags[type]['_' + tag]) { if (_tags[type] && _tags[type]['_' + name]) {
return _tags[type]['_' + tag]; return _tags[type]['_' + name];
} }
var sql = 'SELECT tagID FROM tags WHERE tag=? AND tagType=?'; var sql = 'SELECT tagID FROM tags WHERE name=? AND type=?';
var tagID = Zotero.DB.valueQuery(sql, [tag, type]); var tagID = Zotero.DB.valueQuery(sql, [name, type]);
if (tagID) { if (tagID) {
if (!_tags[type]) { if (!_tags[type]) {
_tags[type] = []; _tags[type] = [];
} }
_tags[type]['_' + tag] = tagID; _tags[type]['_' + name] = tagID;
} }
return tagID; return tagID;
@ -106,30 +106,40 @@ Zotero.Tags = new function() {
/* /*
* Returns all tagIDs for this tag (of all types) * Returns all tagIDs for this tag (of all types)
*/ */
function getIDs(tag) { function getIDs(name) {
var sql = 'SELECT tagID FROM tags WHERE tag=?'; var sql = 'SELECT tagID FROM tags WHERE name=?';
return Zotero.DB.columnQuery(sql, [tag]); return Zotero.DB.columnQuery(sql, [name]);
} }
/* /*
* Returns an array of tagTypes for tags matching given tag * Returns an array of tag types for tags matching given tag
*/ */
function getTypes(tag) { function getTypes(name) {
var sql = 'SELECT tagType FROM tags WHERE tag=?'; var sql = 'SELECT type FROM tags WHERE name=?';
return Zotero.DB.columnQuery(sql, [tag]); return Zotero.DB.columnQuery(sql, [name]);
}
function getUpdated(date) {
var sql = "SELECT tagID FROM tags";
if (date) {
sql += " WHERE dateModified>?";
return Zotero.DB.columnQuery(sql, Zotero.Date.dateToSQL(date, true));
}
return Zotero.DB.columnQuery(sql);
} }
/** /**
* Get all tags indexed by tagID * Get all tags indexed by tagID
* *
* _types_ is an optional array of tagTypes to fetch * _types_ is an optional array of tag types to fetch
*/ */
function getAll(types) { function getAll(types) {
var sql = "SELECT tagID, tag, tagType FROM tags "; var sql = "SELECT tagID, name FROM tags ";
if (types) { if (types) {
sql += "WHERE tagType IN (" + types.join() + ") "; sql += "WHERE type IN (" + types.join() + ") ";
} }
var tags = Zotero.DB.query(sql); var tags = Zotero.DB.query(sql);
if (!tags) { if (!tags) {
@ -138,15 +148,13 @@ Zotero.Tags = new function() {
var collation = Zotero.getLocaleCollation(); var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) { tags.sort(function(a, b) {
return collation.compareString(1, a.tag, b.tag); return collation.compareString(1, a.name, b.name);
}); });
var indexed = {}; var indexed = {};
for (var i=0; i<tags.length; i++) { for (var i=0; i<tags.length; i++) {
indexed[tags[i].tagID] = { var tag = this.get(tags[i].tagID, true);
tag: tags[i].tag, indexed[tags[i].tagID] = tag;
type: tags[i].tagType
};
} }
return indexed; return indexed;
} }
@ -155,7 +163,7 @@ Zotero.Tags = new function() {
/* /*
* Get all tags within the items of a Zotero.Search object * Get all tags within the items of a Zotero.Search object
* *
* _types_ is an optional array of tagTypes to fetch * _types_ is an optional array of tag types to fetch
*/ */
function getAllWithinSearch(search, types) { function getAllWithinSearch(search, types) {
// Save search results to temporary table // Save search results to temporary table
@ -176,11 +184,11 @@ Zotero.Tags = new function() {
return {}; return {};
} }
var sql = "SELECT DISTINCT tagID, tag, tagType FROM itemTags " var sql = "SELECT DISTINCT tagID, name, type FROM itemTags "
+ "NATURAL JOIN tags WHERE itemID IN " + "NATURAL JOIN tags WHERE itemID IN "
+ "(SELECT itemID FROM " + tmpTable + ") "; + "(SELECT itemID FROM " + tmpTable + ") ";
if (types) { if (types) {
sql += "AND tagType IN (" + types.join() + ") "; sql += "AND type IN (" + types.join() + ") ";
} }
var tags = Zotero.DB.query(sql); var tags = Zotero.DB.query(sql);
@ -192,15 +200,13 @@ Zotero.Tags = new function() {
var collation = Zotero.getLocaleCollation(); var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) { tags.sort(function(a, b) {
return collation.compareString(1, a.tag, b.tag); return collation.compareString(1, a.name, b.name);
}); });
var indexed = {}; var indexed = {};
for (var i=0; i<tags.length; i++) { for (var i=0; i<tags.length; i++) {
indexed[tags[i].tagID] = { var tag = this.get(tags[i].tagID, true);
tag: tags[i].tag, indexed[tags[i].tagID] = tag;
type: tags[i].tagType
};
} }
return indexed; return indexed;
} }
@ -213,76 +219,49 @@ Zotero.Tags = new function() {
function search(str) { function search(str) {
var sql = 'SELECT tagID, tag, tagType FROM tags'; var sql = 'SELECT tagID, name, type FROM tags';
if (str) { if (str) {
sql += ' WHERE tag LIKE ?'; sql += ' WHERE name LIKE ?';
} }
sql += ' ORDER BY tag COLLATE NOCASE';
var tags = Zotero.DB.query(sql, str ? '%' + str + '%' : undefined); var tags = Zotero.DB.query(sql, str ? '%' + str + '%' : undefined);
if (!tags) {
return {};
}
var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) {
return collation.compareString(1, a.name, b.name);
});
var indexed = {}; var indexed = {};
for each(var tag in tags) { for (var i=0; i<tags.length; i++) {
indexed[tag.tagID] = { var tag = this.get(tags[i].tagID, true);
tag: tag.tag, indexed[tags[i].tagID] = tag;
type: tag.tagType
};
} }
return indexed; return indexed;
} }
/* function rename(tagID, name) {
* 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.debug('Renaming tag', 4);
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();
var tagObj = this.get(tagID); var tagObj = this.get(tagID);
var oldName = tagObj.tag; var oldName = tagObj.name;
var oldType = tagObj.type; var oldType = tagObj.type;
var notifierData = {}; var notifierData = {};
notifierData[this.id] = { old: this.toArray() }; notifierData[tagID] = { old: tag.serialize() };
if (oldName == tag) { if (oldName == name) {
// 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(); Zotero.DB.commitTransaction();
return; return;
} }
// Check if the new tag already exists // Check if the new tag already exists
var sql = "SELECT tagID FROM tags WHERE tag=? AND tagType=0"; var sql = "SELECT tagID FROM tags WHERE name=? AND type=0";
var existingTagID = Zotero.DB.valueQuery(sql, tag); var existingTagID = Zotero.DB.valueQuery(sql, name);
if (existingTagID) { if (existingTagID) {
var itemIDs = this.getTagItems(tagID); var itemIDs = this.getTagItems(tagID);
var existingItemIDs = this.getTagItems(existingTagID); var existingItemIDs = this.getTagItems(existingTagID);
@ -316,54 +295,44 @@ Zotero.Tags = new function() {
} }
} }
Zotero.Notifier.trigger('add', 'item-tag', itemTags); Zotero.Notifier.trigger('add', 'item-tag', itemTags);
// TODO: notify linked items?
//Zotero.Notifier.trigger('modify', 'item', itemIDs);
Zotero.Notifier.trigger('modify', 'item', itemIDs);
Zotero.DB.commitTransaction(); Zotero.DB.commitTransaction();
return; return;
} }
// 0 == user tag -- we set all renamed tags to 0 tagObj.name = name;
var sql = "UPDATE tags SET tag=?, tagType=0 WHERE tagID=?"; // Set all renamed tags to manual
Zotero.DB.query(sql, [{string: tag}, tagID]); tagObj.type = 0;
tagObj.save();
var itemIDs = this.getTagItems(tagID);
if (_tags[oldType]) {
delete _tags[oldType]['_' + oldName];
}
delete _tagsByID[tagID];
Zotero.DB.commitTransaction(); Zotero.DB.commitTransaction();
Zotero.Notifier.trigger('modify', 'item', itemIDs);
Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData);
} }
function remove(tagID) { function reload(ids) {
this.unload(ids);
}
function erase(ids) {
ids = Zotero.flattenArguments(ids);
var erasedTags = {};
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();
for each(var id in ids) {
var sql = "SELECT itemID FROM itemTags WHERE tagID=?"; var tag = this.get(id);
var itemIDs = Zotero.DB.columnQuery(sql, tagID); if (tag) {
erasedTags[id] = tag.serialize();
if (!itemIDs) { tag.erase();
Zotero.DB.commitTransaction(); }
return;
} }
var sql = "DELETE FROM itemTags WHERE tagID=?"; this.unload(ids);
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(); Zotero.DB.commitTransaction();
return;
} }
@ -373,47 +342,72 @@ Zotero.Tags = new function() {
* Returns removed tagIDs on success * Returns removed tagIDs on success
*/ */
function purge() { function purge() {
Zotero.UnresponsiveScriptIndicator.disable();
try {
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();
var sql = 'SELECT tagID, tag, tagType FROM tags WHERE tagID ' var sql = "CREATE TEMPORARY TABLE tagDelete AS "
+ 'NOT IN (SELECT tagID FROM itemTags);'; + "SELECT tagID FROM tags WHERE tagID "
var toDelete = Zotero.DB.query(sql); + "NOT IN (SELECT tagID FROM itemTags);";
Zotero.DB.query(sql);
sql = "CREATE INDEX tagDelete_tagID ON tagDelete(tagID)";
Zotero.DB.query(sql);
sql = "SELECT * FROM tagDelete";
var toDelete = Zotero.DB.columnQuery(sql);
if (!toDelete) { if (!toDelete) {
Zotero.DB.commitTransaction(); Zotero.DB.rollbackTransaction();
return false; return;
} }
var purged = [];
var notifierData = {}; var notifierData = {};
// Clear tag entries in internal array for each(var tagID in toDelete) {
for each(var tag in toDelete) { var tag = Zotero.Tags.get(tagID);
notifierData[tag.tagID] = { old: Zotero.Tags.toArray(tag.tagID) } Zotero.debug(tag);
notifierData[tagID] = { old: tag.serialize() }
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 ' this.unload(toDelete);
+ '(SELECT tagID FROM itemTags);';
var result = Zotero.DB.query(sql); sql = "DELETE FROM tags WHERE tagID IN "
+ "(SELECT tagID FROM tagDelete);";
Zotero.DB.query(sql);
sql = "DROP TABLE tagDelete";
Zotero.DB.query(sql);
Zotero.DB.commitTransaction(); Zotero.DB.commitTransaction();
Zotero.Notifier.trigger('delete', 'tag', purged, notifierData); Zotero.Notifier.trigger('delete', 'tag', toDelete, notifierData);
}
return toDelete; catch (e) {
Zotero.DB.rollbackTransaction();
throw (e);
}
finally {
Zotero.UnresponsiveScriptIndicator.enable();
}
} }
function toArray(tagID) { /**
var obj = this.get(tagID); * Unload tags from caches
obj.id = tagID; *
return obj; * @param int|array ids One or more tagIDs
*/
function unload() {
var ids = Zotero.flattenArguments(arguments);
for each(var id in ids) {
var tag = _tagsByID[id];
delete _tagsByID[id];
if (tag && _tags[tag.type]) {
delete _tags[tag.type]['_' + tag.name];
}
}
} }
} }

View File

@ -47,6 +47,7 @@ Zotero.ID = new function () {
case 'creatorData': case 'creatorData':
case 'collections': case 'collections':
case 'savedSearches': case 'savedSearches':
case 'tags':
var id = _getNextAvailable(table, skip); var id = _getNextAvailable(table, skip);
if (!id && notNull) { if (!id && notNull) {
return _getNext(table, skip); return _getNext(table, skip);
@ -57,7 +58,6 @@ Zotero.ID = new function () {
// //
// TODO: use autoincrement instead where available in 1.5 // TODO: use autoincrement instead where available in 1.5
case 'itemDataValues': case 'itemDataValues':
case 'tags':
var id = _getNextAvailable(table, skip); var id = _getNextAvailable(table, skip);
if (!id) { if (!id) {
// If we can't find an empty id quickly, just use MAX() + 1 // If we can't find an empty id quickly, just use MAX() + 1

View File

@ -1447,6 +1447,27 @@ Zotero.Schema = new function(){
} }
} }
statement.reset(); statement.reset();
// Tags
var tags = Zotero.DB.query("SELECT * FROM tags");
Zotero.DB.query("DROP TABLE tags");
Zotero.DB.query("CREATE TABLE tags (\n tagID INTEGER PRIMARY KEY,\n name TEXT,\n type INT,\n dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,\n key TEXT NOT NULL UNIQUE,\n UNIQUE (name, type)\n)");
var statement = Zotero.DB.getStatement("INSERT INTO tags (tagID, name, type, key) VALUES (?,?,?,?)");
for (var j=0, len=searches.length; j<len; j++) {
statement.bindInt32Parameter(0, tags[j].tagID);
statement.bindUTF8StringParameter(1, tags[j].tag);
statement.bindInt32Parameter(2, tags[j].tagType);
var key = Zotero.ID.getKey();
statement.bindStringParameter(3, key);
try {
statement.execute();
}
catch (e) {
throw (Zotero.DB.getLastErrorString());
}
}
statement.reset();
} }
} }

View File

@ -1765,7 +1765,7 @@ Zotero.SearchConditions = new function(){
doesNotContain: true doesNotContain: true
}, },
table: 'itemTags', table: 'itemTags',
field: 'tag' field: 'name'
}, },
{ {

View File

@ -27,7 +27,12 @@ Zotero.Sync = new function() {
search: { search: {
singular: 'Search', singular: 'Search',
plural: 'Searches' plural: 'Searches'
},
tag: {
singular: 'Tag',
plural: 'Tags'
} }
}; };
}); });
@ -1068,6 +1073,8 @@ Zotero.Sync.Server.Data = new function() {
this.xmlToCreator = xmlToCreator; this.xmlToCreator = xmlToCreator;
this.searchToXML = searchToXML; this.searchToXML = searchToXML;
this.xmlToSearch = xmlToSearch; this.xmlToSearch = xmlToSearch;
this.tagToXML = tagToXML;
this.xmlToTag = xmlToTag;
var _noMergeTypes = ['search']; var _noMergeTypes = ['search'];
@ -1208,7 +1215,7 @@ Zotero.Sync.Server.Data = new function() {
// Update id in local updates array // Update id in local updates array
var index = uploadIDs.updated[types].indexOf(oldID); var index = uploadIDs.updated[types].indexOf(oldID);
if (index == -1) { if (index == -1) {
_error("Local " + type + " " + oldID + " not in " throw ("Local " + type + " " + oldID + " not in "
+ "update array when changing id"); + "update array when changing id");
} }
uploadIDs.updated[types][index] = newID; uploadIDs.updated[types][index] = newID;
@ -1256,7 +1263,7 @@ Zotero.Sync.Server.Data = new function() {
if (type != 'item') { if (type != 'item') {
alert('Delete reconciliation unimplemented for ' + types); alert('Delete reconciliation unimplemented for ' + types);
_error('Delete reconciliation unimplemented for ' + types); throw ('Delete reconciliation unimplemented for ' + types);
} }
var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
@ -1653,7 +1660,7 @@ Zotero.Sync.Server.Data = new function() {
} }
} }
else if (skipPrimary) { else if (skipPrimary) {
_error("Cannot use skipPrimary with existing item in " throw ("Cannot use skipPrimary with existing item in "
+ "Zotero.Sync.Server.Data.xmlToItem()"); + "Zotero.Sync.Server.Data.xmlToItem()");
} }
@ -1699,7 +1706,7 @@ Zotero.Sync.Server.Data = new function() {
for each(var creator in xmlItem.creator) { for each(var creator in xmlItem.creator) {
var pos = parseInt(creator.@index); var pos = parseInt(creator.@index);
if (pos != i) { if (pos != i) {
_error('No creator in position ' + i); throw ('No creator in position ' + i);
} }
item.setCreator( item.setCreator(
@ -1799,7 +1806,7 @@ Zotero.Sync.Server.Data = new function() {
} }
} }
else if (skipPrimary) { else if (skipPrimary) {
_error("Cannot use skipPrimary with existing collection in " throw ("Cannot use skipPrimary with existing collection in "
+ "Zotero.Sync.Server.Data.xmlToCollection()"); + "Zotero.Sync.Server.Data.xmlToCollection()");
} }
@ -1877,7 +1884,7 @@ Zotero.Sync.Server.Data = new function() {
} }
} }
else if (skipPrimary) { else if (skipPrimary) {
_error("Cannot use skipPrimary with existing creator in " throw ("Cannot use skipPrimary with existing creator in "
+ "Zotero.Sync.Server.Data.xmlToCreator()"); + "Zotero.Sync.Server.Data.xmlToCreator()");
} }
@ -1960,7 +1967,7 @@ Zotero.Sync.Server.Data = new function() {
} }
} }
else if (skipPrimary) { else if (skipPrimary) {
_error("Cannot use new id with existing search in " throw ("Cannot use new id with existing search in "
+ "Zotero.Sync.Server.Data.xmlToSearch()"); + "Zotero.Sync.Server.Data.xmlToSearch()");
} }
@ -2010,4 +2017,63 @@ Zotero.Sync.Server.Data = new function() {
return search; return search;
} }
function tagToXML(tag) {
var xml = <tag/>;
xml.@id = tag.id;
xml.@name = tag.name;
if (tag.type) {
xml.@type = tag.type;
}
xml.@dateModified = tag.dateModified;
xml.@key = tag.key;
var linkedItems = tag.getLinkedItems(true);
if (linkedItems) {
xml.items = linkedItems.join(' ');
}
return xml;
}
/**
* Convert E4X <tag> object into an unsaved Zotero.Tag
*
* @param object xmlTag E4X XML node with tag data
* @param object tag (Optional) Existing Zotero.Tag to update
* @param bool skipPrimary (Optional) Ignore passed primary fields
*/
function xmlToTag(xmlTag, tag, skipPrimary) {
if (!tag) {
if (skipPrimary) {
tag = new Zotero.Tag;
}
else {
tag = new Zotero.Tag(parseInt(xmlTag.@id));
/*
if (tag.exists()) {
throw ("Tag specified in XML node already exists "
+ "in Zotero.Sync.Server.Data.xmlToTag()");
}
*/
}
}
else if (skipPrimary) {
throw ("Cannot use new id with existing tag in "
+ "Zotero.Sync.Server.Data.xmlToTag()");
}
tag.name = xmlTag.@name.toString();
tag.type = parseInt(xmlTag.@type);
if (!skipPrimary) {
tag.dateModified = xmlTag.@dateModified.toString();
tag.key = xmlTag.@key.toString();
}
var str = xmlTag.items ? xmlTag.items.toString() : false;
tag.linkedItems = str ? str.split(' ') : [];
return tag;
}
} }

View File

@ -134,15 +134,21 @@ ZoteroAutoComplete.prototype.startSearch = function(searchString, searchParam,
break; break;
case 'tag': case 'tag':
var sql = "SELECT tag FROM tags WHERE tag LIKE ?"; var sql = "SELECT name FROM tags WHERE name LIKE ?";
var sqlParams = [searchString + '%']; var sqlParams = [searchString + '%'];
if (extra){ if (extra){
sql += " AND tagID NOT IN (SELECT tagID FROM itemTags WHERE " sql += " AND tagID NOT IN (SELECT tagID FROM itemTags WHERE "
+ "itemID = ?)"; + "itemID = ?)";
sqlParams.push(extra); sqlParams.push(extra);
} }
sql += " ORDER BY tag";
var results = this._zotero.DB.columnQuery(sql, sqlParams); var results = this._zotero.DB.columnQuery(sql, sqlParams);
if (results) {
var collation = Zotero.getLocaleCollation();
results.sort(function(a, b) {
return collation.compareString(1, a, b);
});
}
break; break;
case 'creator': case 'creator':

View File

@ -14,13 +14,14 @@ var ZoteroWrapped = this;
* Include the core objects to be stored within XPCOM * Include the core objects to be stored within XPCOM
*********************************************************************/ *********************************************************************/
var xpcomFiles = [ 'zotero', var xpcomFiles = ['zotero',
'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView', 'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView',
'dataServer', 'data_access', 'data/item', 'data/items', 'data/collection', 'data/collections', 'dataServer', 'data_access', 'data/item', 'data/items', 'data/collection',
'data/cachedTypes', 'data/creator', 'data/creators', 'data/itemFields', 'data/collections', 'data/cachedTypes', 'data/creator', 'data/creators',
'data/notes', 'data/tags', 'db', 'file', 'fulltext', 'id', 'ingester', 'integration', 'data/itemFields', 'data/notes', 'data/tag', 'data/tags', 'db', 'file',
'itemTreeView', 'mime', 'notifier', 'progressWindow', 'quickCopy', 'report', 'fulltext', 'id', 'ingester', 'integration', 'itemTreeView', 'mime',
'schema', 'search', 'sync', 'timeline', 'translate', 'utilities', 'zeroconf']; 'notifier', 'progressWindow', 'quickCopy', 'report', 'schema', 'search',
'sync', 'timeline', 'translate', 'utilities', 'zeroconf'];
for (var i=0; i<xpcomFiles.length; i++) { for (var i=0; i<xpcomFiles.length; i++) {
Cc["@mozilla.org/moz/jssubscript-loader;1"] Cc["@mozilla.org/moz/jssubscript-loader;1"]

View File

@ -1249,3 +1249,4 @@ INSERT INTO "syncObjectTypes" VALUES(1, 'collection');
INSERT INTO "syncObjectTypes" VALUES(2, 'creator'); INSERT INTO "syncObjectTypes" VALUES(2, 'creator');
INSERT INTO "syncObjectTypes" VALUES(3, 'item'); INSERT INTO "syncObjectTypes" VALUES(3, 'item');
INSERT INTO "syncObjectTypes" VALUES(4, 'search'); INSERT INTO "syncObjectTypes" VALUES(4, 'search');
INSERT INTO "syncObjectTypes" VALUES(5, 'tag');

View File

@ -71,9 +71,11 @@ CREATE INDEX itemAttachments_mimeType ON itemAttachments(mimeType);
-- Individual entries for each tag -- Individual entries for each tag
CREATE TABLE tags ( CREATE TABLE tags (
tagID INTEGER PRIMARY KEY, tagID INTEGER PRIMARY KEY,
tag TEXT, name TEXT,
tagType INT, type INT,
UNIQUE (tag, tagType) dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,
key TEXT NOT NULL UNIQUE,
UNIQUE (name, type)
); );
-- Associates items with keywords -- Associates items with keywords