<?xml version="1.0"?>
<!--
    ***** BEGIN LICENSE BLOCK *****
    
    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org
    
    This file is part of Zotero.
    
    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    
    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
    
    ***** END LICENSE BLOCK *****
-->
<?xml-stylesheet href="chrome://zotero/skin/overlay.css" type="text/css"?>
<!DOCTYPE bindings SYSTEM "chrome://zotero/locale/zotero.dtd">

<bindings xmlns="http://www.mozilla.org/xbl"
		  xmlns:xbl="http://www.mozilla.org/xbl">

	<binding id="tag-selector">
		<resources>
			<stylesheet src="chrome://zotero/skin/bindings/tagselector.css"/>
			<stylesheet src="chrome://zotero-platform/content/tagselector.css"/>
		</resources>
		
		<implementation>
			<field name="collectionTreeRow"/>
			<field name="updateScope"/>
			<field name="selection"/>
			<field name="onchange"/>
			
			<field name="_initialized">false</field>
			<field name="_notifierID">false</field>
			<field name="_tags">null</field>
			<field name="_popupNode"/>
			<field name="_dirty">null</field>
			<field name="_emptyRegular">null</field>
			
			<!-- Modes are predefined settings groups for particular tasks -->
			<field name="_mode">"view"</field>
			<property name="mode" onget="return this._mode;">
				<setter>
				<![CDATA[
					this.clickable = false;
					this.editable = false;
					
					switch (val) {
						case 'view':
							break;
						
						case 'edit':
							this.clickable = true;
							this.editable = true;
							//this.clickHandler = this.showEditor;
							//this.blurHandler = this.hideEditor;
							break;
						
						default:
							throw ("Invalid mode '" + val + "' in tagselector.xml");
					}
					
					this._mode = val;
					document.getAnonymousNodes(this)[0].setAttribute('mode', val);
				]]>
				</setter>
			</property>
			
			<field name="_libraryID"/>
			<property name="libraryID" onget="return this._libraryID">
				<setter>
				<![CDATA[
					if (this._libraryID != val) {
						this._dirty = true;
					}
					this._libraryID = val;
				]]>
				</setter>
			</property>
			
			<property name="showAutomatic" onget="return this.getAttribute('showAutomatic') != 'false'"/>
			<property name="_types">
				<getter>
					<![CDATA[
					var types = [0];
					if (this.showAutomatic) {
						types.push(1);
					}
					return types;
					]]>
				</getter>
			</property>
			
			<field name="_search">null</field>
			<method name="setSearch">
				<parameter name="val"/>
				<parameter name="skipRefresh"/>
				<body>
				<![CDATA[
					this._search = val
						? val.toLowerCase().normalize()
						: false;
					
					if (!skipRefresh) {
						this.refresh();
					}
				]]>
				</body>
			</method>
			
			<field name="_hasScope">false</field>
			<field name="_scope">null</field>
			<property name="scope">
				<setter>
				<![CDATA[
					if (val.length) {
						this._hasScope = true;
						this._scope = new Map();
						for (let i=0; i<val.length; i++) {
							let tag = val[i];
							let types = this._scope.get(tag.tag);
							if (!types) {
								types = []
							}
							types.push(tag.type || 0);
							this._scope.set(tag.tag, types);
						}
					}
					else {
						this._hasScope = false;
						this._scope = new Map();
					}
					
					this.refresh();
				]]>
				</setter>
			</property>
			
			<property name="filterToScope">
				<getter>
				<![CDATA[
					// Default is to filter, so test for explicit 'false'
					return this.getAttribute('filterToScope') != 'false';
				]]>
				</getter>
				<setter>
				<![CDATA[
					this.setAttribute('filterToScope', val);
					this.refresh();
				]]>
				</setter>
			</property>
			
			<constructor>
				<![CDATA[
					this.id('show-automatic').setAttribute('checked', this.showAutomatic);
					this.id('display-all-tags').setAttribute('checked', !this.filterToScope);
					this.dragObserver = new this._dragObserverConstructor;
					
					//
					// Event listeners
					//
					var tagsBox = this.id('tags-box');
					
					tagsBox.addEventListener('click', function (event) {
						this.handleTagClick(event);
					}.bind(this));
					
					tagsBox.addEventListener('mousedown', function (event) {
						this.handleTagMouseDown(event);
					}.bind(this));
					
					tagsBox.addEventListener('dragover', this.dragObserver.onDragOver);
					tagsBox.addEventListener('dragexit', this.dragObserver.onDragExit);
					tagsBox.addEventListener('drop', this.dragObserver.onDrop, true);
				]]>
			</constructor>
			
			
			<method name="init">
				<body>
				<![CDATA[
					this._initialized = true;
					this.selection = new Set();
					this._notifierID = Zotero.Notifier.registerObserver(
						this,
						['collection-item', 'item', 'item-tag', 'tag', 'setting'],
						'tagSelector'
					);
				]]>
				</body>
			</method>
			
			
			<method name="uninit">
				<body>
				<![CDATA[
					if (!this._initialized) {
						return;
					}
					
					this._initialized = false;
					this.unregister();
					this.selection = new Set();
					if (this.onchange) {
						this.onchange();
					}
				]]>
				</body>
			</method>
			
			
			<method name="unregister">
				<body>
				<![CDATA[
					if (this._notifierID) {
						Zotero.Notifier.unregisterObserver(this._notifierID);
					}
				]]>
				</body>
			</method>
			
			
			<method name="refresh">
				<parameter name="fetch"/>
				<body>
				<![CDATA[
					return Zotero.spawn(function* () {
						var t = new Date;
						
						if (fetch || this._dirty) {
							Zotero.debug('Reloading tags selector');
						}
						else {
							Zotero.debug('Refreshing tags selector');
						}
						
						if (!this._initialized) {
							this.init();
							fetch = true;
						}
						
						this._emptyRegular = true;
						var tagsBox = this.id('tags-box');
						
						// If new data, rebuild buttons
						if (fetch || this._dirty) {
							this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types)
								.tap(() => Zotero.Promise.check(this.mode));
							
							let { div, emptyRegular } = this.createTagsList(this._tags);
							
							this._emptyRegular = emptyRegular;
							tagsBox.innerHTML = "";
							tagsBox.appendChild(div);
							this._dirty = false;
							this._tagsDiv = null;
						}
						// Otherwise just update based on visibility
						else {
							// If only a few tags, regenerate buttons from scratch
							if (this.filterToScope && this._scope.size <= 100) {
								// If full set is currently displayed, store it for later
								if (!this._tagsDiv) {
									this._tagsDiv = tagsBox.firstChild;
								}
								
								let tags = [];
								this._scope.forEach(function (types, name) {
									tags.push(...types.map(type => {
										return {
											tag: name,
											type
										};
									}));
								});
								let { div, emptyRegular } = this.createTagsList(tags);
								tagsBox.replaceChild(div, tagsBox.firstChild);
								this._emptyRegular = emptyRegular;
							}
							// Otherwise swap in the stored buttons from the last full run,
							// after updating their visibility
							else {
								let oldDiv = tagsBox.removeChild(tagsBox.firstChild);
								if (!this._tagsDiv) {
									this._tagsDiv = oldDiv;
								}
								
								let elems = this._tagsDiv.childNodes;
								let tagColors = Zotero.Tags.getColors(this.libraryID);
								for (let i = 0; i < elems.length; i++) {
									let elem = elems[i];
									let visible = this._updateClickableTag(
										elem, elem.textContent, tagColors
									);
									if (visible) {
										this._emptyRegular = false;
									}
								}
								tagsBox.appendChild(this._tagsDiv);
							}
						}
						
						//start tag cloud code
						
						var tagCloud = Zotero.Prefs.get('tagCloud');
						if (false && tagCloud) {
							var labels = tagsBox.getElementsByTagName('label');
						
							//loop through displayed labels and find number of linked items
							var numlinked= [];
							for (var i=0; i<labels.length; i++){
								if(labels[i].getAttribute("hidden") != 'true') {
									var tagIDs = labels[i].getAttribute('tagID').split('-');
									
									
									//replace getLinkedItems() with function that gets linked items within the current collection
									var linked = this._tags[tagIDs[0]].getLinkedItems();
									
									numlinked.push(parseInt(linked.length));
								}	
							}
							//
							numlinked.sort();
							
							//Get number of attached items from tag with fewest items
							var min = numlinked[0];
							
							//Get number of attached items from tag with most items
							var max = numlinked.pop();
							numlinked.push(max);
							
							//Create array of possible tag text sizes
							var sizes = ["11", "12", "13", "14", "15", "16", "17", "18", "19","20","21","22"];
							
							//Number of possible tag sizes
							var categories = sizes.length;
							
							//inc is the size of each size category of tags, in terms of the number of attached items
							var inc = Math.ceil((max-min)/categories);
							if(inc<1) {
								inc = 1;
							}
							
							for (var i=0; i<labels.length; i++){
								if(labels[i].getAttribute("hidden") != 'true') {
									var tagIDs = labels[i].getAttribute('tagID').split('-');
									
									
									//replace getLinkedItems() with function that gets linked items within the current collection
									var linked = this._tags[tagIDs[0]].getLinkedItems();
									
									numlink = linked.length;
									
									//range is the difference between how many items this tag has and how many items the smallest tag has
									var range=(numlink-min);
									
									//Divide the range by the size of the categories
									s=range/inc;
									
									if(s==categories) {
										s=categories-1;
									}
									var stylestr = 'font-size:'+sizes[s]+'px;';
									labels[i].setAttribute('style',stylestr);
								}
							}
						}
						
						//end tag cloud code
						
						this.updateNumSelected();
						// TODO: Show loading again when switching libraries/collections?
						this.id('tags-deck').selectedIndex = this._emptyRegular ? 1 : 2;
						
						if (this.onRefresh) {
							this.onRefresh();
							this.onRefresh = null;
						}
						
						Zotero.debug("Loaded tag selector in " + (new Date - t) + " ms");
						
						var event = new Event('refresh');
						this.dispatchEvent(event);
					}, this);
				]]>
				</body>
			</method>
			
			<method name="createTagsList">
				<parameter name="tags"/>
				<body><![CDATA[
					var tagColors = Zotero.Tags.getColors(this.libraryID);
					
					// Add colored tags that aren't already real tags
					var regularTags = new Set(tags.map(tag => tag.tag));
					[...new Set(tagColors.keys())].filter(x => !regularTags.has(x)).forEach((x) => {
						tags.push(Zotero.Tags.cleanData({ tag: x }));
					});
					
					// Sort by name
					var t = new Date();
					Zotero.debug("Sorting tags");
					var collation = Zotero.getLocaleCollation();
					tags.sort(function (a, b) {
						return collation.compareString(1, a.tag, b.tag);
					});
					Zotero.debug(`Sorted tags in ${new Date() - t} ms`);
					
					var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
					var emptyRegular = true;
					var lastTag;
					for (let i = 0; i < tags.length; i++) {
						let tagData = tags[i];
						
						// Only show tags of different types once
						if (tagData.tag === lastTag) {
							continue;
						}
						lastTag = tagData.tag;
						
						let elem = this._insertClickableTag(div, tagData);
						let visible = this._updateClickableTag(
							elem, tagData.tag, tagColors
						);
						if (visible) {
							emptyRegular = false;
						}
					}
					return { div, emptyRegular };
				]]>
				</body>
			</method>
			
			<method name="insertSorted">
				<parameter name="tagsList"/>
				<parameter name="tagObjs"/>
				<body><![CDATA[
					var tagColors = Zotero.Tags.getColors(this._libraryID);
					
					var collation = Zotero.getLocaleCollation();
					tagObjs.sort(function (a, b) {
						return collation.compareString(1, a.tag, b.tag);
					});
					
					// Create tag elements in sorted order
					var tagElems = tagsList.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;
							}
							if (comp < 0) {
								break;
							}
							j++;
						}
						this._insertClickableTag(tagsList, tagObj, tagElems[j]);
						this._updateClickableTag(
							tagElems[j], tagElems[j].textContent, tagColors
						);
					}
				]]></body>
			</method>
			
			
			<method name="getNumSelected">
				<body><![CDATA[
					return this.selection.size;
				]]></body>
			</method>
			
			
			<method name="updateNumSelected">
				<body>
				<![CDATA[
					var count = this.getNumSelected();
					
					switch (count) {
						case 0:
							var mod = 'none';
							break;
						case 1:
							var mod = 'singular';
							break;
						default:
							var mod = 'plural';
					}
					
					this.id('num-selected').label = Zotero.getString('pane.tagSelector.numSelected.' + mod, [count]);
					this.id('deselect-all').disabled = count == 0;
				]]>
				</body>
			</method>
			
			<field name="_notified"/>
			<method name="notify">
				<parameter name="event"/>
				<parameter name="type"/>
				<parameter name="ids"/>
				<parameter name="extraData"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						if (type == 'setting') {
							if (ids.some(val => val.split("/")[1] == 'tagColors')) {
								yield this.refresh(true);
							}
							return;
						}
						
						// Ignore anything other than deletes in duplicates view
						if (this.collectionTreeRow.isDuplicates()) {
							switch (event) {
								case 'delete':
								case 'trash':
									break;
								
								default:
									return;
							}
						}
						
						// Ignore item events other than 'trash'
						if (type == 'item' && event != 'trash') {
							return;
						}
						
						var selectionChanged = false;
						
						// If a selected tag no longer exists, deselect it
						if (event == 'delete' || event == 'trash' || event == 'modify') {
							// TODO: necessary, or just use notifier value?
							this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types);
							
							for (let tag of this.selection) {
								for (let tag2 of this._tags) {
									if (tag == tag2) {
										var found = true;
										break;
									}
								}
								if (!found) {
									this.selection.delete(tag);
									selectionChanged = true;
								}
							}
						}
						
						if (event == 'add') {
							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) {
									this.insertSorted(this.id('tags-box').firstChild, tagObjs);
									// If full set isn't currently displayed, update it too
									if (this._tagsDiv) {
										this.insertSorted(this._tagsDiv, tagObjs);
									}
								}
							}
							// Don't add anything for item or collection-item; just update scope
							
							return this.updateScope();
						}
						
						var t = this.id('tags-search').inputField;
						if (t.value) {
							this.setSearch(t.value, true);
						}
						else {
							this.setSearch(false, true);
						}
						this._dirty = true;
						
						// This is a hack, but set this to run after the refresh,
						// since _emptyRegular isn't set until then
						this.onRefresh = function () {
							// If no regular tags visible after a delete, deselect all.
							// This is necessary so that a selected tag that's removed
							// from its last item doesn't cause all regular tags to
							// disappear without anything being visibly selected.
							if ((event == 'remove' || event == 'delete') &&
									this._emptyRegular && this.getNumSelected()) {
								Zotero.debug('No tags visible after delete -- deselecting all');
								return this.clearAll();
							}
						}.bind(this);
						
						// If the selection changed, update the items list
						if (selectionChanged && this.onchange) {
							return this.onchange();
						}
						
						// Otherwise, just update the tag selector
						return this.updateScope();
					}, this);
				]]>
				</body>
			</method>
			
			
			<method name="deselectAll">
				<body><![CDATA[
					if (!this.selection || !this.selection.size) {
						return;
					}
					
					this.selection = new Set();
					
					var elems = this.id('tags-box').querySelectorAll("button[selected=true]");
					for (let i = 0; i < elems.length; i++) {
						let elem = elems[i];
						elem.setAttribute('selected', false);
						this.selection.delete(elem.textContent);
					}
					
					if (this.onchange) {
						this.onchange();
					}
				]]></body>
			</method>
			
			
			<method name="handleKeyPress">
				<parameter name="clear"/>
				<body>
					<![CDATA[
						var textbox = this.id('tags-search');
						var t = textbox.inputField;
						
						if (typeof clear != 'undefined') {
							if (clear){
								t.value = '';
								this.setSearch();
								return false;
							}
							else {
								return true;
							}
						}
						
						if (t.value) {
							this.setSearch(t.value);
						}
						else {
							this.setSearch();
						}
						return true;
					]]>
				</body>
			</method>
			
			
			<method name="handleTagClick">
				<parameter name="event"/>
				<body>
					<![CDATA[
						if (event.button != 0) {
							return;
						}
						
						var elem = event.target;
						
						// Ignore clicks not on tags
						if (elem.localName != 'button') {
							return;
						}
						
						// Ignore clicks on tags not in scope
						if (elem.getAttribute('inScope') == 'false') {
							return;
						}
						
						// Deselect
						if (elem.getAttribute('selected')=='true'){
							this.selection.delete(elem.textContent);
							elem.setAttribute('selected', 'false');
						}
						// Select
						else {
							this.selection.add(elem.textContent);
							elem.setAttribute('selected', 'true');
						}
						
						this.updateNumSelected();
						
						if (this.onchange) {
							this.onchange();
						}
						
						event.stopPropagation();
					]]>
				</body>
			</method>
			
			<method name="handleTagMouseDown">
				<parameter name="event"/>
				<body><![CDATA[
					if (event.button != 2) {
						return;
					}
					
					var elem = event.target;
					
					// Ignore clicks not on tags
					if (elem.localName != 'button') {
						return;
					}
					
					if (!this.editable) return;
					
					// Without the setTimeout, the popup gets immediately hidden for some reason
					setTimeout(function () {
						_popupNode = event.originalTarget;
						this.id('tag-menu').openPopup(
							null,
							'after_pointer',
							event.clientX + 2,
							event.clientY + 2,
							true,
							event
						);
					}.bind(this));
				]]></body>
			</method>
			
			<method name="rename">
				<parameter name="oldName"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
							.getService(Components.interfaces.nsIPromptService);
						
						var newName = { value: oldName };
						var result = promptService.prompt(window,
							Zotero.getString('pane.tagSelector.rename.title'),
							Zotero.getString('pane.tagSelector.rename.message'),
							newName, '', {});
						
						if (!result || !newName.value || oldName == newName.value) {
							return;
						}
						
						if (this.selection.has(oldName)) {
							var wasSelected = true;
							this.selection.delete(oldName);
						}
						
						if (Zotero.Tags.getID(oldName)) {
							yield Zotero.Tags.rename(this.libraryID, oldName, newName.value);
						}
						// Colored tags don't need to exist, so in that case
						// just rename the color setting
						else {
							let color = Zotero.Tags.getColor(this.libraryID, oldName);
							if (!color) {
								throw new Error("Can't rename missing tag");
							}
							
							yield Zotero.DB.executeTransaction(function* () {
								yield Zotero.Tags.setColor(this.libraryID, oldName, false);
								yield Zotero.Tags.setColor(
									this.libraryID,
									newName.value,
									color.color,
									color.position
								);
							}.bind(this));
						}
						
						if (wasSelected) {
							this.selection.add(newName.value);
						}
					}.bind(this));
				]]>
				</body>
			</method>
			
			
			<method name="deleteTag">
				<parameter name="name"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
							.getService(Components.interfaces.nsIPromptService);
						
						var confirmed = promptService.confirm(window,
							Zotero.getString('pane.tagSelector.delete.title'),
							Zotero.getString('pane.tagSelector.delete.message'));
						
						if (!confirmed) {
							return;
						}
						
						var tagID = Zotero.Tags.getID(name);
						if (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));
				]]></body>
			</method>
			
			
			<method name="_updateDeleteAutomaticMenuOption">
				<body><![CDATA[
					(async function () {
						var hasAutomatic = !!(await Zotero.Tags.getAutomaticInLibrary(this.libraryID)).length;
						var menuitem = this.id('delete-automatic-tags');
						menuitem.disabled = !hasAutomatic;
					}.bind(this))();
				]]></body>
			</method>
			
			
			<method name="_deleteAutomatic">
				<body><![CDATA[
					(async function () {
						var num = (await Zotero.Tags.getAutomaticInLibrary(this.libraryID)).length;
						if (!num) {
							return;
						}
						
						var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
							.getService(Components.interfaces.nsIPromptService);
						var confirmed = ps.confirm(
							window,
							Zotero.getString('pane.tagSelector.deleteAutomatic.title'),
							Zotero.getString(
									'pane.tagSelector.deleteAutomatic.message',
									new Intl.NumberFormat().format(num),
									num
								)
								+ "\n\n"
								+ Zotero.getString('general.actionCannotBeUndone')
						);
						if (confirmed) {
							Zotero.showZoteroPaneProgressMeter(null, true);
							try {
								await Zotero.Tags.removeAutomaticFromLibrary(
									this.libraryID,
									(progress, progressMax) => {
										Zotero.updateZoteroPaneProgressMeter(
											Math.round(progress / progressMax * 100)
										);
									}
								);
							}
							finally {
								Zotero.hideZoteroPaneOverlays();
							}
						}
					}.bind(this))();
				]]></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 name="_makeClickableTag">
				<parameter name="tagObj"/>
				<parameter name="editable"/>
				<body><![CDATA[
					var elem = document.createElementNS('http://www.w3.org/1999/xhtml', 'button');
					elem.textContent = tagObj.tag;
					if (tagObj.type) {
						elem.setAttribute('tagType', tagObj.type);
					}
					// Take the tags out of the tab order so that tabbing moves from the collections
					// pane to the items pane
					elem.setAttribute('tabindex', "-1");
					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.has(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');
					}
					
					return visible;
				]]></body>
			</method>
			
			
			<method name="_openColorPickerWindow">
				<parameter name="name"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						var io = {
							libraryID: this.libraryID,
							name: name
						};
						
						var tagColors = Zotero.Tags.getColors(this.libraryID);
						if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) {
							var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
								.getService(Components.interfaces.nsIPromptService);
							ps.alert(null, "", Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS));
							return;
						}
						
						io.tagColors = tagColors;
						
						window.openDialog(
							'chrome://zotero/content/tagColorChooser.xul',
							"zotero-tagSelector-colorChooser",
							"chrome,modal,centerscreen", io
						);
						
						// Dialog cancel
						if (typeof io.color == 'undefined') {
							return;
						}
						
						yield Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position);
					}.bind(this));
				]]>
				</body>
			</method>
			
			
			<method name="focusTextbox">
				<body>
					<![CDATA[
						this.id('tags-search').focus();
					]]>
				</body>
			</method>
			
			
			<property name="dragObserver"/>
			<method name="_dragObserverConstructor">
				<body>
				<![CDATA[
					this.onDragOver = function (event) {
						//Zotero.debug(event.dataTransfer.mozTypesAt(0));
						if (!event.dataTransfer.getData('zotero/item')) {
							return;
						}
						
						/*
						// TODO: get drop data
						var ids = dropData.data.split(',');
						var items = Zotero.Items.get(ids);
						for (var i=0; i<items.length; i++) {
							if (!Zotero.Items.isEditable(items[i])) {
								return true;
							}
						}
						*/
						
						var elem = event.target;
						
						// Ignore drops not on tags
						if (elem.localName != 'button') {
							return;
						}
						
						// Store the event, because drop event does not have shiftKey attribute set
						Zotero.DragDrop.currentEvent = event;
						elem.setAttribute('draggedOver', true);
						event.preventDefault();
						event.dataTransfer.dropEffect = "copy";
					}
					
					this.onDragExit = function (event) {
						Zotero.DragDrop.currentEvent = null;
						event.target.setAttribute('draggedOver', false);
					}
					
					
					this.onDrop = Zotero.Promise.method(function (event) {
						var elem = event.target;
						
						// Ignore drops not on tags
						if (elem.localName != 'button') {
							return;
						}
						
						elem.setAttribute('draggedOver', false);
						
						var dt = event.dataTransfer;
						var ids = dt.getData('zotero/item');
						if (!ids) {
							return;
						}
						
						return Zotero.DB.executeTransaction(function* () {
							ids = ids.split(',');
							var items = Zotero.Items.get(ids);
							var value = elem.textContent
							
							for (let i=0; i<items.length; i++) {
								let item = items[i];
								if (Zotero.DragDrop.currentEvent.shiftKey) {
									item.removeTag(value);
								} else {
									item.addTag(value);
								}
								yield item.save();
							}
						}.bind(this));
					});
				]]>
				</body>
			</method>
			
			
			<method name="id">
				<parameter name="id"/>
				<body>
					<![CDATA[
						return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
					]]>
				</body>
			</method>
		</implementation>
		
		<content>
			<groupbox flex="1"
					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;"
						oncommand="_openColorPickerWindow(_popupNode.textContent); event.stopPropagation()"/>
					<menuitem label="&zotero.tagSelector.renameTag;"
						oncommand="document.getBindingParent(this).rename(_popupNode.textContent); event.stopPropagation()"/>
					<menuitem label="&zotero.tagSelector.deleteTag;"
						oncommand="document.getBindingParent(this).deleteTag(_popupNode.textContent); event.stopPropagation()"/>
				</menupopup>
				
				<deck id="tags-deck">
					<box id="loading-box">
						<label value="&zotero.tagSelector.loadingTags;"/>
					</box>
					
					<box id="no-tags-box">
						<label value="&zotero.tagSelector.noTagsToDisplay;"/>
					</box>
					
					<html:div id="tags-box" tabindex="-1"/>
				</deck>
				
				<vbox id="tag-controls">
					<hbox>
						<textbox id="tags-search" flex="1" type="search" timeout="250" dir="reverse"
							oncommand="document.getBindingParent(this).handleKeyPress(); event.stopPropagation()"
							onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) { document.getBindingParent(this).handleKeyPress(true); }"
							tabindex="-1"/>
						<toolbarbutton id="view-settings-menu" tooltiptext="&zotero.toolbar.actions.label;"
								image="chrome://zotero/skin/tag-selector-menu.png" type="menu">
							<menupopup id="view-settings-popup"
									onpopupshowing="document.getBindingParent(this)._updateDeleteAutomaticMenuOption()"
									onpopupshown="/*
											This is necessary to fix a bug with Display All Tags not
											being checked if enabled before menuu is shown (OS X only?)
										*/
										document.getElementById('show-automatic').setAttribute('checked', document.getBindingParent(this).showAutomatic);
										document.getElementById('display-all-tags').setAttribute('checked', !document.getBindingParent(this).filterToScope);">
								<menuitem id="num-selected" disabled="true"/>
								<menuitem id="deselect-all" label="&zotero.tagSelector.clearAll;"
									oncommand="document.getBindingParent(this).deselectAll(); event.stopPropagation();"/>
								<menuseparator/>
								<menuitem id="show-automatic" label="&zotero.tagSelector.showAutomatic;" type="checkbox"
									oncommand="var ts = document.getBindingParent(this);
										ts._dirty = true;
										var showAutomatic = this.getAttribute('checked') == 'true';
										ts.setAttribute('showAutomatic', showAutomatic);
										this.setAttribute('checked', showAutomatic);
										ts.refresh();
										event.stopPropagation();"/>
								<menuitem id="display-all-tags" label="&zotero.tagSelector.displayAllInLibrary;" type="checkbox"
									oncommand="var displayAll = this.getAttribute('checked') == 'true';
										this.setAttribute('checked', !displayAll);
										document.getBindingParent(this).filterToScope = !displayAll;
										event.stopPropagation();"/>
								<menuseparator/>
								<menuitem id="delete-automatic-tags" label="&zotero.tagSelector.deleteAutomaticInLibrary;" type="checkbox"
									oncommand="document.getBindingParent(this)._deleteAutomatic();
										this.setAttribute('checked', false);
										event.stopPropagation();"/>
							</menupopup>
						</toolbarbutton>
					</hbox>
				</vbox>
			</groupbox>
		</content>
	</binding>
</bindings>
