<?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 *****
-->

<!DOCTYPE bindings SYSTEM "chrome://zotero/locale/zotero.dtd">

<bindings 	xmlns="http://www.mozilla.org/xbl"
			xmlns:xbl="http://www.mozilla.org/xbl"
			xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
	<binding id="tags-box">
		<resources>
			<stylesheet src="chrome://zotero/skin/bindings/tagsbox.css"/>
		</resources>
		
		<implementation>
			<field name="clickHandler"/>
			
			<field name="_lastTabIndex">false</field>
			<field name="_tabDirection"/>
			<field name="_tagColors"/>
			<field name="_notifierID"/>
			
			<!-- 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':
						case 'merge':
						case 'mergeedit':
							break;
						
						case 'edit':
							this.clickable = true;
							this.editable = true;
							this.clickHandler = this.showEditor;
							this.blurHandler = this.hideEditor;
							break;
						
						default:
							throw ("Invalid mode '" + val + "' in tagsbox.xml");
					}
					
					this._mode = val;
					document.getAnonymousNodes(this)[0].setAttribute('mode', val);
				]]>
				</setter>
			</property>
			
			<field name="_item"/>
			<property name="item" onget="return this._item;">
				<setter>
					<![CDATA[
						// Don't reload if item hasn't changed
						if (this._item == val) {
							return;
						}
						this._item = val;
						this.reload();
					]]>
				</setter>
			</property>
			
			<property name="count"/>
			
			<property name="summary">
				<getter><![CDATA[
					return Zotero.spawn(function* () {
						var r = "";
						
						if (this.item) {
							yield this.item.loadTags();
							var tags = this.item.getTags();
							if (tags) {
								for(var i = 0; i < tags.length; i++)
								{
									r = r + tags[i].tag + ", ";
								}
								r = r.substr(0,r.length-2);
							}
						}
					
						return r;
					}, this);
				]]></getter>
			</property>
			
			<constructor>
			<![CDATA[
				if (this.hasAttribute('mode')) {
					this.mode = this.getAttribute('mode');
				}
				
				this._notifierID = Zotero.Notifier.registerObserver(this, ['item-tag', 'setting']);
			]]>
			</constructor>
			
			
			<destructor>
			<![CDATA[
				Zotero.Notifier.unregisterObserver(this._notifierID);
			]]>
			</destructor>
			
			
			<method name="notify">
				<parameter name="event"/>
				<parameter name="type"/>
				<parameter name="ids"/>
				<parameter name="extraData"/>
				<body>
				<![CDATA[
					if (type == 'setting') {
						if (ids.some(function (val) val.split("/")[1] == 'tagColors') && this.item) {
							this.reload();
						}
						return;
					}
					else if (type == 'item-tag') {
						let itemID, tagName;
						
						for (let i=0; i<ids.length; i++) {
							[itemID, tagName] = ids[i].match(/^([0-9]+)-(.+)/).slice(1);
							if (!this.item || itemID != this.item.id) {
								continue;
							}
							
							if (event == 'add') {
								var newTabIndex = this.add(tagName);
								if (newTabIndex == -1) {
									return;
								}
								if (this._tabDirection == -1) {
									if (this._lastTabIndex > newTabIndex) {
										this._lastTabIndex++;
									}
								}
								else if (this._tabDirection == 1) {
									if (this._lastTabIndex > newTabIndex) {
										this._lastTabIndex++;
									}
								}
							}
							else if (event == 'modify') {
								Zotero.debug("EXTRA");
								Zotero.debug(extraData);
								let oldTagName = extraData[tagName].old.tag;
								this.remove(oldTagName);
								this.add(tagName);
							}
							else if (event == 'remove') {
								var oldTabIndex = this.remove(tagName);
								if (oldTabIndex == -1) {
									return;
								}
								if (this._tabDirection == -1) {
									if (this._lastTabIndex > oldTabIndex) {
										this._lastTabIndex--;
									}
								}
								else if (this._tabDirection == 1) {
									if (this._lastTabIndex >= oldTabIndex) {
										this._lastTabIndex--;
									}
								}
							}
						}
						
						this.updateCount();
					}
					else if (type == 'tag') {
						if (event == 'modify') {
							this.reload();
						}
					}
				]]>
				</body>
			</method>
			
			
			<method name="reload">
				<body><![CDATA[
					return Zotero.spawn(function* () {
						Zotero.debug('Reloading tags box');
						
						yield this.item.loadTags();
						
						// Cancel field focusing while we're updating
						this._reloading = true;
						
						this.id('addButton').hidden = !this.editable;
						
						this._tagColors = yield Zotero.Tags.getColors(this.item.libraryID)
						
						var rows = this.id('tagRows');
						while(rows.hasChildNodes()) {
							rows.removeChild(rows.firstChild);
						}
						var tags = this.item.getTags();
						
						// Sort tags alphabetically
						var collation = Zotero.getLocaleCollation();
						tags.sort(function (a, b) collation.compareString(1, a.tag, b.tag));
						
						for (let i=0; i<tags.length; i++) {
							this.addDynamicRow(tags[i], i+1);
						}
						this.updateCount(tags.length);
						
						this._reloading = false;
						this._focusField();
					}, this);
				]]></body>
			</method>
			
			
			<method name="addDynamicRow">
				<parameter name="tagData"/>
				<parameter name="tabindex"/>
				<parameter name="skipAppend"/>
				<body>
					<![CDATA[
						var isNew = !tagData;
						var name = tagData ? tagData.tag : "";
						var type = tagData ? tagData.type : 0;
						
						if (!tabindex) {
							tabindex = this.id('tagRows').childNodes.length + 1;
						}
						
						var icon = document.createElement("image");
						icon.className = "zotero-box-icon";
						
						// DEBUG: Why won't just this.nextSibling.blur() work?
						icon.setAttribute('onclick','if (this.nextSibling.inputField){ this.nextSibling.inputField.blur() }');
						
						var label = this.createValueElement(name, tabindex);
						
						if (this.editable) {
							var remove = document.createElement("label");
							remove.setAttribute('value','-');
							remove.setAttribute('class','zotero-clicky zotero-clicky-minus');
							remove.setAttribute('tabindex', -1);
						}
						
						var row = document.createElement("row");
						if (isNew) {
							row.setAttribute('isNew', true);
						}
						row.appendChild(icon);
						row.appendChild(label);
						if (this.editable) {
							row.appendChild(remove);
						}
						
						this.updateRow(row, tagData);
						
						if (!skipAppend) {
							this.id('tagRows').appendChild(row);
						}
						
						return row;
					]]>
				</body>
			</method>
			
			
			<!--
				Update various attributes of a row to match the given tag
				and current editability
			-->
			<method name="updateRow">
				<parameter name="row"/>
				<parameter name="tagData"/>
				<body><![CDATA[
					var tagName = tagData ? tagData.tag : "";
					var tagType = (tagData && tagData.type) ? tagData.type : 0;
					
					var icon = row.firstChild;
					var label = row.firstChild.nextSibling;
					if (this.editable) {
						var remove = row.lastChild;
					}
					
					// Row
					row.setAttribute('tagName', tagName);
					row.setAttribute('tagType', tagType);
					
					// Icon
					var iconFile = 'tag';
					if (!tagData || tagType == 0) {
						icon.setAttribute('tooltiptext', Zotero.getString('pane.item.tags.icon.user'));
					}
					else if (tagType == 1) {
						iconFile += '-automatic';
						icon.setAttribute('tooltiptext', Zotero.getString('pane.item.tags.icon.automatic'));
					}
					icon.setAttribute('src', 'chrome://zotero/skin/' + iconFile + '.png');
					
					// "-" button
					if (this.editable) {
						remove.setAttribute('disabled', false);
						var self = this;
						remove.addEventListener('click', function () {
							Zotero.spawn(function* () {
								self._lastTabIndex = false;
								if (tagData) {
									let item = document.getBindingParent(this).item
									item.removeTag(tagName);
									yield item.saveTx()
								}
								
								// Return focus to items pane
								var tree = document.getElementById('zotero-items-tree');
								if (tree) {
									tree.focus();
								}
							}.bind(this));
						});
					}
				]]></body>
			</method>
			
			
			<method name="createValueElement">
				<parameter name="valueText"/>
				<parameter name="tabindex"/>
				<body>
				<![CDATA[
					var valueElement = document.createElement("label");
					valueElement.setAttribute('fieldname', 'tag');
					valueElement.setAttribute('flex', 1);
					valueElement.className = 'zotero-box-label';
					
					if (this.clickable) {
						if (tabindex) {
							valueElement.setAttribute('ztabindex', tabindex);
						}
						valueElement.addEventListener('click', function (event) {
							/* Skip right-click on Windows */
							if (event.button) {
								return;
							}
							document.getBindingParent(this).clickHandler(this, 1, valueText);
						}, false);
						valueElement.className += ' zotero-clicky';
					}
					
					var firstSpace;
					if (typeof valueText == 'string') {
						firstSpace = valueText.indexOf(" ");
					}
					
					// 29 == arbitrary length at which to chop uninterrupted text
					if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29) {
						valueElement.setAttribute('crop', 'end');
						valueElement.setAttribute('value',valueText);
					}
					else {
						// Wrap to multiple lines
						valueElement.appendChild(document.createTextNode(valueText));
					}
					
					// Tag color
					let color = this._tagColors[valueText];
					if (color) {
						valueElement.setAttribute(
							'style',
							'color:' + this._tagColors[valueText].color + '; '
								+ 'font-weight: bold'
						);
					}
					
					return valueElement;
				]]>
				</body>
			</method>
			
			
			<method name="showEditor">
				<parameter name="elem"/>
				<parameter name="rows"/>
				<parameter name="value"/>
				<body><![CDATA[
					// Blur any active fields
					/*
					if (this._dynamicFields) {
						this._dynamicFields.focus();
					}
					*/
					
					Zotero.debug('Showing editor');
					
					var fieldName = 'tag';
					var tabindex = elem.getAttribute('ztabindex');
					
					var itemID = Zotero.getAncestorByTagName(elem, 'tagsbox').item.id;
					
					var t = document.createElement("textbox");
					t.setAttribute('value', value);
					t.setAttribute('fieldname', fieldName);
					t.setAttribute('ztabindex', tabindex);
					t.setAttribute('flex', '1');
					t.setAttribute('newlines','pasteintact');
					// Multi-line
					if (rows > 1) {
						t.setAttribute('multiline', true);
						t.setAttribute('rows', rows);
					}
					// Add auto-complete
					else {
						t.setAttribute('type', 'autocomplete');
						t.setAttribute('autocompletesearch', 'zotero');
						let params = {
							fieldName: fieldName,
							libraryID: this.item.libraryID
						};
						params.itemID = itemID ? itemID : '';
						t.setAttribute(
							'autocompletesearchparam', JSON.stringify(params)
						);
						t.setAttribute('completeselectedindex', true);
					}
					
					var box = elem.parentNode;
					box.replaceChild(t, elem);
					
					t.setAttribute('onblur', "return document.getBindingParent(this).blurHandler(this)");
					t.setAttribute('onkeypress', "return document.getBindingParent(this).handleKeyPress(event)");
					t.setAttribute('onpaste', "return document.getBindingParent(this).handlePaste(event)");
					
					this._tabDirection = false;
					this._lastTabIndex = tabindex;
					
					// Prevent error when clicking between a changed field
					// and another -- there's probably a better way
					if (!t.select) {
						return;
					}
					t.select();
					
					return t;
				]]></body>
			</method>
			
			
			<method name="handleKeyPress">
				<parameter name="event"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						var target = event.target;
						var focused = document.commandDispatcher.focusedElement;
						
						switch (event.keyCode) {
							case event.DOM_VK_RETURN:
								var multiline = target.getAttribute('multiline');
								var empty = target.value == "";
								if (event.shiftKey) {
									if (!multiline) {
										var self = this;
										setTimeout(function () {
											var val = target.value;
											if (val !== "") {
												val += "\n";
											}
											self.makeMultiline(target, val, 6);
										}, 0);
										return false;
									}
									// Submit
								}
								else if (multiline) {
									return true;
								}
								
								var fieldname = 'tag';
								
								var row = Zotero.getAncestorByTagName(target, 'row');
								
								// If non-empty last row, add new row
								if (row == row.parentNode.lastChild && !empty) {
									var focusField = true;
									this._tabDirection = 1;
								}
								// If empty non-last row, refocus current row
								else if (row != row.parentNode.lastChild && empty) {
									var focusField = true;
								}
								// If non-empty non-last row, return focus to items pane
								else {
									var focusField = false;
									this._lastTabIndex = false;
								}
								
								target.onblur = null;
								yield this.blurHandler(target);
								
								if (focusField) {
									Zotero.debug("FOCUSING FIELD");
									this._focusField();
								}
								// Return focus to items pane
								else {
									Zotero.debug("FOCUSING ITEM PANE");
									var tree = document.getElementById('zotero-items-tree');
									if (tree) {
										tree.focus();
									}
								}
								
								return false;
								
							case event.DOM_VK_ESCAPE:
								// Reset field to original value
								target.value = target.getAttribute('value');
								
								var tagsbox = Zotero.getAncestorByTagName(focused, 'tagsbox');
								
								this._lastTabIndex = false;
								target.onblur = null;
								yield this.blurHandler(target);
								
								if (tagsbox) {
									tagsbox.closePopup();
								}
								
								// Return focus to items pane
								var tree = document.getElementById('zotero-items-tree');
								if (tree) {
									tree.focus();
								}
								
								return false;
								
							case event.DOM_VK_TAB:
								// If already an empty last row, ignore forward tab
								if (target.value == "" && !event.shiftKey) {
									var row = Zotero.getAncestorByTagName(target, 'row');
									if (row == row.parentNode.lastChild) {
										return false;
									}
								}
								
								this._tabDirection = event.shiftKey ? -1 : 1;
								target.onblur = null;
								yield this.blurHandler(target);
								this._focusField();
								return false;
						}
						
						return true;
					}.bind(this));
				]]></body>
			</method>
			
			<!--
				Intercept paste, check for newlines, and convert textbox
				to multiline if necessary
			-->
			<method name="handlePaste">
				<parameter name="event"/>
				<body>
				<![CDATA[
					var textbox = event.target;
					
					var clip = Components.classes["@mozilla.org/widget/clipboard;1"]
						.getService(Components.interfaces.nsIClipboard);
					var trans = Components.classes["@mozilla.org/widget/transferable;1"]
						.createInstance(Components.interfaces.nsITransferable);
					trans.addDataFlavor("text/unicode");
					clip.getData(trans, clip.kGlobalClipboard);
					var str = {};
					try {
						trans.getTransferData("text/unicode", str, {});
						str = str.value.QueryInterface(Components.interfaces.nsISupportsString).data;
					}
					catch (e) {
						Zotero.debug(e);
						return true;
					}
					
					var multiline = !!str.trim().match(/\n/);
					if (multiline) {
						var self = this;
						setTimeout(function () {
							self.makeMultiline(textbox, str.trim());
						}, 0);
						// Cancel paste
						return false;
					}
					
					return true;
				]]>
				</body>
			</method>
			
			
			<method name="makeMultiline">
				<parameter name="textbox"/>
				<parameter name="value"/>
				<parameter name="rows"/>
				<body><![CDATA[
					// If rows not specified, use one more than lines in input
					if (!rows) {
						rows = value.match(/\n/g).length + 1;
					}
					textbox = this.showEditor(textbox, rows, textbox.getAttribute('value'));
					textbox.value = value;
					// Move cursor to end
					textbox.selectionStart = value.length;
				]]></body>
			</method>
			
			
			<method name="hideEditor">
				<parameter name="textbox"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						Zotero.debug('Hiding editor');
						
						var fieldName = 'tag';
						var tabindex = textbox.getAttribute('ztabindex');
						
						var oldValue = textbox.getAttribute('value');
						var value = textbox.value = textbox.value.trim();
						
						var tagsbox = Zotero.getAncestorByTagName(textbox, 'tagsbox');
						if (!tagsbox)
						{
							Zotero.debug('Tagsbox not found', 1);
							return;
						}
						
						var row = textbox.parentNode;
						var rows = row.parentNode;
						
						var isNew = row.getAttribute('isNew');
						
						// Remove empty row at end
						if (isNew && value === "") {
							row.parentNode.removeChild(row);
							return;
						}
						
						// If row hasn't changed, change back to label
						if (oldValue == value) {
							this.textboxToLabel(textbox);
							return;
						}
						
						var tags = value.split(/\r\n?|\n/).map(function (val) val.trim());
						
						// Modifying existing tag with a single new one
						if (!isNew && tags.length < 2) {
							if (value !== "") {
								if (oldValue !== value) {
									// The existing textbox will be removed in notify()
									this.item.replaceTag(oldValue, value);
									yield this.item.saveTx();
								}
							}
							// Existing tag cleared
							else {
								this.item.removeTag(oldValue);
								yield this.item.saveTx();
							}
						}
						// Multiple tags
						else if (tags.length > 1) {
							var lastTag = row == row.parentNode.lastChild;
							
							if (!isNew) {
								// If old tag isn't in array, remove it
								if (tags.indexOf(oldValue) == -1) {
									this.item.removeTag(oldValue);
								}
								// If old tag is staying, restore the textbox
								// immediately. This isn't strictly necessary, but it
								// makes the transition nicer.
								else {
									textbox.value = textbox.getAttribute('value');
									this.textboxToLabel(textbox);
								}
							}
							
							this.item.addTags(tags);
							yield this.item.saveTx();
							
							if (lastTag) {
								this._lastTabIndex = this.item.getTags().length;
							}
							
							yield this.reload();
						}
						// Single tag at end
						else {
							// Remove the textbox row. The new tag will be added in notify()
							// if it doesn't already exist.
							row.parentNode.removeChild(row);
							this.item.addTag(value);
							yield this.item.saveTx();
						}
					}.bind(this));
				]]></body>
			</method>
			
			
			<method name="new">
				<body>
					<![CDATA[
						var row = this.addDynamicRow();
						row.firstChild.nextSibling.click();
						return row;
					]]>
				</body>
			</method>
			
			
			<method name="textboxToLabel">
				<parameter name="textbox"/>
				<body><![CDATA[
					var elem = this.createValueElement(
						textbox.value, textbox.getAttribute('ztabindex')
					);
					var row = textbox.parentNode;
					row.replaceChild(elem, textbox);
				]]></body>
			</method>
			
			
			<method name="add">
				<parameter name="tagName"/>
				<body><![CDATA[
					Zotero.debug("ADDING ROW WITH " + tagName);
					
					var rowsElement = this.id('tagRows');
					var rows = rowsElement.childNodes;
					
					// Get this tag's existing row, if there is one
					var row = false;
					for (let i=0; i<rows.length; i++) {
						if (rows[i].getAttribute('tagName') === tagName) {
							Zotero.debug("FOUND ROW with " + tagName);
							return rows[i].getAttribute('ztabindex');
						}
					}
					
					var tagData = {
						tag: tagName,
						type: this.item.getTagType(tagName)
					};
					
					if (row) {
						// Update row and label
						this.updateRow(row, tagData);
						var elem = this.createValueElement(tagName);
						
						// Remove the old row, which we'll reinsert at the correct place
						rowsElement.removeChild(row);
						
						// Find the current label or textbox within the row
						// and replace it with the new element -- this is used
						// both when creating new rows and when hiding the
						// entry textbox
						var oldElem = row.getElementsByAttribute('fieldname', 'tag')[0];
						row.replaceChild(elem, oldElem);
					}
					else {
						// Create new row, but don't insert it
						row = this.addDynamicRow(tagData, false, true);
						var elem = row.getElementsByAttribute('fieldname', 'tag')[0];
					}
					
					// Move row to appropriate place, alphabetically
					var collation = Zotero.getLocaleCollation();
					var labels = rowsElement.getElementsByAttribute('fieldname', 'tag');
					
					var before = null;
					var inserted = false;
					var newTabIndex = false;
					for (var i=0; i<labels.length; i++) {
						let index = i + 1;
						if (inserted) {
							labels[i].setAttribute('ztabindex', index);
							continue;
						}
						
						if (collation.compareString(1, tagName, labels[i].textContent) > 0) {
							labels[i].setAttribute('ztabindex', index);
							continue;
						}
						
						elem.setAttribute('ztabindex', index);
						rowsElement.insertBefore(row, labels[i].parentNode);
						newTabIndex = index;
						inserted = true;
					}
					if (!inserted) {
						newTabIndex = i + 1;
						elem.setAttribute('ztabindex', newTabIndex);
						rowsElement.appendChild(row);
					}
					
					return newTabIndex;
				]]></body>
			</method>
			
			
			<method name="remove">
				<parameter name="tagName"/>
				<body><![CDATA[
					Zotero.debug("REMOVING ROW WITH " + tagName);
					
					var rowsElement = this.id('tagRows');
					var rows = rowsElement.childNodes;
					var removed = false;
					var oldTabIndex = -1;
					for (var i=0; i<rows.length; i++) {
						let value = rows[i].getAttribute('tagName');
						Zotero.debug("-=-=");
						Zotero.debug(value);
						Zotero.debug(tagName);
						Zotero.debug(value === tagName);
						if (value === tagName) {
							oldTabIndex = i + 1;
							removed = true;
							rowsElement.removeChild(rows[i]);
							i--;
							continue;
						}
						// After the removal, update tab indexes
						if (removed) {
							var elem = rows[i].getElementsByAttribute('fieldname', 'tag')[0];
							elem.setAttribute('ztabindex', i + 1);
						}
					}
					return oldTabIndex;
				]]></body>
			</method>
			
			
			<method name="updateCount">
				<parameter name="count"/>
				<body>
					<![CDATA[
						if (!this.item) {
							return;
						}
						
						if(typeof count == 'undefined') {
							var tags = this.item.getTags();
							if(tags)
								count = tags.length;
							else
								count = 0;
						}
						
						var str = 'pane.item.tags.count.';
						switch (count){
							case 0:
								str += 'zero';
								break;
							case 1:
								str += 'singular';
								break;
							default:
								str += 'plural';
								break;
						}
						
						this.id('tagsNum').value = Zotero.getString(str, [count]);
						this.count = count;
					]]>
				</body>
			</method>
			
			<method name="closePopup">
				<body>
					<![CDATA[
						if (this.parentNode.hidePopup) {
							this.parentNode.hidePopup()
						}
					]]>
				</body>
			</method>
			
			
			<!-- 
				Open the textbox for a particular label
				
				Note: We're basically replicating the built-in tabindex functionality,
				which doesn't work well with the weird label/textbox stuff we're doing.
				(The textbox being tabbed away from is deleted before the blur()
				completes, so it doesn't know where it's supposed to go next.)
			-->
			<method name="_focusField">
				<body>
				<![CDATA[
					if (this._reloading) {
						return;
					}
					
					if (this._lastTabIndex === false) {
						return;
					}
					
					var maxIndex = this.id('tagRows').childNodes.length + 1;
					
					var tabindex = parseInt(this._lastTabIndex);
					var dir = this._tabDirection;
					
					if (dir == 1) {
						var nextIndex = tabindex + 1;
					}
					else if (dir == -1) {
						if (tabindex == 1) {
							// Focus Add button
							this.id('addButton').focus();
							return false;
						}
						var nextIndex = tabindex - 1;
					}
					else {
						var nextIndex = tabindex;
					}
					
					nextIndex = Math.min(nextIndex, maxIndex);
					
					Zotero.debug('Looking for tabindex ' + nextIndex, 4);
					
					var next = document.getAnonymousNodes(this)[0]
						.getElementsByAttribute('ztabindex', nextIndex);
					if (next.length) {
						next = next[0];
						next.click();
					}
					else {
						next = this.new();
						next = next.firstChild.nextSibling;
					}
					
					if (!next) {
						Components.utils.reportError('Next row not found');
						return;
					}
					
					this.ensureElementIsVisible(next);
				]]>
				</body>
			</method>
			
			
			<method name="_onAddButtonKeypress">
				<parameter name="event"/>
				<body><![CDATA[
					if (event.keyCode != event.DOM_VK_TAB || event.shiftKey) {
						return true;
					}
					
					this._lastTabIndex = 0;
					this._tabDirection = 1;
					this._focusField();
					return false;
				]]></body>
			</method>
			
			
			<!-- unused -->
			<method name="getTagIndex">
				<parameter name="id"/>
				<body><![CDATA[
					var rows = this.id('tagRows').getElementsByTagName('row');
					for (let i=0; i<rows.length; i++) {
						var row = rows[i].getAttribute('id');
						if (row && row.split("-")[1] == id) {
							return i;
						}
					}
					return -1;
				]]></body>
			</method>
			
			
			<method name="scrollToTop">
				<body>
				<![CDATA[
					Zotero.debug('SCROLL TO TOP');
					if (!this._activeScrollbox) {
						Zotero.debug('NO');
						return;
					}
					var sbo = this._activeScrollbox.boxObject;
					sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject);
					sbo.scrollTo(0,0);
				]]>
				</body>
			</method>
			
			
			<method name="ensureElementIsVisible">
				<parameter name="elem"/>
				<body>
				<![CDATA[
					var sbo = document.getAnonymousNodes(this)[0].boxObject;
					if (Zotero.platformMajorVersion < 36) {
						sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject);
					}
					sbo.ensureElementIsVisible(elem);
				]]>
				</body>
			</method>
			
			
			<method name="blurOpenField">
				<body><![CDATA[
					return Zotero.spawn(function* () {
						this._lastTabIndex = false;
						
						var textboxes = document.getAnonymousNodes(this)[0].getElementsByTagName('textbox');
						if (textboxes && textboxes.length) {
							textboxes[0].inputField.onblur = null;
							yield this.blurHandler(textboxes[0].inputField);
						}
					}.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>
			<xul:scrollbox xbl:inherits="flex" orient="vertical" style="overflow:auto" class="zotero-box">
				<xul:hbox align="center">
					<xul:label id="tagsNum"/>
					<xul:button id="addButton" label="&zotero.item.add;"
						onkeypress="return document.getBindingParent(this)._onAddButtonKeypress(event)"
						oncommand="document.getBindingParent(this).new();"/>
				</xul:hbox>
				<xul:grid>
					<xul:columns>
						<xul:column/>
						<xul:column flex="1"/>
						<xul:column/>
					</xul:columns>
					<xul:rows id="tagRows"/>
				</xul:grid>
			</xul:scrollbox>
		</content>
	</binding>
</bindings>
