- 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">
<binding id="tags-box">
<implementation>
<field name="itemRef"/>
<property name="item" onget="return this.itemRef;">
<field name="clickHandler"/>
<!-- 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;
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.itemRef = val;
this._item = val;
this.reload();
]]>
</setter>
</property>
<property name="count"/>
<property name="summary">
<getter>
<![CDATA[
@ -50,7 +83,7 @@
{
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);
}
@ -60,10 +93,13 @@
]]>
</getter>
</property>
<method name="reload">
<body>
<![CDATA[
//Zotero.debug('Reloading tags');
Zotero.debug('Reloading tags');
var rows = this.id('tagRows');
while(rows.hasChildNodes())
@ -88,6 +124,8 @@
]]>
</body>
</method>
<method name="addDynamicRow">
<parameter name="tagObj"/>
<parameter name="tabindex"/>
@ -95,11 +133,11 @@
<![CDATA[
if (tagObj) {
var tagID = tagObj.id;
var tag = tagObj.tag;
var name = tagObj.name;
var type = tagObj.type;
}
if (!tag) {
tag = '';
if (!name) {
name = '';
}
if (!tabindex)
@ -128,7 +166,7 @@
// DEBUG: Why won't just this.nextSibling.blur() work?
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");
remove.setAttribute('value','-');
@ -159,6 +197,284 @@
]]>
</body>
</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">
<body>
<![CDATA[
@ -167,18 +483,21 @@
]]>
</body>
</method>
<method name="add">
<parameter name="value"/>
<body>
<![CDATA[
if (value)
{
if (value) {
return this.item.addTag(value);
}
return false;
]]>
</body>
</method>
<method name="replace">
<parameter name="oldTagID"/>
<parameter name="newTag"/>
@ -196,6 +515,8 @@
]]>
</body>
</method>
<method name="remove">
<parameter name="id"/>
<body>
@ -204,6 +525,8 @@
]]>
</body>
</method>
<method name="updateCount">
<parameter name="count"/>
<body>
@ -235,14 +558,8 @@
]]>
</body>
</method>
<method name="id">
<parameter name="id"/>
<body>
<![CDATA[
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
]]>
</body>
</method>
<method name="fixPopup">
<body>
<![CDATA[
@ -262,6 +579,8 @@
]]>
</body>
</method>
<method name="closePopup">
<body>
<![CDATA[
@ -271,10 +590,78 @@
]]>
</body>
</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>
<![CDATA[
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];
return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
]]>
</body>
</method>

View File

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

View File

