Tags overhaul [DB reupgrade]

- Simplified schema
- Tags are now added without reloading entire tag selector
  - On my system, adding 400 tags to an item (separately, with the tag
    selector updating each time) went from 59 seconds to 42. (Given that
    it takes only 13 seconds with the tag selector closed, though,
    there's clearly more work to be done.)
- Tag selector now uses HTML flexbox (in identical fashion, for now, but
  with the possibility of fancier changes later, and with streamlined
  logic thanks to the flexbox 'order' property)
- Various async fixes
- Tests
This commit is contained in:
Dan Stillman 2015-06-23 04:35:45 -04:00
parent b602cc4bd2
commit 33dedd1753
24 changed files with 951 additions and 689 deletions

View file

@ -117,7 +117,9 @@
this.mode = this.getAttribute('mode'); this.mode = this.getAttribute('mode');
} }
this._notifierID = Zotero.Notifier.registerObserver(this, ['item-tag', 'setting']); this._notifierID = Zotero.Notifier.registerObserver(
this, ['item-tag', 'setting'], 'tagsbox'
);
]]> ]]>
</constructor> </constructor>
@ -134,22 +136,23 @@
<parameter name="type"/> <parameter name="type"/>
<parameter name="ids"/> <parameter name="ids"/>
<parameter name="extraData"/> <parameter name="extraData"/>
<body> <body><![CDATA[
<![CDATA[ return Zotero.spawn(function* () {
if (type == 'setting') { if (type == 'setting') {
if (ids.some(function (val) val.split("/")[1] == 'tagColors') && this.item) { if (ids.some(function (val) val.split("/")[1] == 'tagColors') && this.item) {
this.reload(); return this.reload();
} }
return;
} }
else if (type == 'item-tag') { else if (type == 'item-tag') {
let itemID, tagName; let itemID, tagID;
for (let i=0; i<ids.length; i++) { for (let i=0; i<ids.length; i++) {
[itemID, tagName] = ids[i].match(/^([0-9]+)-(.+)/).slice(1); [itemID, tagID] = ids[i].split('-').map(x => parseInt(x));
if (!this.item || itemID != this.item.id) { if (!this.item || itemID != this.item.id) {
continue; continue;
} }
let data = extraData[ids[i]];
let tagName = data.tag;
if (event == 'add') { if (event == 'add') {
var newTabIndex = this.add(tagName); var newTabIndex = this.add(tagName);
@ -168,9 +171,7 @@
} }
} }
else if (event == 'modify') { else if (event == 'modify') {
Zotero.debug("EXTRA"); let oldTagName = data.old.tag;
Zotero.debug(extraData);
let oldTagName = extraData[tagName].old.tag;
this.remove(oldTagName); this.remove(oldTagName);
this.add(tagName); this.add(tagName);
} }
@ -196,11 +197,11 @@
} }
else if (type == 'tag') { else if (type == 'tag') {
if (event == 'modify') { if (event == 'modify') {
this.reload(); return this.reload();
} }
} }
]]> }.bind(this));
</body> ]]></body>
</method> </method>
@ -235,6 +236,9 @@
this._reloading = false; this._reloading = false;
this._focusField(); this._focusField();
var event = new Event('refresh');
this.dispatchEvent(event);
}, this); }, this);
]]></body> ]]></body>
</method> </method>
@ -388,13 +392,10 @@
} }
// Tag color // Tag color
let color = this._tagColors[valueText]; var colorData = this._tagColors.get(valueText);
if (color) { if (colorData) {
valueElement.setAttribute( valueElement.style.color = colorData.color;
'style', valueElement.style.fontWeight = 'bold';
'color:' + this._tagColors[valueText].color + '; '
+ 'font-weight: bold'
);
} }
return valueElement; return valueElement;
@ -522,12 +523,10 @@
yield this.blurHandler(target); yield this.blurHandler(target);
if (focusField) { if (focusField) {
Zotero.debug("FOCUSING FIELD");
this._focusField(); this._focusField();
} }
// Return focus to items pane // Return focus to items pane
else { else {
Zotero.debug("FOCUSING ITEM PANE");
var tree = document.getElementById('zotero-items-tree'); var tree = document.getElementById('zotero-items-tree');
if (tree) { if (tree) {
tree.focus(); tree.focus();
@ -757,8 +756,6 @@
<method name="add"> <method name="add">
<parameter name="tagName"/> <parameter name="tagName"/>
<body><![CDATA[ <body><![CDATA[
Zotero.debug("ADDING ROW WITH " + tagName);
var rowsElement = this.id('tagRows'); var rowsElement = this.id('tagRows');
var rows = rowsElement.childNodes; var rows = rowsElement.childNodes;
@ -766,7 +763,6 @@
var row = false; var row = false;
for (let i=0; i<rows.length; i++) { for (let i=0; i<rows.length; i++) {
if (rows[i].getAttribute('tagName') === tagName) { if (rows[i].getAttribute('tagName') === tagName) {
Zotero.debug("FOUND ROW with " + tagName);
return rows[i].getAttribute('ztabindex'); return rows[i].getAttribute('ztabindex');
} }
} }
@ -835,18 +831,12 @@
<method name="remove"> <method name="remove">
<parameter name="tagName"/> <parameter name="tagName"/>
<body><![CDATA[ <body><![CDATA[
Zotero.debug("REMOVING ROW WITH " + tagName);
var rowsElement = this.id('tagRows'); var rowsElement = this.id('tagRows');
var rows = rowsElement.childNodes; var rows = rowsElement.childNodes;
var removed = false; var removed = false;
var oldTabIndex = -1; var oldTabIndex = -1;
for (var i=0; i<rows.length; i++) { for (var i=0; i<rows.length; i++) {
let value = rows[i].getAttribute('tagName'); let value = rows[i].getAttribute('tagName');
Zotero.debug("-=-=");
Zotero.debug(value);
Zotero.debug(tagName);
Zotero.debug(value === tagName);
if (value === tagName) { if (value === tagName) {
oldTabIndex = i + 1; oldTabIndex = i + 1;
removed = true; removed = true;
@ -1010,9 +1000,7 @@
<method name="scrollToTop"> <method name="scrollToTop">
<body> <body>
<![CDATA[ <![CDATA[
Zotero.debug('SCROLL TO TOP');
if (!this._activeScrollbox) { if (!this._activeScrollbox) {
Zotero.debug('NO');
return; return;
} }
var sbo = this._activeScrollbox.boxObject; var sbo = this._activeScrollbox.boxObject;

View file

@ -44,8 +44,8 @@
<field name="_initialized">false</field> <field name="_initialized">false</field>
<field name="_notifierID">false</field> <field name="_notifierID">false</field>
<field name="_tags">null</field> <field name="_tags">null</field>
<field name="_popupNode"/>
<field name="_dirty">null</field> <field name="_dirty">null</field>
<field name="_emptyColored">null</field>
<field name="_emptyRegular">null</field> <field name="_emptyRegular">null</field>
<!-- Modes are predefined settings groups for particular tasks --> <!-- Modes are predefined settings groups for particular tasks -->
@ -173,7 +173,7 @@
<body> <body>
<![CDATA[ <![CDATA[
this._initialized = true; this._initialized = true;
this.selection = {}; this.selection = new Set();
this._notifierID = Zotero.Notifier.registerObserver( this._notifierID = Zotero.Notifier.registerObserver(
this, this,
['collection-item', 'item', 'item-tag', 'tag', 'setting'], ['collection-item', 'item', 'item-tag', 'tag', 'setting'],
@ -193,7 +193,7 @@
this._initialized = false; this._initialized = false;
this.unregister(); this.unregister();
this.selection = {}; this.selection = new Set();
if (this.onchange) { if (this.onchange) {
this.onchange(); this.onchange();
} }
@ -217,10 +217,16 @@
<parameter name="fetch"/> <parameter name="fetch"/>
<body> <body>
<![CDATA[ <![CDATA[
Zotero.spawn(function* () { return Zotero.spawn(function* () {
Zotero.debug('Refreshing tags selector');
var t = new Date; var t = new Date;
if (fetch || this._dirty) {
Zotero.debug('Reloading tags selector');
}
else {
Zotero.debug('Refreshing tags selector');
}
if (!this._initialized) { if (!this._initialized) {
this.init(); this.init();
fetch = true; fetch = true;
@ -228,49 +234,26 @@
var emptyColored = true; var emptyColored = true;
var emptyRegular = true; var emptyRegular = true;
var tagsToggleBox = this.id('tags-toggle'); var tagsBox = this.id('tags-box');
var tagColors = yield Zotero.Tags.getColors(this.libraryID) var tagColors = yield Zotero.Tags.getColors(this.libraryID)
.tap(() => Zotero.Promise.check(this.mode)); .tap(() => Zotero.Promise.check(this.mode));
// If new data, rebuild boxes
if (fetch || this._dirty) { if (fetch || this._dirty) {
this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types) this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types)
.tap(() => Zotero.Promise.check(this.mode)); .tap(() => Zotero.Promise.check(this.mode));
tagsBox.textContent = "";
// Remove children
tagsToggleBox.textContent = "";
// Sort by name // Sort by name
var collation = Zotero.getLocaleCollation(); let collation = Zotero.getLocaleCollation();
var orderedTags = this._tags.concat(); this._tags.sort(function (a, b) {
orderedTags.sort(function(a, b) {
return collation.compareString(1, a.tag, b.tag); return collation.compareString(1, a.tag, b.tag);
}); });
var tagColorsLowerCase = {}; let lastTag;
var colorTags = []; for (let i = 0; i < this._tags.length; i++) {
for (let name in tagColors) { let tagData = this._tags[i];
colorTags[tagColors[name].position] = name;
tagColorsLowerCase[name.toLowerCase()] = true;
}
var positions = Object.keys(colorTags);
for (let i=positions.length-1; i>=0; i--) {
let name = colorTags[positions[i]];
orderedTags.unshift({
tag: name,
type: 0,
hasColor: true
});
}
var lastTag;
for (let i=0; i<orderedTags.length; i++) {
let tagData = orderedTags[i];
// Skip colored tags in the regular section,
// since we add them to the beginning above
if (!tagData.hasColor && tagColorsLowerCase[tagData.tag.toLowerCase()]) {
continue;
}
// Only show tags of different types once // Only show tags of different types once
if (tagData.tag === lastTag) { if (tagData.tag === lastTag) {
@ -278,108 +261,35 @@
} }
lastTag = tagData.tag; lastTag = tagData.tag;
let tagButton = this._makeClickableTag(tagData, this.editable); let elem = this._insertClickableTag(tagsBox, tagData);
if (tagButton) { let visible = this._updateClickableTag(
var self = this; elem, tagData.tag, tagColors
tagButton.addEventListener('click', function(event) { );
self.handleTagClick(event, this); if (visible) {
}); emptyRegular = false;
if (this.editable) {
tagButton.addEventListener('dragover', this.dragObserver.onDragOver);
tagButton.addEventListener('dragexit', this.dragObserver.onDragExit);
tagButton.addEventListener('drop', this.dragObserver.onDrop, true);
}
tagsToggleBox.appendChild(tagButton);
} }
} }
this._dirty = false; this._dirty = false;
} }
// Otherwise just update based on visibility
// Set attributes else {
var colorTags = {}; elems = tagsBox.childNodes;
var labels = tagsToggleBox.getElementsByTagName('label'); for (let i = 0; i < elems.length; i++) {
for (let i=0; i<labels.length; i++) { let elem = elems[i];
let name = labels[i].value; let visible = this._updateClickableTag(
let lcname = name.toLowerCase(); elem, elem.textContent, tagColors
let colorData = tagColors[name];
if (colorData) {
labels[i].setAttribute(
'style', 'color:' + colorData.color + '; ' + 'font-weight: bold'
); );
} if (visible) {
else {
labels[i].removeAttribute('style');
}
// Restore selection
if (this.selection[name]){
labels[i].setAttribute('selected', 'true');
}
else {
labels[i].setAttribute('selected', 'false');
}
// Check tags against search
if (this._search) {
var inSearch = lcname.indexOf(this._search) != -1;
}
// Check tags against scope
if (this._hasScope) {
var inScope = !!this._scope[name];
}
// If not in search, hide
if (this._search && !inSearch) {
labels[i].setAttribute('hidden', true);
}
else if (this.filterToScope) {
if (this._hasScope && inScope) {
labels[i].className = 'zotero-clicky';
labels[i].setAttribute('inScope', true);
labels[i].setAttribute('hidden', false);
emptyRegular = false; emptyRegular = false;
} }
else {
labels[i].className = '';
labels[i].setAttribute('hidden', true);
labels[i].setAttribute('inScope', false);
}
}
// Display all
else {
if (this._hasScope && inScope) {
labels[i].className = 'zotero-clicky';
labels[i].setAttribute('inScope', true);
}
else {
labels[i].className = '';
labels[i].setAttribute('inScope', false);
}
labels[i].setAttribute('hidden', false);
emptyRegular = false;
}
// Always show colored tags at top, unless they
// don't match an active tag search
if (colorData && (!this._search || inSearch)) {
labels[i].setAttribute('hidden', false);
labels[i].setAttribute('hasColor', true);
emptyColored = false;
}
else {
labels[i].removeAttribute('hasColor');
} }
} }
//start tag cloud code //start tag cloud code
var tagCloud = Zotero.Prefs.get('tagCloud'); var tagCloud = Zotero.Prefs.get('tagCloud');
if (false && tagCloud) {
if(tagCloud) { var labels = tagsBox.getElementsByTagName('label');
var labels = tagsToggleBox.getElementsByTagName('label');
//loop through displayed labels and find number of linked items //loop through displayed labels and find number of linked items
var numlinked= []; var numlinked= [];
@ -444,20 +354,15 @@
//end tag cloud code //end tag cloud code
this.updateNumSelected(); this.updateNumSelected();
this._emptyColored = emptyColored; var empty = this._emptyRegular = emptyRegular;
this._emptyRegular = emptyRegular; // TODO: Show loading again when switching libraries/collections?
var empty = emptyColored && emptyRegular; this.id('tags-deck').selectedIndex = empty ? 1 : 2;
this.id('tags-toggle').setAttribute('collapsed', empty);
this.id('no-tags-box').setAttribute('collapsed', !empty);
if (this.onRefresh) { if (this.onRefresh) {
this.onRefresh(); this.onRefresh();
this.onRefresh = null; this.onRefresh = null;
} }
// Clear "Loading tags…" after the first load
this.id('no-tags-deck').selectedIndex = 1;
Zotero.debug("Loaded tag selector in " + (new Date - t) + " ms"); Zotero.debug("Loaded tag selector in " + (new Date - t) + " ms");
var event = new Event('refresh'); var event = new Event('refresh');
@ -467,34 +372,56 @@
</body> </body>
</method> </method>
<method name="insertSorted">
<method name="getVisible"> <parameter name="tagObjs"/>
<body><![CDATA[ <body><![CDATA[
var tagsBox = this.id('tags-toggle'); return Zotero.spawn(function* () {
var labels = tagsBox.getElementsByTagName('label'); var tagColors = yield Zotero.Tags.getColors(this._libraryID);
var visible = [];
for (let i = 0; i < labels.length; i++){ var collation = Zotero.getLocaleCollation();
let label = labels[i]; tagObjs.sort(function (a, b) {
if (label.getAttribute('hidden') != 'true' return collation.compareString(1, a.tag, b.tag);
&& label.getAttribute('inScope') == 'true') { });
visible.push(label.value);
// Create tag elements in sorted order
var tagsBox = this.id('tags-box');
var tagElems = tagsBox.childNodes;
var j = 0;
loop:
for (let i = 0; i < tagObjs.length; i++) {
let tagObj = tagObjs[i];
while (j < tagElems.length) {
let elem = tagElems[j];
let comp = collation.compareString(
1, tagObj.tag, elem.textContent
);
// If tag already exists, update type if new one is lower
if (comp == 0) {
let tagType = elem.getAttribute('tagType');
if (parseInt(tagObj.type) < parseInt(tagType)) {
elem.setAttribute('tagType', tagObj.type);
} }
continue loop;
} }
return visible; if (comp < 0) {
break;
}
j++;
}
this._insertClickableTag(tagsBox, tagObj, tagElems[j]);
this._updateClickableTag(
tagElems[j], tagElems[j].textContent, tagColors
);
}
}, this);
]]></body> ]]></body>
</method> </method>
<method name="getNumSelected"> <method name="getNumSelected">
<body> <body><![CDATA[
<![CDATA[ return this.selection.size;
var count = 0; ]]></body>
for (var i in this.selection) {
count++;
}
return count;
]]>
</body>
</method> </method>
@ -525,11 +452,12 @@
<parameter name="event"/> <parameter name="event"/>
<parameter name="type"/> <parameter name="type"/>
<parameter name="ids"/> <parameter name="ids"/>
<parameter name="extraData"/>
<body><![CDATA[ <body><![CDATA[
return Zotero.spawn(function* () { return Zotero.spawn(function* () {
if (type == 'setting') { if (type == 'setting') {
if (ids.some(function (val) val.split("/")[1] == 'tagColors')) { if (ids.some(function (val) val.split("/")[1] == 'tagColors')) {
this.refresh(true); yield this.refresh(true);
} }
return; return;
} }
@ -558,7 +486,7 @@
// TODO: necessary, or just use notifier value? // TODO: necessary, or just use notifier value?
this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types); this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types);
for (var tag in this.selection) { for (let tag of this.selection) {
for each(var tag2 in this._tags) { for each(var tag2 in this._tags) {
if (tag == tag2) { if (tag == tag2) {
var found = true; var found = true;
@ -566,14 +494,32 @@
} }
} }
if (!found) { if (!found) {
delete this.selection[tag]; this.selection.delete(tag);
selectionChanged = true; selectionChanged = true;
} }
} }
} }
// This could be more optimized to insert new/changed tags at the appropriate if (event == 'add') {
// spot if we cared, but we probably don't if (type == 'item-tag') {
let tagObjs = ids
// Get tag name and type
.map(x => extraData[x])
// Ignore tag adds for items not in the current library, if there is one
.filter(function (x) {
if (!this._libraryID) return true;
return x.libraryID == this._libraryID;
}.bind(this));
if (tagObjs.length) {
yield this.insertSorted(tagObjs);
}
}
// Don't add anything for item or collection-item; just update scope
return this.updateScope();
}
var t = this.id('tags-search').inputField; var t = this.id('tags-search').inputField;
if (t.value) { if (t.value) {
this.setSearch(t.value, true); this.setSearch(t.value, true);
@ -610,34 +556,19 @@
</method> </method>
<!-- Not currently used --> <method name="deselectAll">
<method name="selectVisible">
<body>
<![CDATA[
var tagsToggleBox = this.id('tags-toggle');
var labels = tagsToggleBox.getElementsByTagName('label');
for (var i=0; i<labels.length; i++){
if (labels[i].getAttribute('hidden') != 'true'
&& labels[i].getAttribute('inScope') == 'true') {
labels[i].setAttribute('selected', 'true');
this.selection[labels[i].value] = true;
}
}
]]>
</body>
</method>
<method name="clearVisible">
<body><![CDATA[ <body><![CDATA[
var tagsToggleBox = this.id('tags-toggle'); if (!this.selection || !this.selection.size) {
return;
}
var labels = Zotero.Utilities.xpath(tagsToggleBox, 'label[@selected="true"]'); this.selection = new Set();
for (var i=0; i<labels.length; i++){
var label = labels[i]; var elems = this.id('tags-box').querySelectorAll("button[selected=true]");
label.setAttribute('selected', 'false'); for (let i = 0; i < elems.length; i++) {
delete this.selection[label.value]; let elem = elems[i];
elem.setAttribute('selected', false);
this.selection.delete(elem.textContent);
} }
if (this.onchange) { if (this.onchange) {
@ -647,14 +578,6 @@
</method> </method>
<method name="clearAll">
<body><![CDATA[
this.selection = {};
return this.clearVisible();
]]></body>
</method>
<method name="handleKeyPress"> <method name="handleKeyPress">
<parameter name="clear"/> <parameter name="clear"/>
<body> <body>
@ -687,7 +610,7 @@
<method name="handleTagClick"> <method name="handleTagClick">
<parameter name="event"/> <parameter name="event"/>
<parameter name="label"/> <parameter name="elem"/>
<body> <body>
<![CDATA[ <![CDATA[
if (event.button != 0) { if (event.button != 0) {
@ -695,19 +618,19 @@
} }
// Ignore clicks on tags not in scope // Ignore clicks on tags not in scope
if (label.getAttribute('inScope') == 'false') { if (elem.getAttribute('inScope') == 'false') {
return; return;
} }
// Deselect // Deselect
if (label.getAttribute('selected')=='true'){ if (elem.getAttribute('selected')=='true'){
delete this.selection[label.value]; this.selection.delete(elem.textContent);
label.setAttribute('selected', 'false'); elem.setAttribute('selected', 'false');
} }
// Select // Select
else { else {
this.selection[label.value] = true; this.selection.add(elem.textContent);
label.setAttribute('selected', 'true'); elem.setAttribute('selected', 'true');
} }
this.updateNumSelected(); this.updateNumSelected();
@ -723,7 +646,7 @@
<method name="rename"> <method name="rename">
<parameter name="oldName"/> <parameter name="oldName"/>
<body><![CDATA[ <body><![CDATA[
Zotero.spawn(function* () { return Zotero.spawn(function* () {
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService); .getService(Components.interfaces.nsIPromptService);
@ -737,13 +660,12 @@
return; return;
} }
if (this.selection[oldName]) { if (this.selection.has(oldName)) {
var wasSelected = true; var wasSelected = true;
delete this.selection[oldName]; this.selection.delete(oldName);
} }
yield Zotero.Tags.load(this.libraryID); if (yield Zotero.Tags.getID(oldName)) {
if (Zotero.Tags.getID(this.libraryID, oldName)) {
yield Zotero.Tags.rename(this.libraryID, oldName, newName.value); yield Zotero.Tags.rename(this.libraryID, oldName, newName.value);
} }
// Colored tags don't need to exist, so in that case // Colored tags don't need to exist, so in that case
@ -758,7 +680,7 @@
} }
if (wasSelected) { if (wasSelected) {
this.selection[newName.value] = true; this.selection.add(newName.value);
} }
}.bind(this)); }.bind(this));
]]> ]]>
@ -768,8 +690,8 @@
<method name="delete"> <method name="delete">
<parameter name="name"/> <parameter name="name"/>
<body> <body><![CDATA[
<![CDATA[ return Zotero.spawn(function* () {
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService); .getService(Components.interfaces.nsIPromptService);
@ -781,81 +703,190 @@
return; return;
} }
return Zotero.DB.executeTransaction(function* () { var tagID = yield Zotero.Tags.getID(name);
yield Zotero.Tags.load(this.libraryID);
var tagID = Zotero.Tags.getID(this.libraryID, name);
if (tagID) { if (tagID) {
yield Zotero.Tags.erase(this.libraryID, tagID); yield Zotero.Tags.removeFromLibrary(this.libraryID, tagID);
}
// If only a tag color setting, remove that
else {
yield Zotero.Tags.setColor(this.libraryID, name, false);
} }
}.bind(this)); }.bind(this));
]]></body>
// If only a tag color setting, remove that
if (!tagID) {
Zotero.Tags.setColor(this.libraryID, name, false);
}
]]>
</body>
</method> </method>
<method name="getColor"> <method name="getColor">
<parameter name="tagIDs"/> <parameter name="tagIDs"/>
<body> <body><![CDATA[
<![CDATA[ return Zotero.spawn(function* () {
tagIDs = tagIDs.split('-'); tagIDs = tagIDs.split('-');
var name = Zotero.Tags.getName(this.libraryID, tagIDs[0]); var name = yield Zotero.Tags.getName(tagIDs[0]);
return Zotero.Tags.getColor(this.libraryID, name) var colorData = yield Zotero.Tags.getColor(this.libraryID, name);
.then(function (colorData) {
return colorData ? colorData.color : '#000000'; return colorData ? colorData.color : '#000000';
}); }.bind(this));
]]> ]]></body>
</body> </method>
<method name="_insertClickableTag">
<parameter name="tagsBox"/>
<parameter name="tagData"/>
<parameter name="insertBefore"/>
<body><![CDATA[
var button = this._makeClickableTag(tagData, this.editable);
if (insertBefore === undefined) {
tagsBox.appendChild(button);
}
else {
tagsBox.insertBefore(button, insertBefore);
}
return button;
]]></body>
</method> </method>
<method name="_makeClickableTag"> <method name="_makeClickableTag">
<parameter name="tagObj"/> <parameter name="tagObj"/>
<parameter name="editable"/> <parameter name="editable"/>
<body> <body><![CDATA[
<![CDATA[ var elem = document.createElementNS('http://www.w3.org/1999/xhtml', 'button');
var tagName = tagObj.tag; elem.textContent = tagObj.tag;
var tagType = tagObj.type; if (tagObj.type) {
elem.setAttribute('tagType', tagObj.type);
var label = document.createElement('label');
label.setAttribute('value', tagName);
label.setAttribute('tagType', tagType);
if (editable) {
label.setAttribute('context', 'tag-menu');
} }
return label; var self = this;
]]> elem.addEventListener('click', function(event) {
</body> self.handleTagClick(event, this);
});
if (this.editable) {
elem.addEventListener('mousedown', function (event) {
if (event.button == 2) {
// Without the setTimeout, the popup gets immediately hidden
// for some reason
setTimeout(function () {
_popupNode = elem;
self.id('tag-menu').openPopup(
null,
'after_pointer',
event.clientX + 2,
event.clientY + 2,
true,
event
);
event.stopPropagation();
event.preventDefault();
});
}
}, true);
elem.addEventListener('dragover', this.dragObserver.onDragOver);
elem.addEventListener('dragexit', this.dragObserver.onDragExit);
elem.addEventListener('drop', this.dragObserver.onDrop, true);
}
return elem;
]]></body>
</method>
<method name="_updateClickableTag">
<parameter name="elem"/>
<parameter name="name"/>
<parameter name="colors"/>
<body><![CDATA[
var visible = false;
var colorData = colors.get(name);
if (colorData) {
elem.style.color = colorData.color;
elem.style.fontWeight = 'bold';
}
else {
elem.style.color = '';
elem.style.fontWeight = '';
}
// Restore selection
elem.setAttribute('selected', this.selection.has(name));
// Check against tag search
if (this._search) {
var inSearch = name.toLowerCase().indexOf(this._search) != -1;
}
// Check tags against scope
if (this._hasScope) {
var inScope = !!this._scope[name];
}
// If not in search, hide
if (this._search && !inSearch) {
elem.style.display = 'none';
}
// Only show tags matching current scope
// (i.e., associated with items in the current view)
else if (this.filterToScope) {
if (inScope) {
elem.className = 'zotero-clicky';
elem.setAttribute('inScope', true);
elem.style.display = '';
visible = true;
}
else {
elem.className = '';
elem.style.display = 'none';
elem.setAttribute('inScope', false);
}
}
// Display all
else {
if (inScope) {
elem.className = 'zotero-clicky';
elem.setAttribute('inScope', true);
}
else {
elem.className = '';
elem.setAttribute('inScope', false);
}
elem.style.display = '';
visible = true;
}
// Always show colored tags at top, unless they
// don't match an active tag search
if (colorData && (!this._search || inSearch)) {
elem.style.display = '';
elem.style.order = (Zotero.Tags.MAX_COLORED_TAGS * -1) + colorData.position - 1;
elem.setAttribute('hasColor', true);
visible = true;
}
else {
elem.style.order = 0;
elem.removeAttribute('hasColor', false);
}
return visible;
]]></body>
</method> </method>
<method name="_openColorPickerWindow"> <method name="_openColorPickerWindow">
<parameter name="name"/> <parameter name="name"/>
<body> <body><![CDATA[
<![CDATA[ return Zotero.spawn(function* () {
var io = { var io = {
libraryID: this.libraryID, libraryID: this.libraryID,
name: name name: name
}; };
var self = this; var tagColors = yield Zotero.Tags.getColors(this.libraryID);
Zotero.Tags.getColors(this.libraryID) if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) {
.then(function (tagColors) {
if (Object.keys(tagColors).length >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors[io.name]) {
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService); .getService(Components.interfaces.nsIPromptService);
ps.alert(null, "", Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS)); ps.alert(null, "", Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS));
return; return;
} }
// Opening a modal window directly from within this promise handler causes io.tagColors = tagColors;
// the opened window to block on the first yielded promise until the window
// is closed.
setTimeout(function () {
window.openDialog( window.openDialog(
'chrome://zotero/content/tagColorChooser.xul', 'chrome://zotero/content/tagColorChooser.xul',
"zotero-tagSelector-colorChooser", "zotero-tagSelector-colorChooser",
@ -867,9 +898,8 @@
return; return;
} }
Zotero.Tags.setColor(self.libraryID, io.name, io.color, io.position); yield Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position);
}, 0); }.bind(this));
});
]]> ]]>
</body> </body>
</method> </method>
@ -928,7 +958,7 @@
return Zotero.DB.executeTransaction(function* () { return Zotero.DB.executeTransaction(function* () {
ids = ids.split(','); ids = ids.split(',');
var items = Zotero.Items.get(ids); var items = Zotero.Items.get(ids);
var value = node.getAttribute('value') var value = node.textContent
for (let i=0; i<items.length; i++) { for (let i=0; i<items.length; i++) {
let item = items[i]; let item = items[i];
@ -953,28 +983,33 @@
</implementation> </implementation>
<content> <content>
<groupbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" flex="1"> <groupbox flex="1"
<menupopup id="tag-menu"> xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml">
<menupopup id="tag-menu"
onpopuphidden="_popupNode = null">
<menuitem label="&zotero.tagSelector.assignColor;" <menuitem label="&zotero.tagSelector.assignColor;"
oncommand="_openColorPickerWindow(document.popupNode.getAttribute('value')); event.stopPropagation()"/> oncommand="_openColorPickerWindow(_popupNode.textContent); event.stopPropagation()"/>
<menuitem label="&zotero.tagSelector.renameTag;" <menuitem label="&zotero.tagSelector.renameTag;"
oncommand="document.getBindingParent(this).rename(document.popupNode.getAttribute('value')); event.stopPropagation()"/> oncommand="document.getBindingParent(this).rename(_popupNode.textContent); event.stopPropagation()"/>
<menuitem label="&zotero.tagSelector.deleteTag;" <menuitem label="&zotero.tagSelector.deleteTag;"
oncommand="document.getBindingParent(this).delete(document.popupNode.getAttribute('value')); event.stopPropagation()"/> oncommand="document.getBindingParent(this).delete(_popupNode.textContent); event.stopPropagation()"/>
</menupopup> </menupopup>
<vbox id="no-tags-box" align="center" pack="center" flex="1"> <deck id="tags-deck">
<deck id="no-tags-deck"> <box id="loading-box">
<label value="&zotero.tagSelector.loadingTags;"/> <label value="&zotero.tagSelector.loadingTags;"/>
<label value="&zotero.tagSelector.noTagsToDisplay;"/> </box>
</deck>
</vbox>
<vbox id="tags-toggle" flex="1"/> <box id="no-tags-box">
<label value="&zotero.tagSelector.noTagsToDisplay;"/>
</box>
<html:div id="tags-box"/>
</deck>
<vbox id="tag-controls"> <vbox id="tag-controls">
<hbox> <hbox>
<!-- TODO: &zotero.tagSelector.filter; is now unused -->
<textbox id="tags-search" flex="1" type="search" timeout="250" dir="reverse" <textbox id="tags-search" flex="1" type="search" timeout="250" dir="reverse"
oncommand="document.getBindingParent(this).handleKeyPress(); event.stopPropagation()" oncommand="document.getBindingParent(this).handleKeyPress(); event.stopPropagation()"
onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) { document.getBindingParent(this).handleKeyPress(true); }"/> onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) { document.getBindingParent(this).handleKeyPress(true); }"/>
@ -989,14 +1024,16 @@
document.getElementById('display-all-tags').setAttribute('checked', !document.getBindingParent(this).filterToScope);"> document.getElementById('display-all-tags').setAttribute('checked', !document.getBindingParent(this).filterToScope);">
<menuitem id="num-selected" disabled="true"/> <menuitem id="num-selected" disabled="true"/>
<menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;" <menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;"
oncommand="document.getBindingParent(this).clearAll(); event.stopPropagation();"/> oncommand="document.getBindingParent(this).deselectAll(); event.stopPropagation();"/>
<menuseparator/> <menuseparator/>
<menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" type="checkbox" <menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" type="checkbox"
oncommand="var ts = document.getBindingParent(this); oncommand="var ts = document.getBindingParent(this);
ts._dirty = true; ts._dirty = true;
var showAutomatic = this.getAttribute('checked') == 'true'; var showAutomatic = this.getAttribute('checked') == 'true';
ts.setAttribute('showAutomatic', showAutomatic); ts.setAttribute('showAutomatic', showAutomatic);
this.setAttribute('checked', showAutomatic);"/> this.setAttribute('checked', showAutomatic);
ts.refresh();
event.stopPropagation();"/>
<menuitem id="display-all-tags" label="&zotero.tagSelector.displayAllInLibrary;" type="checkbox" <menuitem id="display-all-tags" label="&zotero.tagSelector.displayAllInLibrary;" type="checkbox"
oncommand="var displayAll = this.getAttribute('checked') == 'true'; oncommand="var displayAll = this.getAttribute('checked') == 'true';
this.setAttribute('checked', !displayAll); this.setAttribute('checked', !displayAll);

View file

@ -180,6 +180,7 @@ var Zotero_Long_Tag_Fixer = new function () {
} }
// Remove old tags // Remove old tags
// TODO: Update
Zotero.Tags.erase(oldTagIDs); Zotero.Tags.erase(oldTagIDs);
Zotero.Tags.purge(); Zotero.Tags.purge();
Zotero.DB.commitTransaction(); Zotero.DB.commitTransaction();

View file

@ -24,13 +24,14 @@
*/ */
"use strict"; "use strict";
var _io;
var Zotero_Tag_Color_Chooser = new function() { var Zotero_Tag_Color_Chooser = new function() {
var _io;
this.init = function () { this.init = function () {
var dialog = document.getElementById('tag-color-chooser'); var dialog = document.getElementById('tag-color-chooser');
return Zotero.spawn(function* () { try {
// Set font size from pref // Set font size from pref
Zotero.setFontSize(document.getElementById("tag-color-chooser-container")); Zotero.setFontSize(document.getElementById("tag-color-chooser-container"));
@ -40,6 +41,7 @@ var Zotero_Tag_Color_Chooser = new function() {
} }
if (typeof _io.libraryID == 'undefined') throw new Error("libraryID not set"); if (typeof _io.libraryID == 'undefined') throw new Error("libraryID not set");
if (typeof _io.name == 'undefined' || _io.name === "") throw new Error("name not set"); if (typeof _io.name == 'undefined' || _io.name === "") throw new Error("name not set");
if (_io.tagColors === undefined) throw new Error("tagColors not provided");
window.sizeToContent(); window.sizeToContent();
@ -58,8 +60,8 @@ var Zotero_Tag_Color_Chooser = new function() {
var maxTags = document.getElementById('max-tags'); var maxTags = document.getElementById('max-tags');
maxTags.value = Zotero.getString('tagColorChooser.maxTags', Zotero.Tags.MAX_COLORED_TAGS); maxTags.value = Zotero.getString('tagColorChooser.maxTags', Zotero.Tags.MAX_COLORED_TAGS);
var tagColors = yield Zotero.Tags.getColors(_io.libraryID); var tagColors = _io.tagColors;
var colorData = tagColors[_io.name]; var colorData = tagColors.get(_io.name);
// Color // Color
if (colorData) { if (colorData) {
@ -68,11 +70,7 @@ var Zotero_Tag_Color_Chooser = new function() {
} }
else { else {
// Get unused color at random // Get unused color at random
var usedColors = []; var usedColors = [for (x of tagColors.values()) x.color];
for (var i in tagColors) {
usedColors.push(tagColors[i].color);
}
var unusedColors = Zotero.Utilities.arrayDiff( var unusedColors = Zotero.Utilities.arrayDiff(
colorPicker.colors, usedColors colorPicker.colors, usedColors
); );
@ -82,7 +80,7 @@ var Zotero_Tag_Color_Chooser = new function() {
} }
colorPicker.setAttribute('disabled', 'false'); colorPicker.setAttribute('disabled', 'false');
var numColors = Object.keys(tagColors).length; var numColors = tagColors.size;
var max = colorData ? numColors : numColors + 1; var max = colorData ? numColors : numColors + 1;
// Position // Position
@ -106,14 +104,13 @@ var Zotero_Tag_Color_Chooser = new function() {
this.onPositionChange(); this.onPositionChange();
window.sizeToContent(); window.sizeToContent();
}.bind(this)) }
.catch(function (e) { catch (e) {
Zotero.debug(e, 1); Zotero.logError(e);
Components.utils.reportError(e);
if (dialog.cancelDialog) { if (dialog.cancelDialog) {
dialog.cancelDialog(); dialog.cancelDialog();
} }
}); }
}; };

View file

@ -2398,12 +2398,10 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = Zotero.Promise.coroutine(fu
} }
if (this.tags){ if (this.tags){
for (var tag in this.tags){ for (let tag of this.tags) {
if (this.tags[tag]){
s2.addCondition('tag', 'is', tag); s2.addCondition('tag', 'is', tag);
} }
} }
}
Zotero.CollectionTreeCache.lastTreeRow = this; Zotero.CollectionTreeCache.lastTreeRow = this;
Zotero.CollectionTreeCache.lastSearch = s2; Zotero.CollectionTreeCache.lastSearch = s2;
@ -2456,9 +2454,7 @@ Zotero.CollectionTreeRow.prototype.isSearchMode = function() {
} }
// Tag filter // Tag filter
if (this.tags) { if (this.tags && this.tags.size) {
for (var i in this.tags) {
return true; return true;
} }
} }
}

View file

@ -370,7 +370,7 @@ Zotero.DataObjects.prototype.getUnwrittenData = function (libraryID) {
Zotero.DataObjects.prototype.reload = Zotero.Promise.coroutine(function* (ids, dataTypes, reloadUnchanged) { Zotero.DataObjects.prototype.reload = Zotero.Promise.coroutine(function* (ids, dataTypes, reloadUnchanged) {
ids = Zotero.flattenArguments(ids); ids = Zotero.flattenArguments(ids);
Zotero.debug('Reloading ' + (dataTypes ? dataTypes + ' for ' : '') Zotero.debug('Reloading ' + (dataTypes ? '[' + dataTypes.join(', ') + '] for ' : '')
+ this._ZDO_objects + ' ' + ids); + this._ZDO_objects + ' ' + ids);
for (let i=0; i<ids.length; i++) { for (let i=0; i<ids.length; i++) {

View file

@ -1495,21 +1495,33 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
for (let i=0; i<toAdd.length; i++) { for (let i=0; i<toAdd.length; i++) {
let tag = toAdd[i]; let tag = toAdd[i];
let tagID = yield Zotero.Tags.getIDFromName(this.libraryID, tag.tag, true); let tagID = yield Zotero.Tags.getID(tag.tag, true);
let tagType = tag.type ? tag.type : 0;
// "OR REPLACE" allows changing type // "OR REPLACE" allows changing type
let sql = "INSERT OR REPLACE INTO itemTags (itemID, tagID, type) VALUES (?, ?, ?)"; let sql = "INSERT OR REPLACE INTO itemTags (itemID, tagID, type) VALUES (?, ?, ?)";
yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]); yield Zotero.DB.queryAsync(sql, [this.id, tagID, tagType]);
Zotero.Notifier.queue('add', 'item-tag', this.id + '-' + tag.tag);
let notifierData = {};
notifierData[this.id + '-' + tagID] = {
libraryID: this.libraryID,
tag: tag.tag,
type: tagType
};
Zotero.Notifier.queue('add', 'item-tag', this.id + '-' + tagID, notifierData);
} }
if (toRemove.length) { if (toRemove.length) {
yield Zotero.Tags.load(this.libraryID);
for (let i=0; i<toRemove.length; i++) { for (let i=0; i<toRemove.length; i++) {
let tag = toRemove[i]; let tag = toRemove[i];
let tagID = Zotero.Tags.getID(this.libraryID, tag.tag); let tagID = yield Zotero.Tags.getID(tag.tag);
let sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=? AND type=?"; let sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=? AND type=?";
yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]); yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]);
Zotero.Notifier.queue('remove', 'item-tag', this.id + '-' + tag.tag); let notifierData = {};
notifierData[this.id + '-' + tagID] = {
libraryID: this.libraryID,
tag: tag.tag
};
Zotero.Notifier.queue('remove', 'item-tag', this.id + '-' + tagID, notifierData);
} }
Zotero.Prefs.set('purge.tags', true); Zotero.Prefs.set('purge.tags', true);
} }
@ -3426,8 +3438,9 @@ Zotero.Item.prototype.getImageSrcWithTags = Zotero.Promise.coroutine(function* (
var colorData = []; var colorData = [];
for (let i=0; i<tags.length; i++) { for (let i=0; i<tags.length; i++) {
let tag = tags[i]; let tag = tags[i];
if (tagColors[tag.tag]) { let data = tagColors.get(tag.tag);
colorData.push(tagColors[tag.tag]); if (data) {
colorData.push(data);
} }
} }
if (!colorData.length) { if (!colorData.length) {

View file

@ -30,10 +30,6 @@
Zotero.Tags = new function() { Zotero.Tags = new function() {
this.MAX_COLORED_TAGS = 6; this.MAX_COLORED_TAGS = 6;
var _tagIDsByName = {};
var _tagNamesByID = {};
var _loaded = {};
var _libraryColors = {}; var _libraryColors = {};
var _libraryColorsByName = {}; var _libraryColorsByName = {};
var _itemsListImagePromises = {}; var _itemsListImagePromises = {};
@ -43,78 +39,43 @@ Zotero.Tags = new function() {
/** /**
* Returns a tag for a given tagID * Returns a tag for a given tagID
* *
* @param {Number} libraryID * @param {Integer} tagID
* @param {Number} tagID * @return {Promise<String|false>} - A tag name, or false if tag with id not found
*/ */
this.getName = function (libraryID, tagID) { this.getName = function (tagID) {
if (!tagID) { return Zotero.DB.valueQueryAsync("SELECT name FROM tags WHERE tagID=?", tagID);
throw new Error("tagID not provided");
} }
if (_tagNamesByID[tagID]) {
return _tagNamesByID[tagID];
}
_requireLoad(libraryID);
return false;
}
/**
* Returns the tagID matching a given tag
*
* @param {Number} libraryID
* @param {String} name
*/
this.getID = function (libraryID, name) {
if (_tagIDsByName[libraryID] && _tagIDsByName[libraryID]['_' + name]) {
return _tagIDsByName[libraryID]['_' + name];
}
_requireLoad(libraryID);
return false;
};
/** /**
* Returns the tagID matching given fields, or creates a new tag and returns its id * Returns the tagID matching given fields, or creates a new tag and returns its id
* *
* @requireTransaction
* @param {Number} libraryID
* @param {String} name - Tag data in API JSON format * @param {String} name - Tag data in API JSON format
* @param {Boolean} [create=false] - If no matching tag, create one * @param {Boolean} [create=false] - If no matching tag, create one;
* requires a wrapping transaction
* @return {Promise<Integer>} tagID * @return {Promise<Integer>} tagID
*/ */
this.getIDFromName = Zotero.Promise.coroutine(function* (libraryID, name, create) { this.getID = Zotero.Promise.coroutine(function* (name, create) {
if (create) {
Zotero.DB.requireTransaction(); Zotero.DB.requireTransaction();
}
data = this.cleanData({ data = this.cleanData({
tag: name tag: name
}); });
var sql = "SELECT tagID FROM tags WHERE libraryID=? AND name=?"; var sql = "SELECT tagID FROM tags WHERE name=?";
var id = yield Zotero.DB.valueQueryAsync(sql, [libraryID, data.tag]); var id = yield Zotero.DB.valueQueryAsync(sql, data.tag);
if (!id && create) { if (!id && create) {
id = yield Zotero.ID.get('tags'); id = yield Zotero.ID.get('tags');
let sql = "INSERT INTO tags (tagID, libraryID, name) VALUES (?, ?, ?)"; let sql = "INSERT INTO tags (tagID, name) VALUES (?, ?)";
let insertID = yield Zotero.DB.queryAsync(sql, [id, libraryID, data.tag]); let insertID = yield Zotero.DB.queryAsync(sql, [id, data.tag]);
if (!id) { if (!id) {
id = insertID; id = insertID;
} }
_cacheTag(libraryID, id, data.tag);
} }
return id; return id;
}); });
/*
* Returns an array of tag types for tags matching given tag
*/
this.getTypes = Zotero.Promise.method(function (name, libraryID) {
if (libraryID != parseInt(libraryID)) {
throw new Error("libraryID must be an integer");
}
var sql = "SELECT type FROM tags WHERE libraryID=? AND name=?";
return Zotero.DB.columnQueryAsync(sql, [libraryID, name.trim()]);
});
/** /**
* Get all tags indexed by tagID * Get all tags indexed by tagID
* *
@ -125,7 +86,7 @@ Zotero.Tags = new function() {
*/ */
this.getAll = Zotero.Promise.coroutine(function* (libraryID, types) { this.getAll = Zotero.Promise.coroutine(function* (libraryID, types) {
var sql = "SELECT DISTINCT name AS tag, type FROM tags " var sql = "SELECT DISTINCT name AS tag, type FROM tags "
+ "JOIN itemTags USING (tagID) WHERE libraryID=?"; + "JOIN itemTags USING (tagID) JOIN items USING (itemID) WHERE libraryID=?";
var params = [libraryID]; var params = [libraryID];
if (types) { if (types) {
sql += " AND type IN (" + types.join() + ")"; sql += " AND type IN (" + types.join() + ")";
@ -177,14 +138,15 @@ Zotero.Tags = new function() {
/** /**
* Get the items associated with the given saved tag * Get the items associated with the given tag
* *
* @param {Number} tagID * @param {Number} tagID
* @return {Promise<Number[]>} A promise for an array of itemIDs * @return {Promise<Number[]>} A promise for an array of itemIDs
*/ */
this.getTagItems = function (tagID) { this.getTagItems = function (libraryID, tagID) {
var sql = "SELECT itemID FROM itemTags WHERE tagID=?"; var sql = "SELECT itemID FROM itemTags JOIN items USING (itemID) "
return Zotero.DB.columnQueryAsync(sql, tagID); + "WHERE tagID=? AND libraryID=?";
return Zotero.DB.columnQueryAsync(sql, [tagID, libraryID]);
} }
@ -198,28 +160,6 @@ Zotero.Tags = new function() {
}); });
this.load = Zotero.Promise.coroutine(function* (libraryID, reload) {
if (_loaded[libraryID] && !reload) {
return;
}
Zotero.debug("Loading tags in library " + libraryID);
var sql = 'SELECT tagID AS id, name FROM tags WHERE libraryID=?';
var tags = yield Zotero.DB.queryAsync(sql, libraryID);
_tagIDsByName[libraryID] = {}
for (var i=0; i<tags.length; i++) {
let tag = tags[i];
_tagIDsByName[libraryID]['_' + tag.name] = tag.id;
_tagNamesByID[tag.id] = tag.name;
}
_loaded[libraryID] = true;
});
/** /**
* Rename a tag and update the tag colors setting accordingly if necessary * Rename a tag and update the tag colors setting accordingly if necessary
* *
@ -228,7 +168,7 @@ Zotero.Tags = new function() {
* @return {Promise} * @return {Promise}
*/ */
this.rename = Zotero.Promise.coroutine(function* (libraryID, oldName, newName) { this.rename = Zotero.Promise.coroutine(function* (libraryID, oldName, newName) {
Zotero.debug("Renaming tag '" + oldName + "' to '" + newName + "'", 4); Zotero.debug("Renaming tag '" + oldName + "' to '" + newName + "' in library " + libraryID);
oldName = oldName.trim(); oldName = oldName.trim();
newName = newName.trim(); newName = newName.trim();
@ -238,16 +178,15 @@ Zotero.Tags = new function() {
return; return;
} }
yield Zotero.Tags.load(libraryID); var oldTagID = yield this.getID(oldName);
var oldTagID = this.getID(libraryID, oldName);
// We need to know if the old tag has a color assigned so that // We need to know if the old tag has a color assigned so that
// we can assign it to the new name // we can assign it to the new name
var oldColorData = yield this.getColor(libraryID, oldName); var oldColorData = yield this.getColor(libraryID, oldName);
yield Zotero.DB.executeTransaction(function* () { yield Zotero.DB.executeTransaction(function* () {
var oldItemIDs = yield this.getTagItems(oldTagID); var oldItemIDs = yield this.getTagItems(libraryID, oldTagID);
var newTagID = yield this.getIDFromName(libraryID, newName, true); var newTagID = yield this.getID(newName, true);
yield Zotero.Utilities.Internal.forEachChunkAsync( yield Zotero.Utilities.Internal.forEachChunkAsync(
oldItemIDs, oldItemIDs,
@ -269,19 +208,23 @@ Zotero.Tags = new function() {
); );
var notifierData = {}; var notifierData = {};
notifierData[newName] = { for (let i = 0; i < oldItemIDs.length; i++) {
notifierData[oldItemIDs[i] + '-' + newTagID] = {
tag: newName,
old: { old: {
tag: oldName tag: oldName
} }
}
}; };
Zotero.Notifier.queue( Zotero.Notifier.queue(
'modify', 'modify',
'item-tag', 'item-tag',
oldItemIDs.map(function (itemID) itemID + '-' + newName), oldItemIDs.map(itemID => itemID + '-' + newTagID),
notifierData notifierData
); );
yield this.purge(libraryID, oldTagID); yield this.purge(oldTagID);
}.bind(this)); }.bind(this));
if (oldColorData) { if (oldColorData) {
@ -302,30 +245,48 @@ Zotero.Tags = new function() {
/** /**
* @return {Promise} * @return {Promise}
*/ */
this.erase = Zotero.Promise.coroutine(function* (libraryID, tagIDs) { this.removeFromLibrary = Zotero.Promise.coroutine(function* (libraryID, tagIDs) {
tagIDs = Zotero.flattenArguments(tagIDs); tagIDs = Zotero.flattenArguments(tagIDs);
var deletedNames = []; var deletedNames = [];
var oldItemIDs = []; var oldItemIDs = [];
yield Zotero.DB.executeTransaction(function* () { yield Zotero.DB.executeTransaction(function* () {
yield Zotero.Tags.load(libraryID); var notifierPairs = [];
var notifierData = {};
for (let i=0; i<tagIDs.length; i++) { for (let i=0; i<tagIDs.length; i++) {
let tagID = tagIDs[i]; let tagID = tagIDs[i];
let name = this.getName(libraryID, tagID); let name = yield this.getName(tagID);
if (name === false) { if (name === false) {
continue; continue;
} }
deletedNames.push(name); deletedNames.push(name);
oldItemIDs = oldItemIDs.concat(yield this.getTagItems(tagID));
// This causes a cascading delete from itemTags // Since we're performing the DELETE query directly,
let sql = "DELETE FROM tags WHERE tagID=?"; // get the list of items that will need their tags reloaded,
yield Zotero.DB.queryAsync(sql, [tagID]); // and generate data for item-tag notifications
let tagItems = yield this.getTagItems(libraryID, tagID);
for (let j = 0; j < tagItems.length; j++) {
let itemID = tagItems[i];
let pair = itemID + "-" + tagID;
notifierPairs.push(pair);
notifierData[pair] = {
libraryID: libraryID,
tag: name
};
}
oldItemIDs = oldItemIDs.concat(tagItems);
}
if (oldItemIDs.length) {
Zotero.Notifier.queue('remove', 'item-tag', notifierPairs, notifierData);
} }
yield this.purge(libraryID, tagIDs); var sql = "DELETE FROM itemTags WHERE tagID IN ("
+ tagIDs.map(x => '?').join(', ') + ") AND itemID IN "
+ "(SELECT itemID FROM items WHERE libraryID=?)";
yield Zotero.DB.queryAsync(sql, tagIDs.concat([libraryID]));
yield this.purge(tagIDs);
// Update internal timestamps on all items that had these tags // Update internal timestamps on all items that had these tags
yield Zotero.Utilities.Internal.forEachChunkAsync( yield Zotero.Utilities.Internal.forEachChunkAsync(
@ -334,11 +295,11 @@ Zotero.Tags = new function() {
function* (chunk) { function* (chunk) {
let placeholders = chunk.map(function () '?').join(','); let placeholders = chunk.map(function () '?').join(',');
sql = 'UPDATE items SET clientDateModified=? ' sql = 'UPDATE items SET synced=0, clientDateModified=? '
+ 'WHERE itemID IN (' + placeholders + ')' + 'WHERE itemID IN (' + placeholders + ')'
yield Zotero.DB.queryAsync(sql, [Zotero.DB.transactionDateTime].concat(chunk)); yield Zotero.DB.queryAsync(sql, [Zotero.DB.transactionDateTime].concat(chunk));
yield Zotero.Items.reload(oldItemIDs, ['tags']); yield Zotero.Items.reload(oldItemIDs, ['primaryData', 'tags'], true);
} }
); );
}.bind(this)); }.bind(this));
@ -355,7 +316,7 @@ Zotero.Tags = new function() {
/** /**
* Delete obsolete tags from database and clear internal cache entries * Delete obsolete tags from database
* *
* @param {Number} libraryID * @param {Number} libraryID
* @param {Number|Number[]} [tagIDs] - tagID or array of tagIDs to purge * @param {Number|Number[]} [tagIDs] - tagID or array of tagIDs to purge
@ -379,7 +340,7 @@ Zotero.Tags = new function() {
for (let i=0; i<tagIDs.length; i++) { for (let i=0; i<tagIDs.length; i++) {
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO tagDelete VALUES (?)", tagIDs[i]); yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO tagDelete VALUES (?)", tagIDs[i]);
} }
sql = "SELECT tagID AS id, libraryID, name FROM tagDelete JOIN tags USING (tagID) " sql = "SELECT tagID AS id, name FROM tagDelete JOIN tags USING (tagID) "
+ "WHERE tagID NOT IN (SELECT tagID FROM itemTags)"; + "WHERE tagID NOT IN (SELECT tagID FROM itemTags)";
var toDelete = yield Zotero.DB.queryAsync(sql); var toDelete = yield Zotero.DB.queryAsync(sql);
} }
@ -393,7 +354,7 @@ Zotero.Tags = new function() {
sql = "CREATE INDEX tagDelete_tagID ON tagDelete(tagID)"; sql = "CREATE INDEX tagDelete_tagID ON tagDelete(tagID)";
yield Zotero.DB.queryAsync(sql); yield Zotero.DB.queryAsync(sql);
sql = "SELECT tagID AS id, libraryID, name FROM tagDelete JOIN tags USING (tagID)"; sql = "SELECT tagID AS id, name FROM tagDelete JOIN tags USING (tagID)";
var toDelete = yield Zotero.DB.queryAsync(sql); var toDelete = yield Zotero.DB.queryAsync(sql);
if (!toDelete.length) { if (!toDelete.length) {
@ -409,16 +370,9 @@ Zotero.Tags = new function() {
ids.push(row.id); ids.push(row.id);
notifierData[row.id] = { notifierData[row.id] = {
old: { old: {
libraryID: row.libraryID,
tag: row.name tag: row.name
} }
}; };
// Clear cached values
delete _tagNamesByID[row.id];
if (_tagIDsByName[row.libraryID]) {
delete _tagIDsByName[row.libraryID]['_' + row.name];
}
} }
sql = "DELETE FROM tags WHERE tagID IN (SELECT tagID FROM tagDelete);"; sql = "DELETE FROM tags WHERE tagID IN (SELECT tagID FROM tagDelete);";
@ -445,8 +399,7 @@ Zotero.Tags = new function() {
this.getColor = function (libraryID, name) { this.getColor = function (libraryID, name) {
return this.getColors(libraryID) return this.getColors(libraryID)
.then(function () { .then(function () {
return _libraryColorsByName[libraryID][name] return _libraryColorsByName[libraryID].get(name) || false;
? _libraryColorsByName[libraryID][name] : false;
}); });
} }
@ -468,8 +421,10 @@ Zotero.Tags = new function() {
/** /**
* Get colored tags within a given library
*
* @param {Integer} libraryID * @param {Integer} libraryID
* @return {Promise} A promise for an object with tag names as keys and * @return {Promise<Map>} - A promise for a Map with tag names as keys and
* objects containing 'color' and 'position' as values * objects containing 'color' and 'position' as values
*/ */
this.getColors = Zotero.Promise.coroutine(function* (libraryID) { this.getColors = Zotero.Promise.coroutine(function* (libraryID) {
@ -487,14 +442,14 @@ Zotero.Tags = new function() {
tagColors = tagColors || []; tagColors = tagColors || [];
_libraryColors[libraryID] = tagColors; _libraryColors[libraryID] = tagColors;
_libraryColorsByName[libraryID] = {}; _libraryColorsByName[libraryID] = new Map;
// Also create object keyed by name for quick checking for individual tag colors // Also create object keyed by name for quick checking for individual tag colors
for (let i=0; i<tagColors.length; i++) { for (let i=0; i<tagColors.length; i++) {
_libraryColorsByName[libraryID][tagColors[i].name] = { _libraryColorsByName[libraryID].set(tagColors[i].name, {
color: tagColors[i].color, color: tagColors[i].color,
position: i position: i
}; });
} }
return _libraryColorsByName[libraryID]; return _libraryColorsByName[libraryID];
@ -511,7 +466,6 @@ Zotero.Tags = new function() {
throw new Error("libraryID must be an integer"); throw new Error("libraryID must be an integer");
} }
yield this.load(libraryID);
yield this.getColors(libraryID); yield this.getColors(libraryID);
var tagColors = _libraryColors[libraryID]; var tagColors = _libraryColors[libraryID];
@ -519,7 +473,7 @@ Zotero.Tags = new function() {
// Unset // Unset
if (!color) { if (!color) {
// Trying to clear color on tag that doesn't have one // Trying to clear color on tag that doesn't have one
if (!_libraryColorsByName[libraryID][name]) { if (!_libraryColorsByName[libraryID].has(name)) {
return; return;
} }
@ -607,12 +561,13 @@ Zotero.Tags = new function() {
var tagNames = tagColors.concat(previousTagColors).map(function (val) val.name); var tagNames = tagColors.concat(previousTagColors).map(function (val) val.name);
tagNames = Zotero.Utilities.arrayUnique(tagNames); tagNames = Zotero.Utilities.arrayUnique(tagNames);
if (tagNames.length) { if (tagNames.length) {
yield Zotero.Tags.load(libraryID);
for (let i=0; i<tagNames.length; i++) { for (let i=0; i<tagNames.length; i++) {
let tagID = this.getID(libraryID, tagNames[i]); let tagID = yield this.getID(tagNames[i]);
// Colored tags may not exist // Colored tags may not exist
if (tagID) { if (tagID) {
affectedItems = affectedItems.concat(yield this.getTagItems(tagID)); affectedItems = affectedItems.concat(
yield this.getTagItems(libraryID, tagID)
);
} }
}; };
} }
@ -629,8 +584,7 @@ Zotero.Tags = new function() {
return; return;
} }
yield this.load(libraryID); var tagID = yield this.getID(tagName);
var tagID = this.getID(libraryID, tagName);
// If there's a color setting but no matching tag, don't throw // If there's a color setting but no matching tag, don't throw
// an error (though ideally this wouldn't be possible). // an error (though ideally this wouldn't be possible).
@ -854,40 +808,5 @@ Zotero.Tags = new function() {
} }
return cleanedData; return cleanedData;
} }
/**
* Clear cache to reload
*/
this.reload = function () {
_tagNamesByID = {};
_tagIDsByName = {};
}
this.getPrimaryDataSQL = function () {
// This should be the same as the query in Zotero.Tag.load(),
// just without a specific tagID
return "SELECT * FROM tags O WHERE 1";
}
function _requireLoad(libraryID) {
if (!_loaded[libraryID]) {
throw new Zotero.Exception.UnloadedDataException(
"Tag data has not been loaded for library " + libraryID,
"tags"
);
}
}
function _cacheTag(libraryID, tagID, name) {
_tagNamesByID[tagID] = name;
if (!_tagIDsByName[libraryID]) {
_tagIDsByName[libraryID] = {};
}
_tagIDsByName[libraryID]['_' + name] = tagID;
}
} }

View file

@ -166,30 +166,23 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
event.preventDefault(); event.preventDefault();
Zotero.Promise.try(function () { Zotero.spawn(function* () {
if (coloredTagsRE.test(key)) { if (coloredTagsRE.test(key)) {
let libraryID = self.collectionTreeRow.ref.libraryID; let libraryID = self.collectionTreeRow.ref.libraryID;
let position = parseInt(key) - 1; let position = parseInt(key) - 1;
return Zotero.Tags.getColorByPosition(libraryID, position) let colorData = yield Zotero.Tags.getColorByPosition(libraryID, position);
.then(function (colorData) {
// If a color isn't assigned to this number or any // If a color isn't assigned to this number or any
// other numbers, allow key navigation // other numbers, allow key navigation
if (!colorData) { if (!colorData) {
return Zotero.Tags.getColors(libraryID) let colors = yield Zotero.Tags.getColors(libraryID);
.then(function (colors) { return !colors.size;
return !Object.keys(colors).length;
});
} }
var items = self.getSelectedItems(); var items = self.getSelectedItems();
return Zotero.Tags.toggleItemsListTags(libraryID, items, colorData.name) yield Zotero.Tags.toggleItemsListTags(libraryID, items, colorData.name);
.then(function () { return;
return false;
});
});
} }
return true;
})
// We have to disable key navigation on the tree in order to // We have to disable key navigation on the tree in order to
// keep it from acting on the 1-6 keys used for colored tags. // keep it from acting on the 1-6 keys used for colored tags.
// To allow navigation with other keys, we temporarily enable // To allow navigation with other keys, we temporarily enable
@ -197,11 +190,6 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
// that will trigger this listener again, we set a flag to // that will trigger this listener again, we set a flag to
// ignore the event, and then clear the flag above when the // ignore the event, and then clear the flag above when the
// event comes in. I see no way this could go wrong... // event comes in. I see no way this could go wrong...
.then(function (resend) {
if (!resend) {
return;
}
tree.disableKeyNavigation = false; tree.disableKeyNavigation = false;
self._skipKeyPress = true; self._skipKeyPress = true;
var nsIDWU = Components.interfaces.nsIDOMWindowUtils; var nsIDWU = Components.interfaces.nsIDOMWindowUtils;
@ -230,10 +218,8 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
tree.disableKeyNavigation = true; tree.disableKeyNavigation = true;
}) })
.catch(function (e) { .catch(function (e) {
Zotero.debug(e, 1); Zotero.logError(e);
Components.utils.reportError(e);
}) })
.done();
}; };
// Store listener so we can call removeEventListener() in ItemTreeView.unregister() // Store listener so we can call removeEventListener() in ItemTreeView.unregister()
this.listener = listener; this.listener = listener;

View file

@ -2122,11 +2122,11 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("CREATE INDEX savedSearches_synced ON savedSearches(synced)"); yield Zotero.DB.queryAsync("CREATE INDEX savedSearches_synced ON savedSearches(synced)");
yield Zotero.DB.queryAsync("ALTER TABLE tags RENAME TO tagsOld"); yield Zotero.DB.queryAsync("ALTER TABLE tags RENAME TO tagsOld");
yield Zotero.DB.queryAsync("CREATE TABLE tags (\n tagID INTEGER PRIMARY KEY,\n libraryID INT NOT NULL,\n name TEXT NOT NULL,\n UNIQUE (libraryID, name)\n)"); yield Zotero.DB.queryAsync("CREATE TABLE tags (\n tagID INTEGER PRIMARY KEY,\n name TEXT NOT NULL UNIQUE\n)");
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO tags SELECT tagID, IFNULL(libraryID, 1), name FROM tagsOld"); yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO tags SELECT tagID, name FROM tagsOld");
yield Zotero.DB.queryAsync("ALTER TABLE itemTags RENAME TO itemTagsOld"); yield Zotero.DB.queryAsync("ALTER TABLE itemTags RENAME TO itemTagsOld");
yield Zotero.DB.queryAsync("CREATE TABLE itemTags (\n itemID INT NOT NULL,\n tagID INT NOT NULL,\n type INT NOT NULL,\n PRIMARY KEY (itemID, tagID),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (tagID) REFERENCES tags(tagID) ON DELETE CASCADE\n)"); yield Zotero.DB.queryAsync("CREATE TABLE itemTags (\n itemID INT NOT NULL,\n tagID INT NOT NULL,\n type INT NOT NULL,\n PRIMARY KEY (itemID, tagID),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (tagID) REFERENCES tags(tagID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemTags SELECT itemID, T.tagID, TOld.type FROM itemTagsOld ITO JOIN tagsOld TOld USING (tagID) JOIN tags T ON (IFNULL(TOld.libraryID, 1)=T.libraryID AND TOld.name=T.name COLLATE BINARY)"); yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemTags SELECT itemID, T.tagID, TOld.type FROM itemTagsOld ITO JOIN tagsOld TOld USING (tagID) JOIN tags T ON (TOld.name=T.name COLLATE BINARY)");
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemTags_tagID"); yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemTags_tagID");
yield Zotero.DB.queryAsync("CREATE INDEX itemTags_tagID ON itemTags(tagID)"); yield Zotero.DB.queryAsync("CREATE INDEX itemTags_tagID ON itemTags(tagID)");

View file

@ -108,6 +108,7 @@ Zotero.SyncedSettings = (function () {
if (currentValue === false) { if (currentValue === false) {
return false; return false;
} }
currentValue = JSON.parse(currentValue);
var id = libraryID + '/' + setting; var id = libraryID + '/' + setting;

View file

@ -2092,7 +2092,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
this.reloadDataObjects = function () { this.reloadDataObjects = function () {
return Zotero.Promise.all([ return Zotero.Promise.all([
Zotero.Tags.reloadAll(),
Zotero.Collections.reloadAll(), Zotero.Collections.reloadAll(),
Zotero.Creators.reloadAll(), Zotero.Creators.reloadAll(),
Zotero.Items.reloadAll() Zotero.Items.reloadAll()

View file

@ -1056,18 +1056,13 @@ var ZoteroPane = new function()
function getTagSelection() { function getTagSelection() {
var tagSelector = document.getElementById('zotero-tag-selector'); var tagSelector = document.getElementById('zotero-tag-selector');
return tagSelector.selection ? tagSelector.selection : {}; return tagSelector.selection ? tagSelector.selection : new Set();
} }
this.clearTagSelection = Zotero.Promise.coroutine(function* () { this.clearTagSelection = function () {
if (Zotero.Utilities.isEmpty(getTagSelection())) { document.getElementById('zotero-tag-selector').deselectAll();
return false;
} }
var tagSelector = document.getElementById('zotero-tag-selector');
yield tagSelector.clearAll();
return true;
});
/* /*
@ -1142,8 +1137,8 @@ var ZoteroPane = new function()
// XBL functions might not yet be available // XBL functions might not yet be available
var tagSelector = document.getElementById('zotero-tag-selector'); var tagSelector = document.getElementById('zotero-tag-selector');
if (tagSelector.clearAll) { if (tagSelector.deselectAll) {
tagSelector.clearAll(); tagSelector.deselectAll();
} }
// Not necessary with seltype="cell", which calls nsITreeView::isSelectable() // Not necessary with seltype="cell", which calls nsITreeView::isSelectable()

View file

@ -142,11 +142,8 @@
<!ENTITY zotero.tagSelector.noTagsToDisplay "No tags to display"> <!ENTITY zotero.tagSelector.noTagsToDisplay "No tags to display">
<!ENTITY zotero.tagSelector.loadingTags "Loading tags…"> <!ENTITY zotero.tagSelector.loadingTags "Loading tags…">
<!ENTITY zotero.tagSelector.filter "Filter:">
<!ENTITY zotero.tagSelector.showAutomatic "Show Automatic"> <!ENTITY zotero.tagSelector.showAutomatic "Show Automatic">
<!ENTITY zotero.tagSelector.displayAllInLibrary "Display All Tags in This Library"> <!ENTITY zotero.tagSelector.displayAllInLibrary "Display All Tags in This Library">
<!ENTITY zotero.tagSelector.selectVisible "Select Visible">
<!ENTITY zotero.tagSelector.clearVisible "Deselect Visible">
<!ENTITY zotero.tagSelector.clearAll "Deselect All"> <!ENTITY zotero.tagSelector.clearAll "Deselect All">
<!ENTITY zotero.tagSelector.assignColor "Assign Color…"> <!ENTITY zotero.tagSelector.assignColor "Assign Color…">
<!ENTITY zotero.tagSelector.renameTag "Rename Tag…"> <!ENTITY zotero.tagSelector.renameTag "Rename Tag…">

View file

@ -6,41 +6,51 @@ groupbox
padding: 1px 1px 0; padding: 1px 1px 0;
} }
#tags-toggle #tags-deck {
{ -moz-box-flex: 1;
}
#tags-deck > box {
-moz-box-align: center;
-moz-box-pack: center;
}
#tags-box {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
display: block; /* allow labels to wrap instead of all being in one line */
background-color: -moz-field; background-color: -moz-field;
} }
checkbox #tags-box button {
{
margin: .75em 0 .4em;
}
#tags-toggle label
{
margin: .15em .05em .15em .3em !important; margin: .15em .05em .15em .3em !important;
padding: 0 .25em 0 .25em !important; padding: 0 .25em 0 .25em !important;
-moz-user-focus: ignore;
max-width: 250px; max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
background-color: transparent;
text-align: left;
white-space: nowrap;
border: 1px solid transparent; /* always include border so height is same as zotero-clicky */ border: 1px solid transparent; /* always include border so height is same as zotero-clicky */
-moz-appearance: none;
-moz-padding-start: 0 !important;
-moz-padding-end: 0 !important;
-moz-user-focus: ignore;
} }
/* Visible out-of-scope tags should be grey */ /* Visible out-of-scope tags should be grey */
#tags-toggle label[inScope=false]:not([hasColor=true]) #tags-box button[inScope=false]:not([hasColor=true]) {
{
color: #666 !important; color: #666 !important;
} }
#tags-toggle label[inScope=false][hasColor=true] #tags-box button[inScope=false][hasColor=true] {
{
opacity: .6; opacity: .6;
} }
#tags-toggle label[draggedOver="true"] #tags-box button[draggedOver="true"] {
{
color: white !important; color: white !important;
background: #666; background: #666;
} }

View file

@ -73,8 +73,9 @@ ZoteroAutoComplete.prototype.startSearch = Zotero.Promise.coroutine(function* (s
case 'tag': case 'tag':
var sql = "SELECT DISTINCT name AS val, NULL AS comment FROM tags WHERE name LIKE ?"; var sql = "SELECT DISTINCT name AS val, NULL AS comment FROM tags WHERE name LIKE ?";
var sqlParams = [searchString + '%']; var sqlParams = [searchString + '%'];
if (typeof searchParams.libraryID != 'undefined') { if (searchParams.libraryID !== undefined) {
sql += " AND libraryID=?"; sql += " AND tagID IN (SELECT tagID FROM itemTags JOIN items USING (itemID) "
+ "WHERE libraryID=?)";
sqlParams.push(searchParams.libraryID); sqlParams.push(searchParams.libraryID);
} }
if (searchParams.itemID) { if (searchParams.itemID) {

View file

@ -210,24 +210,6 @@ CREATE TRIGGER fku_itemNotes
END; END;
-- itemTags libraryID
DROP TRIGGER IF EXISTS fki_itemTags_libraryID;
CREATE TRIGGER fki_itemTags_libraryID
BEFORE INSERT ON itemTags
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'insert on table "itemTags" violates foreign key constraint "fki_itemTags_libraryID"')
WHERE (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);---
END;
DROP TRIGGER IF EXISTS fku_itemTags_libraryID;
CREATE TRIGGER fku_itemTags_libraryID
BEFORE UPDATE ON itemTags
FOR EACH ROW BEGIN
SELECT RAISE(ABORT, 'update on table "itemTags" violates foreign key constraint "fku_itemTags_libraryID"')
WHERE (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);---
END;
-- Make sure tags aren't empty -- Make sure tags aren't empty
DROP TRIGGER IF EXISTS fki_tags; DROP TRIGGER IF EXISTS fki_tags;
CREATE TRIGGER fki_tags CREATE TRIGGER fki_tags

View file

@ -115,9 +115,7 @@ CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState);
CREATE TABLE tags ( CREATE TABLE tags (
tagID INTEGER PRIMARY KEY, tagID INTEGER PRIMARY KEY,
libraryID INT NOT NULL, name TEXT NOT NULL UNIQUE
name TEXT NOT NULL,
UNIQUE (libraryID, name)
); );
CREATE TABLE itemRelations ( CREATE TABLE itemRelations (

View file

@ -77,7 +77,7 @@ describe("Support Functions for Unit Testing", function() {
let tags = data.itemWithTags.tags; let tags = data.itemWithTags.tags;
for (let i=0; i<tags.length; i++) { for (let i=0; i<tags.length; i++) {
let tagID = Zotero.Tags.getID(zItem.libraryID, tags[i].tag); let tagID = yield Zotero.Tags.getID(tags[i].tag);
assert.ok(tagID, '"' + tags[i].tag + '" tag was inserted into the database'); assert.ok(tagID, '"' + tags[i].tag + '" tag was inserted into the database');
assert.ok(zItem.hasTag(tags[i].tag), '"' + tags[i].tag + '" tag was assigned to item'); assert.ok(zItem.hasTag(tags[i].tag), '"' + tags[i].tag + '" tag was assigned to item');
} }

View file

@ -3,6 +3,40 @@
describe("Tag Selector", function () { describe("Tag Selector", function () {
var win, doc, collectionsView; var win, doc, collectionsView;
var clearTagColors = Zotero.Promise.coroutine(function* (libraryID) {
var tagColors = yield Zotero.Tags.getColors(libraryID);
for (let name of tagColors.keys()) {
yield Zotero.Tags.setColor(libraryID, name, false);
}
});
function getColoredTags() {
var tagSelector = doc.getElementById('zotero-tag-selector');
var tagsBox = tagSelector.id('tags-box');
var elems = tagsBox.getElementsByTagName('button');
var names = [];
for (let i = 0; i < elems.length; i++) {
if (elems[i].style.order < 0) {
names.push(elems[i].textContent);
}
}
return names;
}
function getRegularTags() {
var tagSelector = doc.getElementById('zotero-tag-selector');
var tagsBox = tagSelector.id('tags-box');
var elems = tagsBox.getElementsByTagName('button');
var names = [];
for (let i = 0; i < elems.length; i++) {
if (elems[i].style.order >= 0 && elems[i].style.display != 'none') {
names.push(elems[i].textContent);
}
}
return names;
}
before(function* () { before(function* () {
win = yield loadZoteroPane(); win = yield loadZoteroPane();
doc = win.document; doc = win.document;
@ -11,6 +45,10 @@ describe("Tag Selector", function () {
// Wait for things to settle // Wait for things to settle
yield Zotero.Promise.delay(100); yield Zotero.Promise.delay(100);
}); });
beforeEach(function* () {
var libraryID = Zotero.Libraries.userLibraryID;
yield clearTagColors(libraryID);
})
after(function () { after(function () {
win.close(); win.close();
}); });
@ -27,7 +65,7 @@ describe("Tag Selector", function () {
} }
describe("#notify()", function () { describe("#notify()", function () {
it("should add a tag when added to an item in the current view", function* () { it("should add a tag when added to an item in the library root", function* () {
var promise, tagSelector; var promise, tagSelector;
if (collectionsView.selection.currentIndex != 0) { if (collectionsView.selection.currentIndex != 0) {
@ -41,6 +79,10 @@ describe("Tag Selector", function () {
item.setTags([ item.setTags([
{ {
tag: 'A' tag: 'A'
},
{
tag: 'B',
type: 1
} }
]); ]);
promise = waitForTagSelector(); promise = waitForTagSelector();
@ -48,8 +90,11 @@ describe("Tag Selector", function () {
yield promise; yield promise;
// Tag selector should have at least one tag // Tag selector should have at least one tag
tagSelector = doc.getElementById('zotero-tag-selector'); assert.isAbove(getRegularTags().length, 1);
assert.isAbove(tagSelector.getVisible().length, 0); });
it("should add a tag when an item is added in a collection", function* () {
var promise, tagSelector;
// Add collection // Add collection
promise = waitForTagSelector(); promise = waitForTagSelector();
@ -57,14 +102,13 @@ describe("Tag Selector", function () {
yield promise; yield promise;
// Tag selector should be empty in new collection // Tag selector should be empty in new collection
tagSelector = doc.getElementById('zotero-tag-selector'); assert.equal(getRegularTags().length, 0);
assert.equal(tagSelector.getVisible().length, 0);
// Add item with tag to collection // Add item with tag to collection
var item = createUnsavedDataObject('item'); var item = createUnsavedDataObject('item');
item.setTags([ item.setTags([
{ {
tag: 'B' tag: 'C'
} }
]); ]);
item.setCollections([collection.id]); item.setCollections([collection.id]);
@ -72,9 +116,80 @@ describe("Tag Selector", function () {
yield item.saveTx(); yield item.saveTx();
yield promise; yield promise;
// Tag selector should show the new item's tag
assert.equal(getRegularTags().length, 1);
})
it("should add a tag when an item is added to a collection", function* () {
var promise, tagSelector;
// Add collection
promise = waitForTagSelector();
var collection = yield createDataObject('collection');
yield promise;
// Tag selector should be empty in new collection
assert.equal(getRegularTags().length, 0);
// Add item with tag to library root
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: 'C'
}
]);
promise = waitForTagSelector()
yield item.saveTx();
yield promise;
// Tag selector should still be empty in collection
assert.equal(getRegularTags().length, 0);
item.setCollections([collection.id]);
promise = waitForTagSelector();
yield item.saveTx();
yield promise;
// Tag selector should show the new item's tag // Tag selector should show the new item's tag
tagSelector = doc.getElementById('zotero-tag-selector'); tagSelector = doc.getElementById('zotero-tag-selector');
assert.equal(tagSelector.getVisible().length, 1); assert.equal(getRegularTags().length, 1);
})
it("shouldn't re-insert a new tag that matches an existing color", function* () {
var libraryID = Zotero.Libraries.userLibraryID;
/*// Remove all tags in library
var tags = yield Zotero.Tags.getAll(libraryID);
tags.forEach(function (tag) {
var tagID = yield Zotero.Tags.getID(tag);
yield Zotero.Tags.removeFromLibrary(libraryID, tagID);
});*/
// Add B and A as colored tags without any items
yield Zotero.Tags.setColor(libraryID, "B", '#990000');
yield Zotero.Tags.setColor(libraryID, "A", '#CC9933');
// Add A to an item to make it a real tag
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: "A"
}
]);
var promise = waitForTagSelector();
yield item.saveTx();
yield promise;
var tagSelector = doc.getElementById('zotero-tag-selector');
var tagElems = tagSelector.id('tags-box').childNodes;
// Make sure the colored tags are still in the right position
var tags = new Map();
for (let i = 0; i < tagElems.length; i++) {
tags.set(tagElems[i].textContent, tagElems[i].style.order);
}
assert.isBelow(parseInt(tags.get("B")), 0);
assert.isBelow(parseInt(tags.get("B")), parseInt(tags.get("A")));
}) })
it("should remove a tag when an item is removed from a collection", function* () { it("should remove a tag when an item is removed from a collection", function* () {
@ -91,13 +206,12 @@ describe("Tag Selector", function () {
} }
]); ]);
item.setCollections([collection.id]); item.setCollections([collection.id]);
promise = waitForTagSelector() promise = waitForTagSelector();
yield item.saveTx(); yield item.saveTx();
yield promise; yield promise;
// Tag selector should show the new item's tag // Tag selector should show the new item's tag
var tagSelector = doc.getElementById('zotero-tag-selector'); assert.equal(getRegularTags().length, 1);
assert.equal(tagSelector.getVisible().length, 1);
item.setCollections(); item.setCollections();
promise = waitForTagSelector(); promise = waitForTagSelector();
@ -105,8 +219,7 @@ describe("Tag Selector", function () {
yield promise; yield promise;
// Tag selector shouldn't show the removed item's tag // Tag selector shouldn't show the removed item's tag
tagSelector = doc.getElementById('zotero-tag-selector'); assert.equal(getRegularTags().length, 0);
assert.equal(tagSelector.getVisible().length, 0);
}) })
it("should remove a tag when an item in a collection is moved to the trash", function* () { it("should remove a tag when an item in a collection is moved to the trash", function* () {
@ -128,8 +241,7 @@ describe("Tag Selector", function () {
yield promise; yield promise;
// Tag selector should show the new item's tag // Tag selector should show the new item's tag
var tagSelector = doc.getElementById('zotero-tag-selector'); assert.equal(getRegularTags().length, 1);
assert.equal(tagSelector.getVisible().length, 1);
// Move item to trash // Move item to trash
item.deleted = true; item.deleted = true;
@ -138,8 +250,96 @@ describe("Tag Selector", function () {
yield promise; yield promise;
// Tag selector shouldn't show the deleted item's tag // Tag selector shouldn't show the deleted item's tag
tagSelector = doc.getElementById('zotero-tag-selector'); assert.equal(getRegularTags().length, 0);
assert.equal(tagSelector.getVisible().length, 0); })
it("should remove a tag when a tag is deleted for a library", function* () {
yield selectLibrary(win);
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: 'A'
}
]);
var promise = waitForTagSelector();
yield item.saveTx();
yield promise;
// Tag selector should show the new item's tag
assert.include(getRegularTags(), "A");
// Remove tag from library
promise = waitForTagSelector();
var dialogPromise = waitForDialog();
var tagSelector = doc.getElementById('zotero-tag-selector');
yield tagSelector.delete("A");
yield promise;
// Tag selector shouldn't show the deleted item's tag
assert.notInclude(getRegularTags(), "A");
}) })
}) })
describe("#rename()", function () {
it("should rename a tag and update the tag selector", function* () {
yield selectLibrary(win);
var tag = Zotero.Utilities.randomString();
var newTag = Zotero.Utilities.randomString();
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: tag
}
]);
var promise = waitForTagSelector();
yield item.saveTx();
yield promise;
var tagSelector = doc.getElementById('zotero-tag-selector');
promise = waitForTagSelector();
var promptPromise = waitForWindow("chrome://global/content/commonDialog.xul", function (dialog) {
dialog.document.getElementById('loginTextbox').value = newTag;
dialog.document.documentElement.acceptDialog();
})
yield tagSelector.rename(tag);
yield promise;
var tags = getRegularTags();
assert.include(tags, newTag);
})
})
describe("#_openColorPickerWindow()", function () {
it("should assign a color to a tag", function* () {
yield selectLibrary(win);
var tag = "b " + Zotero.Utilities.randomString();
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: "a"
},
{
tag: tag
}
]);
var promise = waitForTagSelector();
yield item.saveTx();
yield promise;
var tagSelector = doc.getElementById('zotero-tag-selector');
assert.include(getRegularTags(), "a");
var dialogPromise = waitForDialog(false, undefined, 'chrome://zotero/content/tagColorChooser.xul');
var tagSelectorPromise = waitForTagSelector();
yield tagSelector._openColorPickerWindow(tag);
yield dialogPromise;
yield tagSelectorPromise;
assert.include(getColoredTags(), tag);
assert.notInclude(getRegularTags(), tag);
})
});
}) })

View file

@ -8,7 +8,7 @@ describe("Zotero.Tags", function () {
item.addTag(tagName); item.addTag(tagName);
yield item.saveTx(); yield item.saveTx();
assert.typeOf(Zotero.Tags.getID(Zotero.Libraries.userLibraryID, tagName), "number"); assert.typeOf((yield Zotero.Tags.getID(tagName)), "number");
}) })
}) })
@ -19,32 +19,49 @@ describe("Zotero.Tags", function () {
item.addTag(tagName); item.addTag(tagName);
yield item.saveTx(); yield item.saveTx();
var tagID = Zotero.Tags.getID(Zotero.Libraries.userLibraryID, tagName); var libraryID = Zotero.Libraries.userLibraryID;
assert.equal(Zotero.Tags.getName(Zotero.Libraries.userLibraryID, tagID), tagName); var tagID = yield Zotero.Tags.getID(tagName);
assert.equal((yield Zotero.Tags.getName(tagID)), tagName);
})
})
describe("#removeFromLibrary()", function () {
it("should reload tags of associated items", function* () {
var libraryID = Zotero.Libraries.userLibraryID;
var tagName = Zotero.Utilities.randomString();
var item = createUnsavedDataObject('item');
item.addTag(tagName);
yield item.saveTx();
assert.lengthOf(item.getTags(), 1);
var tagID = yield Zotero.Tags.getID(tagName);
yield Zotero.Tags.removeFromLibrary(libraryID, tagID);
assert.lengthOf(item.getTags(), 0);
}) })
}) })
describe("#purge()", function () { describe("#purge()", function () {
it("should remove orphaned tags", function* () { it("should remove orphaned tags", function* () {
var libraryID = Zotero.Libraries.userLibraryID; var libraryID = Zotero.Libraries.userLibraryID;
var tagName = Zotero.Utilities.randomString(); var tagName = Zotero.Utilities.randomString();
var item = createUnsavedDataObject('item'); var item = createUnsavedDataObject('item');
item.addTag(tagName); item.addTag(tagName);
yield item.saveTx(); yield item.saveTx();
var tagID = Zotero.Tags.getID(libraryID, tagName); var tagID = yield Zotero.Tags.getID(tagName);
assert.typeOf(tagID, "number"); assert.typeOf(tagID, "number");
yield item.eraseTx(); yield item.eraseTx();
assert.equal(Zotero.Tags.getName(libraryID, tagID), tagName); assert.equal((yield Zotero.Tags.getName(tagID)), tagName);
yield Zotero.DB.executeTransaction(function* () { yield Zotero.DB.executeTransaction(function* () {
yield Zotero.Tags.purge(); yield Zotero.Tags.purge();
}); });
yield Zotero.Tags.load(libraryID); assert.isFalse(yield Zotero.Tags.getName(tagID));
assert.isFalse(Zotero.Tags.getName(libraryID, tagID));
}) })
}) })
}) })

125
test/tests/tagsboxTest.js Normal file
View file

@ -0,0 +1,125 @@
"use strict";
describe("Item Tags Box", function () {
var win, doc, collectionsView;
before(function* () {
win = yield loadZoteroPane();
doc = win.document;
// Wait for things to settle
yield Zotero.Promise.delay(100);
});
after(function () {
win.close();
});
function waitForTagsBox() {
var deferred = Zotero.Promise.defer();
var tagsbox = doc.getElementById('zotero-editpane-tags');
var onRefresh = function (event) {
tagsbox.removeEventListener('refresh', onRefresh);
deferred.resolve();
}
tagsbox.addEventListener('refresh', onRefresh);
return deferred.promise;
}
describe("#notify()", function () {
it("should update an existing tag on rename", function* () {
var tag = Zotero.Utilities.randomString();
var newTag = Zotero.Utilities.randomString();
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 0;
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: tag
}
]);
yield item.saveTx();
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 2;
yield waitForTagsBox();
var tagsbox = doc.getElementById('zotero-editpane-tags');
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
assert.equal(rows.length, 1);
assert.equal(rows[0].textContent, tag);
yield Zotero.Tags.rename(Zotero.Libraries.userLibraryID, tag, newTag);
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
assert.equal(rows.length, 1);
assert.equal(rows[0].textContent, newTag);
})
it("should update when a tag's color is removed", function* () {
var libraryID = Zotero.Libraries.userLibraryID;
var tag = Zotero.Utilities.randomString();
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 0;
yield Zotero.Tags.setColor(libraryID, tag, "#990000");
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: tag,
},
{
tag: "_A"
}
]);
yield item.saveTx();
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 2;
yield waitForTagsBox();
var tagsbox = doc.getElementById('zotero-editpane-tags');
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
// Colored tags aren't sorted first, for now
assert.notOk(rows[0].getElementsByTagName('label')[0].style.color);
assert.ok(rows[1].getElementsByTagName('label')[0].style.color);
assert.equal(rows[0].textContent, "_A");
assert.equal(rows[1].textContent, tag);
yield Zotero.Tags.setColor(libraryID, tag, false);
assert.notOk(rows[1].getElementsByTagName('label')[0].style.color);
})
it("should update when a tag is removed from the library", function* () {
var tag = Zotero.Utilities.randomString();
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 0;
var item = createUnsavedDataObject('item');
item.setTags([
{
tag: tag
}
]);
yield item.saveTx();
var tabbox = doc.getElementById('zotero-view-tabbox');
tabbox.selectedIndex = 2;
yield waitForTagsBox();
var tagsbox = doc.getElementById('zotero-editpane-tags');
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
assert.equal(rows.length, 1);
assert.equal(rows[0].textContent, tag);
yield Zotero.Tags.removeFromLibrary(
Zotero.Libraries.userLibraryID, (yield Zotero.Tags.getID(tag))
);
var rows = tagsbox.id('tagRows').getElementsByTagName('row');
assert.equal(rows.length, 0);
})
})
})