diff --git a/chrome/content/zotero/components/tagSelector.jsx b/chrome/content/zotero/components/tagSelector.jsx
index a2059effec..93994fd786 100644
--- a/chrome/content/zotero/components/tagSelector.jsx
+++ b/chrome/content/zotero/components/tagSelector.jsx
@@ -49,6 +49,7 @@ class TagSelector extends React.PureComponent {
fontSize={this.props.fontSize}
lineHeight={this.props.lineHeight}
uiDensity={this.props.uiDensity}
+ excludeTag={this.props.excludeTag}
/>
@@ -90,6 +91,7 @@ TagSelector.propTypes = {
}),
onSelect: PropTypes.func,
onKeyDown: PropTypes.func,
+ excludeTag: PropTypes.func,
onTagContext: PropTypes.func,
loaded: PropTypes.bool,
width: PropTypes.number.isRequired,
diff --git a/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx b/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx
index b2d9a64e60..a069b214c6 100644
--- a/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx
+++ b/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx
@@ -175,6 +175,9 @@ class TagList extends React.PureComponent {
if (tag.disabled) {
className += ' disabled';
}
+ if (tag.excluded) {
+ className += ' excluded';
+ }
if (Zotero.Utilities.Internal.containsEmoji(tag.name)) {
className += ' emoji';
}
@@ -199,6 +202,13 @@ class TagList extends React.PureComponent {
props.role = "checkbox";
props['aria-checked'] = tag.selected;
props['aria-disabled'] = tag.disabled;
+ // Add a note to excluded tags to differentiate them from just selected
+ if (tag.excluded) {
+ props['data-l10n-id'] = "tag-excluded";
+ }
+ else {
+ props['aria-description'] = '';
+ }
// Don't specify explicit width unless we're truncating, because for some reason the width
// from canvas can sometimes be slightly smaller than the actual width, resulting in an
// unnecessary ellipsis.
@@ -305,6 +315,11 @@ class TagList extends React.PureComponent {
};
async handleKeyDown(e) {
+ if (e.key == "Backspace") {
+ let tag = e.target.textContent;
+ this.props.excludeTag(tag);
+ return;
+ }
if (!["ArrowRight", "ArrowLeft"].includes(e.key)) return;
// If the windowing kicks in, the node of the initially-focused tag may not
// exist, so first we may need to scroll to it.
@@ -393,6 +408,7 @@ class TagList extends React.PureComponent {
onSelect: PropTypes.func,
onKeyDown: PropTypes.func,
onTagContext: PropTypes.func,
+ excludeTag: PropTypes.func,
loaded: PropTypes.bool,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
diff --git a/chrome/content/zotero/containers/tagSelectorContainer.jsx b/chrome/content/zotero/containers/tagSelectorContainer.jsx
index 0ebc5b575b..a4839e8e9c 100644
--- a/chrome/content/zotero/containers/tagSelectorContainer.jsx
+++ b/chrome/content/zotero/containers/tagSelectorContainer.jsx
@@ -65,7 +65,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
// Not stored in state to avoid an unnecessary refresh. Instead, when a tag is selected, we
// trigger the selection handler, which updates the visible items, which triggers
// onItemViewChanged(), which triggers a refresh with the new tags.
- this.selectedTags = new Set();
+ this.selectedTags = new Map();
this.widths = new Map();
this.widthsBold = new Map();
@@ -322,7 +322,14 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
if (this.displayAllTags) {
tags = await Zotero.Tags.getAll(this.libraryID);
}
-
+ else {
+ // Make sure that excluded tags do not disappear
+ for (let [tagName, tagProps] of this.selectedTags.entries()) {
+ if (tagProps.excluded) {
+ tags.push({ tag: tagName })
+ }
+ }
+ }
// If tags haven't changed, return previous array without sorting again
if (this.state.tags.length == tags.length) {
let prevTags = new Set(this.state.tags.map(tag => tag.tag));
@@ -513,8 +520,10 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
name,
width: tag.width
};
- if (this.selectedTags.has(name)) {
+ let selectedTag = this.selectedTags.get(name)
+ if (selectedTag) {
tag.selected = true;
+ tag.excluded = selectedTag.excluded;
}
if (inTagColors && tagColors.has(name)) {
tag.color = tagColors.get(name).color;
@@ -524,7 +533,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
}
// If we're not displaying all tags, we only need to check the scope for colored tags,
// since everything else will be in scope
- if ((this.displayAllTags || inTagColors) && !this.state.scope.has(name)) {
+ // A tag excluded from search results is not in scope but is part of selected tags so it remains enabled
+ if ((this.displayAllTags || inTagColors) && !(this.state.scope.has(name) || this.selectedTags.get(name))) {
tag.disabled = true;
}
const forceUseDOM = this.state.isHighDensity && i < FORCE_DOM_TAGS_FOR_COUNT;
@@ -543,6 +553,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
dragObserver={this.dragObserver}
onSelect={this.handleTagSelected}
onTagContext={this.handleTagContext}
+ excludeTag={this.excludeTag.bind(this)}
onSearch={this.handleSearch}
onSettings={this.handleSettings.bind(this)}
loaded={this.state.loaded}
@@ -572,6 +583,8 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
true
);
this.contextTag = tag;
+ // Disable "Exclude tag" for disabled tags
+ tagContextMenu.querySelector("#tag-menu-exclude-tag").disabled = tag.disabled;
}
handleSettings = (ev) => {
@@ -585,7 +598,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
if(selectedTags.has(tag)) {
selectedTags.delete(tag);
} else {
- selectedTags.add(tag);
+ selectedTags.set(tag, {});
}
if (typeof(this.props.onSelection) === 'function') {
@@ -662,7 +675,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
}
clearTagSelection() {
- this.selectedTags = new Set();
+ this.selectedTags = new Map();
}
async openColorPickerWindow() {
@@ -749,8 +762,9 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
let selectedTags = this.selectedTags;
if (selectedTags.has(this.contextTag.name)) {
+ let excluded = selectedTags.get(this.contextTag.name).excluded;
selectedTags.delete(this.contextTag.name);
- selectedTags.add(newName.value);
+ selectedTags.set(newName.value, { excluded });
}
if (Zotero.Tags.getID(this.contextTag.name)) {
@@ -790,6 +804,17 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
}
}
+ // Inverse of selection for tags. Items with the specified tag do not match search query.
+ async excludeTag(tag) {
+ if (!tag) {
+ tag = this.contextTag.name;
+ }
+ this.selectedTags.set(tag, { excluded: true })
+ if (typeof(this.props.onSelection) === 'function') {
+ this.props.onSelection(this.selectedTags);
+ }
+ }
+
async toggleDisplayAllTags(newValue) {
newValue = typeof(newValue) === 'undefined' ? !this.displayAllTags : newValue;
Zotero.Prefs.set('tagSelector.displayAllTags', newValue);
@@ -804,7 +829,7 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
}
deselectAll() {
- this.selectedTags = new Set();
+ this.selectedTags = new Map();
if('onSelection' in this.props && typeof(this.props.onSelection) === 'function') {
this.props.onSelection(this.selectedTags);
}
diff --git a/chrome/content/zotero/xpcom/collectionTreeRow.js b/chrome/content/zotero/xpcom/collectionTreeRow.js
index 98da6b9571..07e0faaa98 100644
--- a/chrome/content/zotero/xpcom/collectionTreeRow.js
+++ b/chrome/content/zotero/xpcom/collectionTreeRow.js
@@ -434,8 +434,15 @@ Zotero.CollectionTreeRow.prototype.getSearchObject = Zotero.Promise.coroutine(fu
}
if (this.tags){
- for (let tag of this.tags) {
- s2.addCondition('tag', 'is', tag);
+ for (let [tagName, tagProps] of this.tags.entries()) {
+ if (tagProps.excluded) {
+ s2.addCondition('tag', 'isNot', tagName);
+ // When an item with a tag is excluded, have its children excluded as well
+ s2.addCondition('includeChildren', 'true');
+ }
+ else {
+ s2.addCondition('tag', 'is', tagName);
+ }
}
}
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index 08103a5c49..533eeea0e7 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -1025,6 +1025,9 @@
oncommand="ZoteroPane.tagSelector.openTagSplitterWindow(event)"/>
+
+