@ -120,16 +120,15 @@ var ZoteroItemPane = new function() {
// Info pane
if (index == 0) {
var itembox = document.getElementById('zotero-editpane-item-box');
// Hack to allow read-only mode in right pane -- probably a better
// way to allow access to this
if (mode) {
itembox.mode = mode;
_itemBox.mode = mode;
}
else {
itembox.mode = 'edit';
_itemBox.mode = 'edit';
}
itembox.item = _itemBeingEdited;
_itemBox.item = _itemBeingEdited;
}
// Notes pane
@ -147,7 +146,9 @@ var ZoteroItemPane = new function() {
icon.setAttribute('src','chrome://zotero/skin/treeitem-note.png');
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('crop','end');
@ -236,6 +237,13 @@ var ZoteroItemPane = new function() {
// Tags pane
else if(index == 3)
{
if (mode) {
_tagsBox.mode = mode;
}
else {
_tagsBox.mode = 'edit';
}
var focusMode = 'tags';
var focusBox = _tagsBox;
_tagsBox.item = _itemBeingEdited;
@ -269,27 +277,6 @@ var ZoteroItemPane = new function() {
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()
{
var c = _notesList.childNodes.length;

View File

@ -28,45 +28,49 @@
<overlay
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="include.js"/>
<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"/>
<button label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote();"/>
</hbox>
<grid flex="1">
<columns>
<column flex="1"/>
<column/>
</columns>
<rows id="zotero-editpane-dynamic-notes" flex="1"/>
</grid>
</vbox>
<vbox flex="1">
<hbox align="center">
<label id="zotero-editpane-attachments-label"/>
<button id="zotero-tb-item-attachments-add" type="menu" label="&zotero.item.add;">
<menupopup>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-link" label="&zotero.toolbar.attachment.linked;" oncommand="ZoteroItemPane.addAttachmentFromDialog(true);"/>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-file" label="&zotero.toolbar.attachment.add;" oncommand="ZoteroItemPane.addAttachmentFromDialog();"/>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-web-link" label="&zotero.toolbar.attachment.weblink;" oncommand="ZoteroItemPane.addAttachmentFromPage(true);"/>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-snapshot" label="&zotero.toolbar.attachment.snapshot;" oncommand="ZoteroItemPane.addAttachmentFromPage();"/>
</menupopup>
</button>
</hbox>
<grid flex="1">
<columns>
<column flex="1"/>
<column/>
</columns>
<rows id="zotero-editpane-dynamic-attachments" flex="1"/>
</grid>
</vbox>
<tagsbox id="zotero-editpane-tags" flex="1"/>
<seealsobox id="zotero-editpane-related" flex="1"/>
<vbox flex="1">
<hbox align="center">
<label id="zotero-editpane-notes-label"/>
<button label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote();"/>
</hbox>
<grid flex="1">
<columns>
<column flex="1"/>
<column/>
</columns>
<rows id="zotero-editpane-dynamic-notes" flex="1"/>
</grid>
</vbox>
<vbox flex="1">
<hbox align="center">
<label id="zotero-editpane-attachments-label"/>
<button id="zotero-tb-item-attachments-add" type="menu" label="&zotero.item.add;">
<menupopup>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-link" label="&zotero.toolbar.attachment.linked;" oncommand="ZoteroItemPane.addAttachmentFromDialog(true);"/>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-file" label="&zotero.toolbar.attachment.add;" oncommand="ZoteroItemPane.addAttachmentFromDialog();"/>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-web-link" label="&zotero.toolbar.attachment.weblink;" oncommand="ZoteroItemPane.addAttachmentFromPage(true);"/>
<menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-snapshot" label="&zotero.toolbar.attachment.snapshot;" oncommand="ZoteroItemPane.addAttachmentFromPage();"/>
</menupopup>
</button>
</hbox>
<grid flex="1">
<columns>
<column flex="1"/>
<column/>
</columns>
<rows id="zotero-editpane-dynamic-attachments" flex="1"/>
</grid>
</vbox>
<tagsbox id="zotero-editpane-tags" flex="1"/>
<seealsobox id="zotero-editpane-related" flex="1"/>
</deck>
</overlay>

View File

@ -785,8 +785,7 @@ var ZoteroPane = new function()
{
var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex);
if(item.ref.isNote())
{
if(item.ref.isNote()) {
var noteEditor = document.getElementById('zotero-note-editor');
if (this.itemsView.readOnly) {
noteEditor.mode = 'view';
@ -817,8 +816,8 @@ var ZoteroPane = new function()
}
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
// version of createValueElement() from itemPane.js
// (ideally in an XBL binding)
@ -956,6 +955,8 @@ var ZoteroPane = new function()
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
}
// Regular item
else
{
ZoteroItemPane.viewItem(item.ref, this.itemsView.readOnly ? 'view' : false);

View File

@ -551,11 +551,14 @@ Zotero.Collection.prototype.addItems = function(itemIDs) {
* 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;
var childItems = this.getChildItems(true);
if (childItems) {
var index = childItems.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();

View File

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

View File

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

View File

@ -2364,12 +2364,12 @@ Zotero.Item.prototype.getBestSnapshot = function() {
//
// save() is not required for tag functions
//
Zotero.Item.prototype.addTag = function(tag, type) {
Zotero.Item.prototype.addTag = function(name, type) {
if (!this.id) {
throw ('Cannot add tag to unsaved item in Item.addTag()');
}
if (!tag) {
if (!name) {
Zotero.debug('Not saving empty tag in Item.addTag()', 2);
return false;
}
@ -2378,18 +2378,13 @@ Zotero.Item.prototype.addTag = function(tag, 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);
var existingTypes = Zotero.Tags.getTypes(name);
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));
this.removeTag(Zotero.Tags.getID(name, 1));
}
// If existing user and adding automatic, skip
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) {
var tagID = Zotero.Tags.add(tag, type);
var tag = new Zotero.Tag;
tag.name = name;
tag.type = type;
var tagID = tag.save();
}
try {
@ -2433,38 +2432,20 @@ Zotero.Item.prototype.addTags = function (tags, type) {
Zotero.Item.prototype.addTagByID = function(tagID) {
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) {
Zotero.debug('Not saving nonexistent tag in Item.addTagByID()', 2);
return false;
throw ('tagID not provided in Zotero.Item.addTagByID()');
}
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()');
var tag = Zotero.Tags.get(tagID);
if (!tag) {
throw ('Cannot add invalid tag ' + tagID + ' in Zotero.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;
tag.addItem(this.id);
tag.save();
}
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));
}
/**
* Returns all tags assigned to an item
*
* @return array Array of Zotero.Tag objects
*/
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);
var sql = "SELECT tagID, name FROM tags WHERE tagID IN "
+ "(SELECT tagID FROM itemTags WHERE itemID=?)";
var tags = Zotero.DB.query(sql, this.id);
if (!tags) {
return false;
}
var collation = Zotero.getLocaleCollation();
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() {
var sql = "SELECT tagID FROM itemTags WHERE itemID=" + this.id;
return Zotero.DB.columnQuery(sql);
var sql = "SELECT tagID FROM itemTags WHERE itemID=?";
return Zotero.DB.columnQuery(sql, this.id);
}
Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) {
@ -2537,16 +2528,20 @@ Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) {
Zotero.Item.prototype.removeTag = function(tagID) {
if (!this.id) {
throw ('Cannot remove tag on unsaved item');
throw ('Cannot remove tag on unsaved item in Zotero.Item.removeTag()');
}
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);
if (!tagID) {
throw ('tagID not provided in Zotero.Item.removeTag()');
}
var tag = Zotero.Tags.get(tagID);
if (!tag) {
throw ('Cannot remove invalid tag ' + tagID + ' in Zotero.Item.removeTag()');
}
tag.removeItem(this.id);
tag.save();
}
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();
if (!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();
if (!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.getIDs = getIDs;
this.getTypes = getTypes;
this.getUpdated = getUpdated;
this.getAll = getAll;
this.getAllWithinSearch = getAllWithinSearch;
this.getTagItems = getTagItems;
this.search = search;
this.add = add;
this.rename = rename;
this.remove = remove;
this.reload = reload;
this.erase = erase;
this.purge = purge;
this.toArray = toArray;
this.unload = unload;
/*
* Returns a tag and type for a given tagID
*/
function get(tagID) {
function get(tagID, skipCheck) {
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;
if (!skipCheck) {
var sql = 'SELECT COUNT(*) FROM tags WHERE tagID=?';
var result = Zotero.DB.valueQuery(sql, tagID);
if (!result) {
return false;
}
}
_tagsByID[tagID] = {
tag: result.tag,
type: result.tagType
};
return result;
_tagsByID[tagID] = new Zotero.Tag(tagID);
return _tagsByID[tagID];
}
@ -72,31 +72,31 @@ Zotero.Tags = new function() {
*/
function getName(tagID) {
if (_tagsByID[tagID]) {
return _tagsByID[tagID].tag;
return _tagsByID[tagID].name;
}
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
*/
function getID(tag, type) {
if (_tags[type] && _tags[type]['_' + tag]) {
return _tags[type]['_' + tag];
function getID(name, type) {
if (_tags[type] && _tags[type]['_' + name]) {
return _tags[type]['_' + name];
}
var sql = 'SELECT tagID FROM tags WHERE tag=? AND tagType=?';
var tagID = Zotero.DB.valueQuery(sql, [tag, type]);
var sql = 'SELECT tagID FROM tags WHERE name=? AND type=?';
var tagID = Zotero.DB.valueQuery(sql, [name, type]);
if (tagID) {
if (!_tags[type]) {
_tags[type] = [];
}
_tags[type]['_' + tag] = tagID;
_tags[type]['_' + name] = tagID;
}
return tagID;
@ -106,30 +106,40 @@ Zotero.Tags = new function() {
/*
* 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]);
function getIDs(name) {
var sql = 'SELECT tagID FROM tags WHERE name=?';
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) {
var sql = 'SELECT tagType FROM tags WHERE tag=?';
return Zotero.DB.columnQuery(sql, [tag]);
function getTypes(name) {
var sql = 'SELECT type FROM tags WHERE name=?';
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
*
* _types_ is an optional array of tagTypes to fetch
* _types_ is an optional array of tag types to fetch
*/
function getAll(types) {
var sql = "SELECT tagID, tag, tagType FROM tags ";
var sql = "SELECT tagID, name FROM tags ";
if (types) {
sql += "WHERE tagType IN (" + types.join() + ") ";
sql += "WHERE type IN (" + types.join() + ") ";
}
var tags = Zotero.DB.query(sql);
if (!tags) {
@ -138,15 +148,13 @@ Zotero.Tags = new function() {
var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) {
return collation.compareString(1, a.tag, b.tag);
return collation.compareString(1, a.name, b.name);
});
var indexed = {};
for (var i=0; i<tags.length; i++) {
indexed[tags[i].tagID] = {
tag: tags[i].tag,
type: tags[i].tagType
};
var tag = this.get(tags[i].tagID, true);
indexed[tags[i].tagID] = tag;
}
return indexed;
}
@ -155,7 +163,7 @@ Zotero.Tags = new function() {
/*
* 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) {
// Save search results to temporary table
@ -176,11 +184,11 @@ Zotero.Tags = new function() {
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 "
+ "(SELECT itemID FROM " + tmpTable + ") ";
if (types) {
sql += "AND tagType IN (" + types.join() + ") ";
sql += "AND type IN (" + types.join() + ") ";
}
var tags = Zotero.DB.query(sql);
@ -192,15 +200,13 @@ Zotero.Tags = new function() {
var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) {
return collation.compareString(1, a.tag, b.tag);
return collation.compareString(1, a.name, b.name);
});
var indexed = {};
for (var i=0; i<tags.length; i++) {
indexed[tags[i].tagID] = {
tag: tags[i].tag,
type: tags[i].tagType
};
var tag = this.get(tags[i].tagID, true);
indexed[tags[i].tagID] = tag;
}
return indexed;
}
@ -213,76 +219,49 @@ Zotero.Tags = new function() {
function search(str) {
var sql = 'SELECT tagID, tag, tagType FROM tags';
var sql = 'SELECT tagID, name, type FROM tags';
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);
if (!tags) {
return {};
}
var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) {
return collation.compareString(1, a.name, b.name);
});
var indexed = {};
for each(var tag in tags) {
indexed[tag.tagID] = {
tag: tag.tag,
type: tag.tagType
};
for (var i=0; i<tags.length; i++) {
var tag = this.get(tags[i].tagID, true);
indexed[tags[i].tagID] = tag;
}
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) {
function rename(tagID, name) {
Zotero.debug('Renaming tag', 4);
Zotero.DB.beginTransaction();
var tagObj = this.get(tagID);
var oldName = tagObj.tag;
var oldName = tagObj.name;
var oldType = tagObj.type;
var notifierData = {};
notifierData[this.id] = { old: this.toArray() };
notifierData[tagID] = { old: tag.serialize() };
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);
}
if (oldName == name) {
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);
var sql = "SELECT tagID FROM tags WHERE name=? AND type=0";
var existingTagID = Zotero.DB.valueQuery(sql, name);
if (existingTagID) {
var itemIDs = this.getTagItems(tagID);
var existingItemIDs = this.getTagItems(existingTagID);
@ -316,54 +295,44 @@ Zotero.Tags = new function() {
}
}
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();
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];
tagObj.name = name;
// Set all renamed tags to manual
tagObj.type = 0;
tagObj.save();
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();
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
var itemIDs = Zotero.DB.columnQuery(sql, tagID);
if (!itemIDs) {
Zotero.DB.commitTransaction();
return;
for each(var id in ids) {
var tag = this.get(id);
if (tag) {
erasedTags[id] = tag.serialize();
tag.erase();
}
}
var sql = "DELETE FROM itemTags WHERE tagID=?";
Zotero.DB.query(sql, tagID);
this.unload(ids);
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;
}
@ -373,47 +342,72 @@ Zotero.Tags = new function() {
* 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) }
Zotero.UnresponsiveScriptIndicator.disable();
try {
Zotero.DB.beginTransaction();
purged.push(tag.tagID);
if (_tags[tag.tagType]) {
delete _tags[tag.tagType]['_' + tag.tag];
var sql = "CREATE TEMPORARY TABLE tagDelete AS "
+ "SELECT tagID FROM tags WHERE tagID "
+ "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) {
Zotero.DB.rollbackTransaction();
return;
}
delete _tagsByID[tag.tagID];
var notifierData = {};
for each(var tagID in toDelete) {
var tag = Zotero.Tags.get(tagID);
Zotero.debug(tag);
notifierData[tagID] = { old: tag.serialize() }
}
this.unload(toDelete);
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.Notifier.trigger('delete', 'tag', toDelete, notifierData);
}
catch (e) {
Zotero.DB.rollbackTransaction();
throw (e);
}
finally {
Zotero.UnresponsiveScriptIndicator.enable();
}
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;
/**
* Unload tags from caches
*
* @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 'collections':
case 'savedSearches':
case 'tags':
var id = _getNextAvailable(table, skip);
if (!id && notNull) {
return _getNext(table, skip);
@ -57,7 +58,6 @@ Zotero.ID = new function () {
//
// 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

View File

@ -1447,6 +1447,27 @@ Zotero.Schema = new function(){
}
}
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
},
table: 'itemTags',
field: 'tag'
field: 'name'
},
{

View File

@ -27,7 +27,12 @@ Zotero.Sync = new function() {
search: {
singular: 'Search',
plural: 'Searches'
},
tag: {
singular: 'Tag',
plural: 'Tags'
}
};
});
@ -1068,6 +1073,8 @@ Zotero.Sync.Server.Data = new function() {
this.xmlToCreator = xmlToCreator;
this.searchToXML = searchToXML;
this.xmlToSearch = xmlToSearch;
this.tagToXML = tagToXML;
this.xmlToTag = xmlToTag;
var _noMergeTypes = ['search'];
@ -1208,7 +1215,7 @@ Zotero.Sync.Server.Data = new function() {
// Update id in local updates array
var index = uploadIDs.updated[types].indexOf(oldID);
if (index == -1) {
_error("Local " + type + " " + oldID + " not in "
throw ("Local " + type + " " + oldID + " not in "
+ "update array when changing id");
}
uploadIDs.updated[types][index] = newID;
@ -1256,7 +1263,7 @@ Zotero.Sync.Server.Data = new function() {
if (type != 'item') {
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);
@ -1653,7 +1660,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
else if (skipPrimary) {
_error("Cannot use skipPrimary with existing item in "
throw ("Cannot use skipPrimary with existing item in "
+ "Zotero.Sync.Server.Data.xmlToItem()");
}
@ -1699,7 +1706,7 @@ Zotero.Sync.Server.Data = new function() {
for each(var creator in xmlItem.creator) {
var pos = parseInt(creator.@index);
if (pos != i) {
_error('No creator in position ' + i);
throw ('No creator in position ' + i);
}
item.setCreator(
@ -1799,7 +1806,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
else if (skipPrimary) {
_error("Cannot use skipPrimary with existing collection in "
throw ("Cannot use skipPrimary with existing collection in "
+ "Zotero.Sync.Server.Data.xmlToCollection()");
}
@ -1877,7 +1884,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
else if (skipPrimary) {
_error("Cannot use skipPrimary with existing creator in "
throw ("Cannot use skipPrimary with existing creator in "
+ "Zotero.Sync.Server.Data.xmlToCreator()");
}
@ -1960,7 +1967,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
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()");
}
@ -2010,4 +2017,63 @@ Zotero.Sync.Server.Data = new function() {
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;
case 'tag':
var sql = "SELECT tag FROM tags WHERE tag LIKE ?";
var sql = "SELECT name FROM tags WHERE name LIKE ?";
var sqlParams = [searchString + '%'];
if (extra){
sql += " AND tagID NOT IN (SELECT tagID FROM itemTags WHERE "
+ "itemID = ?)";
sqlParams.push(extra);
}
sql += " ORDER BY tag";
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;
case 'creator':

View File

@ -14,13 +14,14 @@ var ZoteroWrapped = this;
* Include the core objects to be stored within XPCOM
*********************************************************************/
var xpcomFiles = [ 'zotero',
var xpcomFiles = ['zotero',
'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView',
'dataServer', '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', 'zeroconf'];
'dataServer', 'data_access', 'data/item', 'data/items', 'data/collection',
'data/collections', 'data/cachedTypes', 'data/creator', 'data/creators',
'data/itemFields', 'data/notes', 'data/tag', 'data/tags', 'db', 'file',
'fulltext', 'id', 'ingester', 'integration', 'itemTreeView', 'mime',
'notifier', 'progressWindow', 'quickCopy', 'report', 'schema', 'search',
'sync', 'timeline', 'translate', 'utilities', 'zeroconf'];
for (var i=0; i<xpcomFiles.length; i++) {
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(3, 'item');
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
CREATE TABLE tags (
tagID INTEGER PRIMARY KEY,
tag TEXT,
tagType INT,
UNIQUE (tag, tagType)
name TEXT,
type INT,
dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,
key TEXT NOT NULL UNIQUE,
UNIQUE (name, type)
);
-- Associates items with keywords