<?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">
<!-- <!DOCTYPE bindings SYSTEM "chrome://zotero/locale/itembox.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="item-box">
		<resources>
			<stylesheet src="chrome://zotero/skin/bindings/itembox.css"/>
			<stylesheet src="chrome://zotero-platform/content/itembox.css"/>
		</resources>
		
		<implementation>
			<!--
				Public properties
			-->
			<field name="clickable">false</field>
			<field name="editable">false</field>
			<field name="saveOnEdit">false</field>
			<field name="showTypeMenu">false</field>
			<field name="hideEmptyFields">false</field>
			<field name="clickByRow">false</field> <!-- Click entire row rather than just field value -->
			<field name="clickByItem">false</field>
			
			<field name="clickHandler"/>
			<field name="blurHandler"/>
			<field name="eventHandlers">[]</field>
			
			<field name="_initialVisibleCreators">10</field>
			<field name="_displayAllCreators"/>
			
			<!-- 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;
					this.saveOnEdit = false;
					this.showTypeMenu = false;
					this.hideEmptyFields = false;
					this.clickByRow = false;
					this.clickByItem = false;
					
					switch (val) {
						case 'view':
							break;
						
						case 'edit':
							this.clickable = true;
							this.editable = true;
							this.saveOnEdit = true
							this.showTypeMenu = true;
							this.clickHandler = this.showEditor;
							this.blurHandler = this.hideEditor;
							break;
						
						case 'merge':
							this.clickByItem = true;
							break;
						
						case 'mergeedit':
							this.clickable = true;
							this.editable = true;
							this.saveOnEdit = false;
							this.showTypeMenu = true;
							this.clickHandler = this.showEditor;
							this.blurHandler = this.hideEditor;
							break;
						
						case 'fieldmerge':
							this.hideEmptyFields = true;
							this._fieldAlternatives = {};
							break;
						
						default:
							throw ("Invalid mode '" + val + "' in itembox.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[
					if (!(val instanceof Zotero.Item)) {
						throw ("<zoteroitembox>.item must be a Zotero.Item");
					}
					
					// When changing items, reset truncation of creator list
					if (!this._item || val.id != this._item.id) {
						this._displayAllCreators = false;
					}
					
					this._item = val;
					this.refresh();
				]]>
				</setter>
			</property>
			
			<!-- .ref is an alias for .item -->
			<property name="ref"
				onget="return this._item;"
				onset="this.item = val; this.refresh();">
			</property>
			
			
			<!--
				 An array of field names that should be shown
				 even if they're empty and hideEmptyFields is set
			-->
			<field name="_visibleFields">[]</field>
			<property name="visibleFields">
				<setter>
				<![CDATA[
					if (val.constructor.name != 'Array') {
						throw ('visibleFields must be an array in <itembox>.visibleFields');
					}
					
					this._visibleFields = val;
				]]>
				</setter>
			</property>
			
			<!--
				 An array of field names that should be hidden
			-->
			<field name="_hiddenFields">[]</field>
			<property name="hiddenFields">
				<setter>
				<![CDATA[
					if (val.constructor.name != 'Array') {
						throw ('hiddenFields must be an array in <itembox>.visibleFields');
					}
					
					this._hiddenFields = val;
				]]>
				</setter>
			</property>
			
			<!--
				 An array of field names that should be clickable
				 even if this.clickable is false
			-->
			<field name="_clickableFields">[]</field>
			<property name="clickableFields">
				<setter>
				<![CDATA[
					if (val.constructor.name != 'Array') {
						throw ('clickableFields must be an array in <itembox>.clickableFields');
					}
					
					this._clickableFields = val;
				]]>
				</setter>
			</property>
			
			<!--
				 An array of field names that should be editable
				 even if this.editable is false
			-->
			<field name="_editableFields">[]</field>
			<property name="editableFields">
				<setter>
				<![CDATA[
					if (val.constructor.name != 'Array') {
						throw ('editableFields must be an array in <itembox>.editableFields');
					}
					
					this._editableFields = val;
				]]>
				</setter>
			</property>
			
			<!--
				 An object of alternative values for keyed fields
				 
			-->
			<field name="_fieldAlternatives">{}</field>
			<property name="fieldAlternatives">
				<setter>
				<![CDATA[
					if (val.constructor.name != 'Object') {
						throw ('fieldAlternatives must be an Object in <itembox>.fieldAlternatives');
					}
					
					if (this.mode != 'fieldmerge') {
						throw ('fieldAlternatives is valid only in fieldmerge mode in <itembox>.fieldAlternatives');
					}
					
					this._fieldAlternatives = val;
				]]>
				</setter>
			</property>
			
			<!--
				 An array of field names in the order they should appear
				 in the list; empty spaces can be created with null
			-->
			<field name="_fieldOrder">[]</field>
			<property name="fieldOrder">
				<setter>
				<![CDATA[
					if (val.constructor.name != 'Array') {
						throw ('fieldOrder must be an array in <itembox>.fieldOrder');
					}
					
					this._fieldOrder = val;
				]]>
				</setter>
			</property>
			
			<property name="itemTypeMenu" onget="return this._id('item-type-menu')"/>
			
			<!-- Private properties -->
			<property name="_dynamicFields" onget="return this._id('dynamic-fields')"/>
			<property name="_creatorTypeMenu" onget="return this._id('creator-type-menu')"/>
			
			<field name="_selectField"/>
			<field name="_beforeRow"/>
			<field name="_addCreatorRow"/>
			<field name="_creatorCount"/>
			
			<field name="_lastTabIndex"/>
			<field name="_tabDirection"/>
			<field name="_tabIndexMinCreators" readonly="true">10</field>
			<field name="_tabIndexMaxCreators">0</field>
			<field name="_tabIndexMinFields" readonly="true">1000</field>
			<field name="_tabIndexMaxFields">0</field>
			
			<property name="_defaultFirstName"
				onget="return '(' + Zotero.getString('pane.item.defaultFirstName') + ')'"/>
			<property name="_defaultLastName"
				onget="return '(' + Zotero.getString('pane.item.defaultLastName') + ')'"/>
			<property name="_defaultFullName"
				onget="return '(' + Zotero.getString('pane.item.defaultFullName') + ')'"/>
			<method name="refresh">
				<body>
				<![CDATA[
					Zotero.debug('Refreshing item box');
					
					if (!this.item) {
						Zotero.debug('No item to refresh', 2);
						return;
					}
					
					if (this.clickByItem) {
						var itemBox = document.getAnonymousNodes(this)[0];
						itemBox.setAttribute('onclick',
							'document.getBindingParent(this).clickHandler(this)');
					}
					
					// Item type menu
					if (this.showTypeMenu) {
						// Build item type menu if it hasn't been built yet
						if (this.itemTypeMenu.itemCount == 0) {
							this.buildItemTypeMenu();
						}
						else {
							this.updateItemTypeMenuSelection();
						}
						
						this.itemTypeMenu.parentNode.hidden = false;
					}
					else {
						this.itemTypeMenu.parentNode.hidden = true;
					}
					
					
					//
					// Clear and rebuild metadata fields
					//
					while (this._dynamicFields.childNodes.length > 1) {
						this._dynamicFields.removeChild(this._dynamicFields.lastChild);
					}
					
					var fieldNames = [];
					
					// Manual field order
					if (this._fieldOrder.length) {
						for each(var field in this._fieldOrder) {
							fieldNames.push(field);
						}
					}
					// Get field order from database
					else {
						if (!this.showTypeMenu) {
							fieldNames.push("itemType");
						}
						
						var fields = Zotero.ItemFields.getItemTypeFields(this.item.getField("itemTypeID"));
						
						for (var i=0; i<fields.length; i++) {
							fieldNames.push(Zotero.ItemFields.getName(fields[i]));
						}
						
						fieldNames.push("dateAdded", "dateModified");
					}
					
					for (var i=0; i<fieldNames.length; i++) {
						var fieldName = fieldNames[i];
						var val = '';
						
						if (fieldName) {
							var fieldID = Zotero.ItemFields.getID(fieldName);
							if (fieldID && !Zotero.ItemFields.isValidForType(fieldID, this.item.itemTypeID)) {
								fieldName = null;
							}
						}
						
						if (fieldName) {
							if (this._hiddenFields.indexOf(fieldName) != -1) {
								continue;
							}
							
							// createValueElement() adds the itemTypeID as an attribute
							// and converts it to a localized string for display
							if (fieldName == 'itemType') {
								val = this.item.itemTypeID;
							}
							else {
								val = this.item.getField(fieldName);
							}
							
							if (!val && this.hideEmptyFields
									&& this._visibleFields.indexOf(fieldName) == -1
									&& (this.mode != 'fieldmerge' || typeof this._fieldAlternatives[fieldName] == 'undefined')) {
								continue;
							}
							
							var fieldIsClickable = this._fieldIsClickable(fieldName);
							
							// Start tabindex at 1001 after creators
							var tabindex = fieldIsClickable
								? (i>0 ? this._tabIndexMinFields + i : 1) : 0;
							this._tabIndexMaxFields = Math.max(this._tabIndexMaxFields, tabindex);
							
							if (fieldIsClickable
									&& !Zotero.Items.isPrimaryField(fieldName)
									&& (Zotero.ItemFields.isFieldOfBase(Zotero.ItemFields.getID(fieldName), 'date')
										// TEMP - filingDate
										|| fieldName == 'filingDate')
									// TEMP - NSF
									&& fieldName != 'dateSent') {
								this.addDateRow(fieldNames[i], this.item.getField(fieldName, true), tabindex);
								continue;
							}
						}
						
						var valueElement = this.createValueElement(
							val, fieldName, tabindex
						);
						
						var label = document.createElement("label");
						label.setAttribute('fieldname', fieldName);
						
						var prefix = '';
						// Add '(...)' before 'Abstract:' for collapsed abstracts
						if (fieldName == 'abstractNote') {
							if (val && !Zotero.Prefs.get('lastAbstractExpand')) {
								prefix = '(...) ';
							}
						}
						
						if (fieldName) {
							label.setAttribute("value", prefix +
								Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, fieldName) + ":");
						}
						
						// TEMP - NSF (homepage)
						if ((fieldName == 'url' || fieldName == 'homepage') && val) {
							label.setAttribute("isButton", true);
							// TODO: make getFieldValue non-private and use below instead
							label.setAttribute("onclick", "ZoteroPane_Local.loadURI(this.nextSibling.firstChild ? this.nextSibling.firstChild.nodeValue : this.nextSibling.value, event)");
							label.setAttribute("tooltiptext", Zotero.getString('locate.online.tooltip'));
						}
						else if (fieldName == 'DOI' && val && typeof val == 'string') {
							// Pull out DOI, in case there's a prefix
							var doi = Zotero.Utilities.cleanDOI(val);
							if (doi) {
								doi = "http://dx.doi.org/" + encodeURIComponent(doi);
								label.setAttribute("isButton", true);
								label.setAttribute("onclick", "ZoteroPane_Local.loadURI('" + doi + "', event)");
								label.setAttribute("tooltiptext", Zotero.getString('locate.online.tooltip'));
							}
						}
						else if (fieldName == 'abstractNote') {
							label.setAttribute("onclick",
								"if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); } "
								+ "else { document.getBindingParent(this).toggleAbstractExpand(this); }");
						}
						else {
							label.setAttribute("onclick",
								"if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); }");
						}
						
						var row = this.addDynamicRow(label, valueElement);
						
						if (fieldName && this._selectField == fieldName) {
							this.showEditor(valueElement);
						}
						
						// In field merge mode, add a button to switch field versions
						else if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') {
							var button = document.createElement("toolbarbutton");
							button.className = 'zotero-field-version-button';
							button.setAttribute('image', 'chrome://zotero/skin/treesource-duplicates.png');
							button.setAttribute('type', 'menu');
							
							var popup = button.appendChild(document.createElement("menupopup"));
							
							for each(var v in this._fieldAlternatives[fieldName]) {
								var menuitem = document.createElement("menuitem");
								var sv = Zotero.Utilities.ellipsize(v, 60);
								menuitem.setAttribute('label', sv);
								if (v != sv) {
									menuitem.setAttribute('tooltiptext', v);
								}
								menuitem.setAttribute('fieldName', fieldName);
								menuitem.setAttribute('originalValue', v);
								menuitem.setAttribute(
									'oncommand',
									"var binding = document.getBindingParent(this); "
									+ "var item = binding.item; "
									+ "item.setField(this.getAttribute('fieldName'), this.getAttribute('originalValue')); "
									+ "var row = Zotero.getAncestorByTagName(this, 'row'); "
									+ "binding.refresh();"
								);
								popup.appendChild(menuitem);
							}
							
							row.appendChild(button);
						}
					}
					this._selectField = false;
					
					//
					// Creators
					//
					
					// Creator type menu
					if (this.editable) {
						while (this._creatorTypeMenu.hasChildNodes()) {
							this._creatorTypeMenu.removeChild(this._creatorTypeMenu.firstChild);
						}
						
						var creatorTypes = Zotero.CreatorTypes.getTypesForItemType(this.item.itemTypeID);

						var localized = {};
						for (var i=0; i<creatorTypes.length; i++) {
							localized[creatorTypes[i]['name']]
								= Zotero.getString('creatorTypes.' + creatorTypes[i]['name']);
						}
						
						for (var i in localized) {
							var menuitem = document.createElement("menuitem");
							menuitem.setAttribute("label", localized[i]);
							menuitem.setAttribute("typeid", Zotero.CreatorTypes.getID(i));
							this._creatorTypeMenu.appendChild(menuitem);
						}
						
						var moveSep = document.createElement("menuseparator");
						var moveUp = document.createElement("menuitem");
						var moveDown = document.createElement("menuitem");
						moveSep.id = "zotero-creator-move-sep";
						moveUp.id = "zotero-creator-move-up";
						moveDown.id = "zotero-creator-move-down";
						moveUp.className = "zotero-creator-move";
						moveDown.className = "zotero-creator-move";
						moveUp.setAttribute("label", Zotero.getString('pane.item.creator.moveUp'));
						moveDown.setAttribute("label", Zotero.getString('pane.item.creator.moveDown'));
						this._creatorTypeMenu.appendChild(moveSep);
						this._creatorTypeMenu.appendChild(moveUp);
						this._creatorTypeMenu.appendChild(moveDown);
					}
					
					// Creator rows
					
					// Place, in order of preference, after title, after type,
					// or at beginning
					var titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
					var field = this._dynamicFields.getElementsByAttribute('fieldname', Zotero.ItemFields.getName(titleFieldID)).item(0);
					if (!field) {
						var field = this._dynamicFields.getElementsByAttribute('fieldname', 'itemType').item(0);
					}
					if (field) {
						this._beforeRow = field.parentNode.nextSibling;
					}
					else {
						this._beforeRow = this._dynamicFields.firstChild;
					}
					
					this._creatorCount = 0;
					var num = this.item.numCreators();
					if (num > 0) {
						// Limit number of creators display
						var max = Math.min(num, this._initialVisibleCreators);
						// If fewer than five more, just display
						if (num < max + 5 || this._displayAllCreators) {
							max = num;
						}
						for (var i = 0; i < max; i++) {
							let data = this.item.getCreator(i);
							this.addCreatorRow(data, data.creatorTypeID);
							
							// Display "+" button on all but last row
							if (i == max - 2) {
								this.disableCreatorAddButtons();
							}
						}
						
						// Additional creators not displayed
						if (num > max) {
							this.addMoreCreatorsRow(num - max);
							
							this.disableCreatorAddButtons();
						}
						else {
							// If we didn't start with creators truncated,
							// don't truncate for as long as we're viewing
							// this item, so that added creators aren't
							// immediately hidden
							this._displayAllCreators = true;
							
							if (this._addCreatorRow) {
								this.addCreatorRow(false, this.item.getCreator(max-1).creatorTypeID, true);
								this._addCreatorRow = false;
								this.disableCreatorAddButtons();
							}
						}
					}
					else if (this.editable && Zotero.CreatorTypes.itemTypeHasCreators(this.item.itemTypeID)) {
						// Add default row
						this.addCreatorRow(false, false, true, true);
						this.disableCreatorAddButtons();
					}
					
					// Move to next or previous field if (shift-)tab was pressed
					if (this._lastTabIndex && this._tabDirection)
					{
						this._focusNextField(this._dynamicFields, this._lastTabIndex, this._tabDirection == -1);
					}
				]]>
				</body>
			</method>
			
			
			<method name="buildItemTypeMenu">
				<body>
				<![CDATA[
					if (!this.item) {
						return;
					}
					
					this.itemTypeMenu.removeAllItems();
					
					var t = Zotero.ItemTypes.getTypes();
							
					// Sort by localized name
					var itemTypes = [];
					for (var i=0; i<t.length; i++) {
						itemTypes.push({
							id: t[i].id,
							name: t[i].name,
							localized: Zotero.ItemTypes.getLocalizedString(t[i].id)
						});
					}
					var collation = Zotero.getLocaleCollation();
					itemTypes.sort(function(a, b) {
						return collation.compareString(1, a.localized, b.localized);
					});
					
					for (var i=0; i<itemTypes.length; i++) {
						var name = itemTypes[i].name;
						if (name != 'attachment' && name != 'note') {
							this.itemTypeMenu.appendItem(itemTypes[i].localized, itemTypes[i].id);
						}
					}
					
					this.updateItemTypeMenuSelection();
				]]>
				</body>
			</method>
			
			
			<method name="updateItemTypeMenuSelection">
				<body>
				<![CDATA[
					var listitems = this.itemTypeMenu.firstChild.childNodes;
					for (var i=0, len=listitems.length; i < len; i++) {
						if (listitems[i].getAttribute('value') == this.item.itemTypeID) {
							this.itemTypeMenu.selectedIndex = i;
						}
					}
				]]>
				</body>
			</method>
			
			
			<method name="addDynamicRow">
				<parameter name="label"/>
				<parameter name="value"/>
				<parameter name="beforeElement"/>
				<body>
				<![CDATA[
					var row = document.createElement("row");
					
					// Add click event to row
					if (this._rowIsClickable(value.getAttribute('fieldname'))) {
						row.className = 'zotero-clicky';
						row.addEventListener('click', function (event) {
							document.getBindingParent(this).clickHandler(this);
						}, false);
					}
					
					row.appendChild(label);
					row.appendChild(value);
					if (beforeElement) {
						this._dynamicFields.insertBefore(row, this._beforeRow);
					}
					else {
						this._dynamicFields.appendChild(row);
					}
					
					return row;
				]]>
				</body>
			</method>
			
			
			<method name="addCreatorRow">
				<parameter name="creatorData"/>
				<parameter name="creatorTypeIDOrName"/>
				<parameter name="unsaved"/>
				<parameter name="defaultRow"/>
				<body>
				<![CDATA[
					// getCreatorFields(), switchCreatorMode() and handleCreatorAutoCompleteSelect()
					// may need need to be adjusted if this DOM structure changes
					
					var fieldMode = Zotero.Prefs.get('lastCreatorFieldMode');
					var firstName = '';
					var lastName = '';
					if (creatorData) {
						fieldMode = creatorData.fieldMode;
						firstName = creatorData.firstName;
						lastName = creatorData.lastName;
					}
					
					// Sub in placeholder text for empty fields
					if (fieldMode == 1) {
						if (lastName === "") {
							lastName = this._defaultFullName;
						}
					}
					else {
						if (firstName === "") {
							firstName = this._defaultFirstName;
						}
						if (lastName === "") {
							lastName = this._defaultLastName;
						}
					}
					
					// Use the first entry in the drop-down for the default type if none specified
					var typeID = creatorTypeIDOrName
						? Zotero.CreatorTypes.getID(creatorTypeIDOrName)
						: this._creatorTypeMenu.childNodes[0].getAttribute('typeid');
					
					var typeBox = document.createElement("hbox");
					typeBox.setAttribute("typeid", typeID);
					typeBox.setAttribute("popup", "creator-type-menu");
					typeBox.setAttribute("fieldname", 'creator-' + this._creatorCount + '-typeID');
					if (this.editable) {
						typeBox.className = 'creator-type-label zotero-clicky';
						var img = document.createElement('image');
						typeBox.appendChild(img);
					}
					else {
						typeBox.className = 'creator-type-label';
					}
					
					var label = document.createElement("label");
					label.setAttribute('value',
						Zotero.getString('creatorTypes.'+Zotero.CreatorTypes.getName(typeID)) + ":")
					typeBox.appendChild(label);
					
					var hbox = document.createElement("hbox");
					hbox.className = 'creator-type-value';
					
					// Name
					var firstlast = document.createElement("hbox");
					firstlast.className = 'creator-name-box';
					firstlast.setAttribute("flex","1");
					var tabindex = this._tabIndexMinCreators + (this._creatorCount * 2);
					var fieldName = 'creator-' + this._creatorCount + '-lastName';
					var lastNameLabel = firstlast.appendChild(
						this.createValueElement(
							lastName,
							fieldName,
							tabindex
						)
					);
					
					// Comma
					var comma = document.createElement('label');
					comma.setAttribute('value', ',');
					comma.className = 'comma';
					firstlast.appendChild(comma);
					
					var fieldName = 'creator-' + this._creatorCount + '-firstName';
					firstlast.appendChild(
						this.createValueElement(
							firstName,
							fieldName,
							tabindex + 1
						)
					);
					if (fieldMode) {
						firstlast.lastChild.setAttribute('hidden', true);
					}
					
					if (this.editable) {
						firstlast.setAttribute('contextmenu', 'zotero-creator-transform-menu');
					}
					
					this._tabIndexMaxCreators = Math.max(this._tabIndexMaxCreators, tabindex);
					
					hbox.appendChild(firstlast);
					
					// Single/double field toggle
					var toggleButton = document.createElement('label');
					toggleButton.setAttribute('fieldname',
						'creator-' + this._creatorCount + '-fieldMode');
					toggleButton.className = 'zotero-field-toggle zotero-clicky';
					hbox.appendChild(toggleButton);
					
					// Minus (-) button
					var removeButton = document.createElement('label');
					removeButton.setAttribute("value","-");
					removeButton.setAttribute("class","zotero-clicky zotero-clicky-minus");
					// If default first row, don't let user remove it
					if (defaultRow) {
						this.disableButton(removeButton);
					}
					else {
						removeButton.setAttribute("onclick",
							"document.getBindingParent(this).removeCreator("
							+ this._creatorCount
							+ ", this.parentNode.parentNode)");
					}
					hbox.appendChild(removeButton);
					
					// Plus (+) button
					var addButton = document.createElement('label');
					addButton.setAttribute("value","+");
					addButton.setAttribute("class", "zotero-clicky zotero-clicky-plus");
					// If row isn't saved, don't let user add more
					if (unsaved) {
						this.disableButton(addButton);
					}
					else {
						this._enablePlusButton(addButton, typeID, fieldMode);
					}
					hbox.appendChild(addButton);
					
					this._creatorCount++;
					
					if (!this.editable) {
						toggleButton.hidden = true;
						removeButton.hidden = true;
						addButton.hidden = true;
					}
					
					this.addDynamicRow(typeBox, hbox, true);
					
					// Set single/double field toggle mode
					if (fieldMode) {
						this.switchCreatorMode(hbox.parentNode, 1, true);
					}
					else {
						this.switchCreatorMode(hbox.parentNode, 0, true);
					}
					
					// Focus new rows
					if (unsaved && !defaultRow){
						lastNameLabel.click();
					}
				]]>
				</body>
			</method>
			
			
			<method name="addMoreCreatorsRow">
				<parameter name="num"/>
				<body>
				<![CDATA[
					var box = document.createElement('box');
					
					var label = document.createElement('label');
					label.id = 'more-creators-label';
					label.setAttribute('value', Zotero.getString('general.numMore', num));
					label.setAttribute('onclick',
						"var binding = document.getBindingParent(this); "
						+ "binding._displayAllCreators = true; "
						+ "binding.refresh()"
					);
					
					this.addDynamicRow(box, label, true);
				]]>
				</body>
			</method>
			
			<method name="addDateRow">
				<parameter name="field"/>
				<parameter name="value"/>
				<parameter name="tabindex"/>
				<body>
				<![CDATA[
					var label = document.createElement("label");
					label.setAttribute("value", Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, field) + ':');
					label.setAttribute("fieldname", field);
					label.setAttribute("onclick", "this.nextSibling.firstChild.blur()");
					
					var elem = this.createValueElement(
						Zotero.Date.multipartToStr(value),
						field,
						tabindex
					);
					elem.setAttribute('flex', 1);
					
					// y-m-d status indicator
					var ymd = document.createElement('label');
					ymd.id = 'zotero-date-field-status';
					ymd.setAttribute(
						'value',
						Zotero.Date.strToDate(Zotero.Date.multipartToStr(value))
							.order.split('').join(' ')
					);
					
					var hbox = document.createElement('hbox');
					hbox.setAttribute('flex', 1);
					hbox.className = "date-box";
					hbox.appendChild(elem);
					hbox.appendChild(ymd);
					
					this.addDynamicRow(label, hbox);
				]]>
				</body>
			</method>
			
			
			<method name="switchCreatorMode">
				<parameter name="row"/>
				<parameter name="fieldMode"/>
				<parameter name="initial"/>
				<parameter name="updatePref"/>
				<body>
				<![CDATA[
					// Change if button position changes
					// row->hbox->label->label->toolbarbutton
					var button = row.lastChild.lastChild.previousSibling.previousSibling;
					var hbox = button.previousSibling;
					var lastName = hbox.firstChild;
					var comma = hbox.firstChild.nextSibling;
					var firstName = hbox.lastChild;
					
					// Switch to single-field mode
					if (fieldMode == 1) {
						button.style.backgroundImage = 'url("chrome://zotero/skin/textfield-dual.png")';
						button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.two'));
						lastName.setAttribute('fieldMode', '1');
						button.setAttribute('onclick', "document.getBindingParent(this).switchCreatorMode(Zotero.getAncestorByTagName(this, 'row'), 0, false, true)");
						lastName.setAttribute('flex', '1');
						delete lastName.style.width;
						delete lastName.style.maxWidth;
						
						// Remove firstname field from tabindex
						var tab = parseInt(firstName.getAttribute('ztabindex'));
						firstName.setAttribute('ztabindex', -1);
						if (this._tabIndexMaxCreators == tab) {
							this._tabIndexMaxCreators--;
						}
						
						// Hide first name field and prepend to last name field
						firstName.setAttribute('hidden', true);
						comma.setAttribute('hidden', true);
						
						if (!initial) {
							var first = this._getFieldValue(firstName);
							if (first && first != this._defaultFirstName) {
								var last = this._getFieldValue(lastName);
								this._setFieldValue(lastName, first + ' ' + last);
							}
						}
						
						if (this._getFieldValue(lastName) == this._defaultLastName) {
							this._setFieldValue(lastName, this._defaultFullName);
						}
					}
					// Switch to two-field mode
					else {
						button.style.backgroundImage = 'url("chrome://zotero/skin/textfield-single.png")';
						button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.one'));
						lastName.setAttribute('fieldMode', '0');
						button.setAttribute('onclick', "document.getBindingParent(this).switchCreatorMode(Zotero.getAncestorByTagName(this, 'row'), 1, false, true)");
						lastName.setAttribute('flex', '0');
						
						// appropriately truncate lastName
						
						// get item box width
						var computedStyle = window.getComputedStyle(this, null);
						var boxWidth = computedStyle.getPropertyValue('width');
						// get field label width
						var computedStyle = window.getComputedStyle(row.firstChild, null);
						var leftHboxWidth = computedStyle.getPropertyValue('width');
						// get last name width
						computedStyle = window.getComputedStyle(lastName, null);
						var lastNameWidth = computedStyle.getPropertyValue('width');
						if(boxWidth.substr(-2) === 'px'
								&& leftHboxWidth.substr(-2) === 'px'
								&& lastNameWidth.substr(-2) === "px") {
							// compute a maximum width
							boxWidth = parseInt(boxWidth);
							leftHboxWidth = parseInt(leftHboxWidth);
							lastNameWidth = parseInt(lastNameWidth);
							var maxWidth = boxWidth-leftHboxWidth-140;
							if(lastNameWidth > maxWidth) {
								lastName.style.width = maxWidth+"px";
								lastName.style.maxWidth = maxWidth+"px";
							} else {
								delete lastName.style.width;
								delete lastName.style.maxWidth;
							}
						}
						
						// Add firstname field to tabindex
						var tab = parseInt(lastName.getAttribute('ztabindex'));
						firstName.setAttribute('ztabindex', tab + 1);
						if (this._tabIndexMaxCreators == tab)
						{
							this._tabIndexMaxCreators++;
						}
						
						if (!initial) {
							// Move all but last word to first name field and show it
							var last = this._getFieldValue(lastName);
							if (last && last != this._defaultFullName) {
								var lastNameRE = /(.*?)[ ]*([^ ]+[ ]*)$/;
								var parts = lastNameRE.exec(last);
								if (parts[2] && parts[2] != last)
								{
									this._setFieldValue(lastName, parts[2]);
									this._setFieldValue(firstName, parts[1]);
								}
							}
						}
						
						if (!this._getFieldValue(firstName)) {
							this._setFieldValue(firstName, this._defaultFirstName);
						}
						
						if (this._getFieldValue(lastName) == this._defaultFullName) {
							this._setFieldValue(lastName, this._defaultLastName);
						}
						
						firstName.setAttribute('hidden', false);
						comma.setAttribute('hidden', false);
					}
					
					// Save the last-used field mode
					if (updatePref) {
						Zotero.debug("Switching lastCreatorFieldMode to " + fieldMode);
						Zotero.Prefs.set('lastCreatorFieldMode', fieldMode);
					}
					
					if (!initial)
					{
						var index = button.getAttribute('fieldname').split('-')[1];
						var fields = this.getCreatorFields(row);
						fields.fieldMode = fieldMode;
						this.modifyCreator(index, fields);
					}
				]]>
				</body>
			</method>
			
			
			<method name="scrollToTop">
				<body>
				<![CDATA[
					// DEBUG: Valid nsIScrollBoxObject but methods return errors
					return;
					
					var sbo = document.getAnonymousNodes(this)[0].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="changeTypeTo">
				<parameter name="itemTypeID"/>
				<parameter name="menu"/>
				<body>
				<![CDATA[
					if (itemTypeID == this.item.itemTypeID) {
						return true;
					}
					
					var fieldsToDelete = this.item.getFieldsNotInType(itemTypeID, true);
					
					// Special cases handled below
					var bookTypeID = Zotero.ItemTypes.getID('book');
					var bookSectionTypeID = Zotero.ItemTypes.getID('bookSection');
					
					// Add warning for shortTitle when moving from book to bookSection
					// when title will be transferred
					if (this.item.itemTypeID == bookTypeID && itemTypeID == bookSectionTypeID) {
						var titleFieldID = Zotero.ItemFields.getID('title');
						var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
						if (this.item.getField(titleFieldID) && this.item.getField(shortTitleFieldID)) {
							if (!fieldsToDelete) {
								fieldsToDelete = [];
							}
							fieldsToDelete.push(shortTitleFieldID);
						}
					}
					
					// Generate list of localized field names for display in pop-up
					if (fieldsToDelete) {
						// Ignore warning for bookTitle when going from bookSection to book
						// if there's not also a title, since the book title is transferred
						// to title automatically in Zotero.Item.setType()
						if (this.item.itemTypeID == bookSectionTypeID && itemTypeID == bookTypeID) {
							var titleFieldID = Zotero.ItemFields.getID('title');
							var bookTitleFieldID = Zotero.ItemFields.getID('bookTitle');
							var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
							if (this.item.getField(bookTitleFieldID) && !this.item.getField(titleFieldID)) {
								var index = fieldsToDelete.indexOf(bookTitleFieldID);
								fieldsToDelete.splice(index, 1);
								// But warn for short title, which will be removed
								if (this.item.getField(shortTitleFieldID)) {
									fieldsToDelete.push(shortTitleFieldID);
								}
							}
						}
						
						var fieldNames = "";
						for (var i=0; i<fieldsToDelete.length; i++) {
							fieldNames += "\n - " +
								Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, fieldsToDelete[i]);
						}
						
						var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
							.getService(Components.interfaces.nsIPromptService);
					}
					
					if (!fieldsToDelete || fieldsToDelete.length == 0 ||
							promptService.confirm(null,
								Zotero.getString('pane.item.changeType.title'),
								Zotero.getString('pane.item.changeType.text') + "\n" + fieldNames)) {
						this.item.setType(itemTypeID);
						
						if (this.saveOnEdit) {
							this.item.saveTx();
						}
						else {
							this.refresh();
						}
						
						if (this.eventHandlers['itemtypechange'] && this.eventHandlers['itemtypechange'].length) {
							var self = this;
							this.eventHandlers['itemtypechange'].forEach(function (f) f.bind(self)());
						}
						
						return true;
					}
					
					// Revert the menu (which changes before the pop-up)
					if (menu) {
						menu.value = this.item.itemTypeID;
					}
					
					return false;
				]]>
				</body>
			</method>
			
			
			<method name="toggleAbstractExpand">
				<parameter name="label"/>
				<body>
				<![CDATA[
					var cur = Zotero.Prefs.get('lastAbstractExpand');
					Zotero.Prefs.set('lastAbstractExpand', !cur);
					
					var ab = label.nextSibling;
					var valueText = this.item.getField('abstractNote');
					var tabindex = ab.getAttribute('ztabindex');
					var elem = this.createValueElement(
						valueText,
						'abstractNote',
						tabindex
					);
					ab.parentNode.replaceChild(elem, ab);
					
					var text = Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, 'abstractNote') + ':';
					// Add '(...)' before "Abstract:" for collapsed abstracts
					if (valueText && cur) {
						text = '(...) ' + text;
					}
					label.setAttribute('value', text);
				]]>
				</body>
			</method>
			
			
			<method name="disableButton">
				<parameter name="button"/>
				<body>
				<![CDATA[
					button.setAttribute('disabled', true);
					button.setAttribute('onclick', false); 
				]]>
				</body>
			</method>
			
			
			<method name="_enablePlusButton">
				<parameter name="button"/>
				<parameter name="creatorTypeID"/>
				<parameter name="fieldMode"/>
				<body>
				<![CDATA[
					button.setAttribute('disabled', false);
					button.onclick = function () {
						var parent = document.getBindingParent(this);
						parent.disableButton(this);
						parent.addCreatorRow(null, creatorTypeID, true);
					};
				]]>
				</body>
			</method>
			
			
			<method name="disableCreatorAddButtons">
				<body>
				<![CDATA[
					// Disable the "+" button on all creator rows
					var elems = this._dynamicFields.getElementsByAttribute('value', '+');
					for (var i = 0, len = elems.length; i < len; i++) {
						this.disableButton(elems[i]);
					}
				]]>
				</body>
			</method>
			
			
			<method name="createValueElement">
				<parameter name="valueText"/>
				<parameter name="fieldName"/>
				<parameter name="tabindex"/>
				<body>
				<![CDATA[
					valueText = valueText + '';
					
					if (fieldName) {
						var fieldID = Zotero.ItemFields.getID(fieldName);
					}
					
					// If an abstract, check last expand state
					var abstractAsVbox = fieldName == 'abstractNote' && Zotero.Prefs.get('lastAbstractExpand');
					
					// Use a vbox for multiline fields (but Abstract only if it's expanded)
					var useVbox = (fieldName != 'abstractNote' || abstractAsVbox)
						&& Zotero.ItemFields.isMultiline(fieldName);
					
					if (useVbox) {
						var valueElement = document.createElement("vbox");
					}
					else {
						var valueElement = document.createElement("label");
					}
					
					valueElement.setAttribute('fieldname', fieldName);
					valueElement.setAttribute('flex', 1);
					
					if (this._fieldIsClickable(fieldName)) {
						valueElement.setAttribute('ztabindex', tabindex);
						valueElement.addEventListener('click', function (event) {
							/* Skip right-click on Windows */
							if (event.button) {
								return;
							}
							document.getBindingParent(this).clickHandler(this);
						}, false);
						valueElement.className = 'zotero-clicky';
					}
					
					switch (fieldName) {
						case 'itemType':
							valueElement.setAttribute('itemTypeID', valueText);
							valueText = Zotero.ItemTypes.getLocalizedString(valueText);
							break;
						
						// Convert dates from UTC
						case 'dateAdded':
						case 'dateModified':
						case 'accessDate':
						
						// TEMP - NSF
						case 'dateSent':
						case 'dateDue':
						case 'accepted':
							if (valueText) {
								var date = Zotero.Date.sqlToDate(valueText, true);
								if (date) {
									// If no time, interpret as local, not UTC
									if (Zotero.Date.isSQLDate(valueText)) {
										date = Zotero.Date.sqlToDate(valueText);
										valueText = date.toLocaleDateString();
									}
									else {
										valueText = date.toLocaleString();
									}
								}
								else {
									valueText = '';
								}
							}
							break;
					}
					
					if (fieldID) {
						// Display the SQL date as a tooltip for date fields
						// TEMP - filingDate
						if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') || fieldName == 'filingDate') {
							valueElement.setAttribute('tooltiptext',
								Zotero.Date.multipartToSQL(this.item.getField(fieldName, true)));
						}
						
						// Display a context menu for certain fields
						if (this.editable && (fieldName == 'seriesTitle' || fieldName == 'shortTitle' ||
								Zotero.ItemFields.isFieldOfBase(fieldID, 'title') ||
								Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) {
							valueElement.setAttribute('contextmenu', 'zotero-field-transform-menu');
						}
					}
					
					
					if (fieldName && fieldName.indexOf('firstName') != -1) {
						valueElement.setAttribute('flex', '1');
					}
					
					var firstSpace = valueText.indexOf(" ");
					
					// To support newlines in Abstract and Extra fields, use multiple
					// <description> elements inside a vbox
					if (useVbox) {
						var lines = valueText.split("\n");
						for (var i = 0; i < lines.length; i++) {
							var descriptionNode = document.createElement("description");
							// Add non-breaking space to empty lines to prevent them from collapsing.
							// (Just using CSS min-height results in overflow in some cases.)
							if (lines[i] === "") {
								lines[i] = "\u00a0";
							}
							var linetext = document.createTextNode(lines[i]);
							descriptionNode.appendChild(linetext);
							valueElement.appendChild(descriptionNode);
						}
					}
					// 29 == arbitrary length at which to chop uninterrupted text
					else if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29
						|| (fieldName &&
							(fieldName.substr(0, 7) == 'creator') || fieldName == 'abstractNote')) {
						if (fieldName == 'abstractNote') {
							valueText = valueText.replace(/[\t\n]/g, ' ');
						}
						valueElement.setAttribute('crop', 'end');
						valueElement.setAttribute('value',valueText);
					}
					else {
						// Wrap to multiple lines
						valueElement.appendChild(document.createTextNode(valueText));
					}
					
					return valueElement;
				]]>
				</body>
			</method>
			
			
			<method name="removeCreator">
				<parameter name="index"/>
				<parameter name="labelToDelete"/>
				<body>
				<![CDATA[
					// If unsaved row, just remove element
					if (!this.item.hasCreatorAt(index)) {
						labelToDelete.parentNode.removeChild(labelToDelete);
						
						// Enable the "+" button on the previous row
						var elems = this._dynamicFields.getElementsByAttribute('value', '+');
						var button = elems[elems.length-1];
						var creatorFields = this.getCreatorFields(Zotero.getAncestorByTagName(button, 'row'));
						this._enablePlusButton(button, creatorFields.creatorTypeID, creatorFields.fieldMode);
						
						this._creatorCount--;
						return;
					}
					this.item.removeCreator(index);
					this.item.saveTx();
				]]>
				</body>
			</method>
			
			
			<method name="showEditor">
				<parameter name="elem"/>
				<body>
				<![CDATA[
					// Blur any active fields
					if (this._dynamicFields) {
						this._dynamicFields.focus();
					}
					
					Zotero.debug('Showing editor');
					
					var fieldName = elem.getAttribute('fieldname');
					var tabindex = elem.getAttribute('ztabindex');
					
					var [field, creatorIndex, creatorField] = fieldName.split('-');
					if (field == 'creator') {
						var value = this.item.getCreator(creatorIndex)[creatorField];
						if (value === undefined) {
							value = "";
						}
						var itemID = this.item.id;
					}
					else {
						var value = this.item.getField(fieldName);
						var itemID = this.item.id;
						
						// Access date needs to be converted from UTC
						if (value != '') {
							switch (fieldName) {
								case 'accessDate':
								
								// TEMP - NSF
								case 'dateSent':
								case 'dateDue':
								case 'accepted':
									// If no time, interpret as local, not UTC
									if (Zotero.Date.isSQLDate(value)) {
										var localDate = Zotero.Date.sqlToDate(value);
									}
									else {
										var localDate = Zotero.Date.sqlToDate(value, true);
									}
									var value = Zotero.Date.dateToSQL(localDate);
									
									// Don't show time in editor
									value = value.replace(' 00:00:00', '');
									break;
							}
						}
					}
					
					var t = document.createElement("textbox");
					t.setAttribute('value', value);
					t.setAttribute('fieldname', fieldName);
					t.setAttribute('ztabindex', tabindex);
					t.setAttribute('flex', '1');
					
					if (creatorField=='lastName') {
						t.setAttribute('fieldMode', elem.getAttribute('fieldMode'));
						t.setAttribute('newlines','pasteintact');
					}
					
					if (Zotero.ItemFields.isMultiline(fieldName) || Zotero.ItemFields.isLong(fieldName)) {
						t.setAttribute('multiline', true);
						t.setAttribute('rows', 8);
					}
					else {
						// Add auto-complete for certain fields
						if (Zotero.ItemFields.isAutocompleteField(fieldName)
								|| fieldName == 'creator') {
							t.setAttribute('type', 'autocomplete');
							t.setAttribute('autocompletesearch', 'zotero');
							
							let params = {
								fieldName: fieldName,
								libraryID: this.item.libraryID
							};
							if (field == 'creator') {
								params.fieldMode = parseInt(elem.getAttribute('fieldMode'));
								
								// Include itemID and creatorTypeID so the autocomplete can
								// avoid showing results for creators already set on the item
								let row = Zotero.getAncestorByTagName(elem, 'row');
								let creatorTypeID = parseInt(
									row.getElementsByClassName('creator-type-label')[0]
									.getAttribute('typeid')
								);
								if (itemID) {
									params.itemID = itemID;
									params.creatorTypeID = creatorTypeID;
								}
								
								t.setAttribute('ontextentered',
									'document.getBindingParent(this).handleCreatorAutoCompleteSelect(this)');
							};
							t.setAttribute(
								'autocompletesearchparam', JSON.stringify(params)
							);
							t.setAttribute('completeselectedindex', true);
						}
					}
					var box = elem.parentNode;
					box.replaceChild(t, elem);
					
					// Prevent error when clicking between a changed field
					// and another -- there's probably a better way
					if (!t.select) {
						return;
					}
					
					t.select();
					
					// Leave text field open when window loses focus
					var ignoreBlur = function () {
						this.ignoreBlur = true;
					}.bind(this);
					var unignoreBlur = function () {
						this.ignoreBlur = false;
					}.bind(this);
					addEventListener("deactivate", ignoreBlur);
					addEventListener("activate", unignoreBlur);
					
					t.addEventListener('blur', function () {
						var self = document.getBindingParent(this);
						if (self.ignoreBlur) return;
						
						removeEventListener("deactivate", ignoreBlur);
						removeEventListener("activate", unignoreBlur);
						self.blurHandler(this);
					});
					t.setAttribute('onkeypress', "return document.getBindingParent(this).handleKeyPress(event)");
					
					this._tabDirection = false;
					this._lastTabIndex = tabindex;
					
					return t;
				]]>
				</body>
			</method>
			
			
			<!--
			 Save a multiple-field selection for the creator autocomplete
			 (e.g. "Shakespeare, William")
			-->
			<method name="handleCreatorAutoCompleteSelect">
				<parameter name="textbox"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						var comment = false;
						var controller = textbox.controller;
						if (!controller.matchCount) return;
						
						for (var i=0; i<controller.matchCount; i++)
						{
							if (controller.getValueAt(i) == textbox.value)
							{
								comment = controller.getCommentAt(i);
								break;
							}
						}
						
						Zotero.debug(comment);
						var [creatorID, numFields] = comment.split('-');
						
						// If result uses two fields, save both
						if (numFields==2)
						{
							// Manually clear autocomplete controller's reference to
							// textbox to prevent error next time around
							textbox.mController.input = null;
							
							var [field, creatorIndex, creatorField] =
								textbox.getAttribute('fieldname').split('-');
							
							// Stay focused
							this._lastTabIndex = parseInt(textbox.getAttribute('ztabindex')) - 1;
							this._tabDirection = 1;
							
							var creator = yield Zotero.Creators.getAsync(creatorID);
							
							var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName';
							
							// Update this textbox
							textbox.setAttribute('value', creator[creatorField]);
							textbox.value = creator[creatorField];
							
							// Update the other label
							if (otherField=='firstName'){
								var label = textbox.nextSibling.nextSibling;
							}
							else if (otherField=='lastName'){
								var label = textbox.previousSibling.previousSibling;
							}
							
							//this._setFieldValue(label, creator[otherField]);
							if (label.firstChild){
								label.firstChild.nodeValue = creator[otherField];
							}
							else {
								label.value = creator[otherField];
							}
							
							var row = Zotero.getAncestorByTagName(textbox, 'row');
							
							var fields = this.getCreatorFields(row);
							fields[creatorField] = creator[creatorField];
							fields[otherField] = creator[otherField];
							yield this.modifyCreator(creatorIndex, fields);
						}
						
						// Otherwise let the autocomplete popup handle matters
					}, this);
				]]></body>
			</method>
			
			
			<method name="handleKeyPress">
				<parameter name="event"/>
				<body>
				<![CDATA[
					var target = event.target;
					var focused = document.commandDispatcher.focusedElement;
					
					switch (event.keyCode)
					{
						case event.DOM_VK_RETURN:
							var fieldname = target.getAttribute('fieldname');
							// Use shift-enter as the save action for the larger fields
							if (Zotero.ItemFields.isMultiline(fieldname) && !event.shiftKey) {
								break;
							}
							
							
							// Prevent blur on containing textbox
							// DEBUG: what happens if this isn't present?
							event.preventDefault();
							
							// Shift-enter adds new creator row
							if (fieldname.indexOf('creator-') == 0 && event.shiftKey) {
								// Value hasn't changed
								if (target.getAttribute('value') == target.value) {
									Zotero.debug("Value hasn't changed");
									// If + button is disabled, just focus next creator row
									if (Zotero.getAncestorByTagName(target, 'row').lastChild.lastChild.disabled) {
										this._focusNextField(this._dynamicFields, this._lastTabIndex, false);
									}
									else {
										var creatorFields = this.getCreatorFields(Zotero.getAncestorByTagName(target, 'row'));
										this.addCreatorRow(false, creatorFields.creatorTypeID, true);
									}
								}
								// Value has changed
								else {
									this._tabDirection = 1;
									this._addCreatorRow = true;
									focused.blur();
								}
								return false;
							}
							focused.blur();
							
							// Return focus to items 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');
							
							focused.blur();
							
							// Return focus to items pane
							var tree = document.getElementById('zotero-items-tree');
							if (tree) {
								tree.focus();
							}
							
							return false;
							
						case event.DOM_VK_TAB:
							this._tabDirection = event.shiftKey ? -1 : 1;
							// Blur the old manually -- not sure why this is necessary,
							// but it prevents an immediate blur() on the next tag
							focused.blur();
							return false;
					}
					
					return true;
				]]>
				</body>
			</method>
			
			
			<method name="itemTypeMenuTab">
				<parameter name="event"/>
				<body>
				<![CDATA[
					if (!event.shiftKey) {
						this.focusFirstField();
						event.preventDefault();
					}
					// Shift-tab
					else {
						this._tabDirection = false;
					}
				]]>
				</body>
			</method>
			
			
			<method name="hideEditor">
				<parameter name="textbox"/>
				<body><![CDATA[
				return Zotero.spawn(function* () {
					Zotero.debug('Hiding editor');
					
					// Prevent autocomplete breakage in Firefox 3
					if (textbox.mController) {
						textbox.mController.input = null;
					}
					
					var fieldName = textbox.getAttribute('fieldname');
					var tabindex = textbox.getAttribute('ztabindex');
					
					//var value = t.value;
					var value = textbox.value;
					
					var elem;
					var [field, creatorIndex, creatorField] = fieldName.split('-');
					
					// Creator fields
					if (field == 'creator') {
						var row = Zotero.getAncestorByTagName(textbox, 'row');
						
						var otherFields = this.getCreatorFields(row);
						otherFields[creatorField] = value;
						var lastName = otherFields.lastName.trim();
						
						//Handle \n\r and \n delimited entries
						var rawNameArray = lastName.split(/\r\n?|\n/);
						if (rawNameArray.length > 1) {
							//Save tab direction and add creator flags since they are reset in the 
							//process of adding multiple authors
							var tabDirectionBuffer = this._tabDirection;
							var addCreatorRowBuffer = this._addCreatorRow;
							var tabIndexBuffer = this._lastTabIndex;
							this._tabDirection = false;
							this._addCreatorRow = false;
							
							//Filter out bad names
							var nameArray = [tempName for each(tempName in rawNameArray) if(tempName)];
							
							//If not adding names at the end of the creator list, make new creator 
							//entries and then shift down existing creators.
							var initNumCreators = this.item.numCreators();
							var creatorsToShift = initNumCreators - creatorIndex;
							if (creatorsToShift > 0) { 
								//Add extra creators
								for (var i=0;i<nameArray.length;i++) {
									yield this.modifyCreator(i + initNumCreators, otherFields, true);
								}
								
								//Shift existing creators
								for (var i=initNumCreators-1; i>=creatorIndex; i--) {
									let shiftedCreatorData = this.item.getCreator(i);
									this.item.setCreator(nameArray.length + i, shiftedCreatorData);
								}
							}
							
							//Add the creators in lastNameArray one at a time
							for each(var tempName in nameArray) {
								// Check for tab to determine creator name format
								otherFields.fieldMode = (tempName.indexOf('\t') == -1) ? 1 : 0;
								if (otherFields.fieldMode == 0) {
									otherFields.lastName=tempName.split('\t')[0];
									otherFields.firstName=tempName.split('\t')[1];
								} 
								else {
									otherFields.lastName=tempName;
									otherFields.firstName='';
								}
								yield this.modifyCreator(creatorIndex, otherFields, true);
								creatorIndex++;
							}
							this._tabDirection = tabDirectionBuffer;
							this._addCreatorRow = (creatorsToShift==0) ? addCreatorRowBuffer : false;
							if (this._tabDirection == 1) {
								this._lastTabIndex = parseInt(tabIndexBuffer,10) + 2*(nameArray.length-1);
								if (otherFields.fieldMode == 0) {
									this._lastTabIndex++;
								}
							}
						}
						else {
							yield this.modifyCreator(creatorIndex, otherFields);
						}
						
						if (this.saveOnEdit) {
							yield this.item.saveTx();
						}
						
						var val = this.item.getCreator(creatorIndex);
						val = val ? val[creatorField] : null;
						
						if (!val) {
							// Reset to '(first)'/'(last)'/'(name)'
							if (creatorField == 'lastName') {
								val = otherFields.fieldMode
									? this._defaultFullName : this._defaultLastName;
							}
							else if (creatorField == 'firstName') {
								val = this._defaultFirstName;
							}
						}
						
						var newVal = val;
					}
					
					// Fields
					else {
						// Access date needs to be parsed and converted to UTC
						if (value != '') {
							switch (fieldName) {
								case 'accessDate':
									// If just date, don't convert to UTC
									if (Zotero.Date.isSQLDate(value)) {
										var localDate = Zotero.Date.sqlToDate(value);
										value = Zotero.Date.dateToSQL(localDate).replace(' 00:00:00', '');
									}
									else if (Zotero.Date.isSQLDateTime(value)) {
										var localDate = Zotero.Date.sqlToDate(value);
										value = Zotero.Date.dateToSQL(localDate, true);
									}
									else {
										var d = Zotero.Date.strToDate(value);
										value = null;
										if (d.year && d.month != undefined && d.day) {
											d = new Date(d.year, d.month, d.day);
											value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', '');
										}
									}
									break;
								
								// TEMP - NSF
								case 'dateSent':
								case 'dateDue':
								case 'accepted':
									if (Zotero.Date.isSQLDate(value)) {
										var localDate = Zotero.Date.sqlToDate(value);
										value = Zotero.Date.dateToSQL(localDate).replace(' 00:00:00', '');
									}
									else {
										var d = Zotero.Date.strToDate(value);
										value = null;
										if (d.year && d.month != undefined && d.day) {
											d = new Date(d.year, d.month, d.day);
											value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', '');
										}
									}
									break;
								
								default:
									// TODO: generalize to all date rows/fields
									if (Zotero.ItemFields.isFieldOfBase(fieldName, 'date')) {
										// Parse 'yesterday'/'today'/'tomorrow' and convert to dates,
										// since it doesn't make sense for those to be actual metadata values
										var lc = value.toLowerCase();
										if (lc == 'yesterday' || lc == Zotero.getString('date.yesterday')) {
											value = Zotero.Date.dateToSQL(new Date(new Date().getTime() - 86400000)).substr(0, 10);
										}
										else if (lc == 'today' || lc == Zotero.getString('date.today')) {
											value = Zotero.Date.dateToSQL(new Date()).substr(0, 10);
										}
										else if (lc == 'tomorrow' || lc == Zotero.getString('date.tomorrow')) {
											value = Zotero.Date.dateToSQL(new Date(new Date().getTime() + 86400000)).substr(0, 10);
										}
									}
							}
						}
						
						yield this._modifyField(fieldName, value, this.saveOnEdit);
						
						var newVal = this.item.getField(fieldName);
					}
					
					// If box is still open (due to field not being modified and there not being
					// a refresh), close it manually
					if (textbox && textbox.parentNode) {
						elem = this.createValueElement(
							this.item.getField(fieldName),
							fieldName,
							tabindex
						);
						var box = textbox.parentNode;
						box.replaceChild(elem,textbox);
					}
					
					if(field === 'creator') {
						// Reset creator mode settings here so that flex attribute gets reset
						this.switchCreatorMode(row, (otherFields.fieldMode ? 1 : 0), true);
						
						if(Zotero.ItemTypes.getName(this.item.itemTypeID) === "bookSection") {
							var creatorTypeLabels = document.getAnonymousNodes(this)[0].getElementsByClassName("creator-type-label");
							document.getElementById("zotero-author-guidance").show(creatorTypeLabels[creatorTypeLabels.length-1]);
						}
					}
					
					if (this._tabDirection) {
						var focusBox = this._dynamicFields;
						this._focusNextField(focusBox, this._lastTabIndex, this._tabDirection == -1);
					}
				}, this);
				]]></body>
			</method>
			
			
			<method name="_rowIsClickable">
				<parameter name="fieldName"/>
				<body>
				<![CDATA[
				 	return this.clickByRow &&
						(this.clickable ||
							this._clickableFields.indexOf(fieldName) != -1);
				]]>
				</body>
			</method>
			
			
			<method name="_fieldIsClickable">
				<parameter name="fieldName"/>
				<body>
				<![CDATA[
					return !this.clickByRow &&
						((this.clickable && !Zotero.Items.isPrimaryField(fieldName))
							|| this._clickableFields.indexOf(fieldName) != -1);
				]]>
				</body>
			</method>
			
			<method name="_modifyField">
				<parameter name="field"/>
				<parameter name="value"/>
				<parameter name="save"/>
				<body><![CDATA[
					return Zotero.spawn(function* () {
						this.item.setField(field, value);
						if (save) {
							yield this.item.saveTx();
						}
					}, this);
				]]></body>
			</method>
			
			
			<method name="_getFieldValue">
				<parameter name="label"/>
				<body>
				<![CDATA[
					return label.firstChild
						? label.firstChild.nodeValue : label.value;
				]]>
				</body>
			</method>
			
			
			<method name="_setFieldValue">
				<parameter name="label"/>
				<parameter name="value"/>
				<body>
				<![CDATA[
					if (label.firstChild) {
						label.firstChild.nodeValue = value;
					}
					else {
						label.value = value;
					}
				]]>
				</body>
			</method>
			
			
			<!-- TODO: work with textboxes too -->
			<method name="textTransform">
				<parameter name="label"/>
				<parameter name="mode"/>
				<body>
				<![CDATA[
					var val = this._getFieldValue(label);
					switch (mode) {
						case 'title':
							var newVal = Zotero.Utilities.capitalizeTitle(val.toLowerCase(), true);
							break;
						case 'sentence':
							// capitalize the first letter, including after beginning punctuation
							// capitalize after ?, ! and remove space(s) before those as well as colon analogous to capitalizeTitle function
							// also deal with initial punctuation here - open quotes and Spanish beginning punctuation marks
							newVal = val.toLowerCase().replace(/\s*:/, ":");
							newVal = newVal.replace(/(([\?!]\s*|^)([\'\"¡¿“‘„«\s]+)?[^\s])/g, function (x) {
								return x.replace(/\s+/m, " ").toUpperCase();});
							break;
						default:
							throw ("Invalid transform mode '" + mode + "' in zoteroitembox.textTransform()");
					}
					this._setFieldValue(label, newVal);
					this._modifyField(label.getAttribute('fieldname'), newVal, this.saveOnEdit).done();
				]]>
				</body>
			</method>
			
			
			<method name="getCreatorFields">
				<parameter name="row"/>
				<body>
				<![CDATA[
					var typeID = row.getElementsByClassName('creator-type-label')[0].getAttribute('typeid');
					var label1 = row.getElementsByClassName('creator-name-box')[0].firstChild;
					var label2 = label1.parentNode.lastChild;
					
					var fields = {
						lastName: label1.firstChild ? label1.firstChild.nodeValue : label1.value,
						firstName: label2.firstChild ? label2.firstChild.nodeValue : label2.value,
						fieldMode: label1.getAttribute('fieldMode')
							? parseInt(label1.getAttribute('fieldMode')) : 0,
						creatorTypeID: parseInt(typeID),
					};
					
					// Ignore '(first)'
					if (fields.fieldMode == 1 || fields.firstName == this._defaultFirstName) {
						fields.firstName = '';
					}
					// Ignore '(last)' or '(name)'
					if (fields.lastName == this._defaultFullName
							|| fields.lastName == this._defaultLastName) {
						fields.lastName = '';
					}
					
					Zotero.debug("RETURNING FIELDS");
					Zotero.debug(fields);
					
					return fields;
				]]>
				</body>
			</method>
			
			
			<method name="modifyCreator">
				<parameter name="index"/>
				<parameter name="fields"/>
				<parameter name="skipSave"/>
				<body><![CDATA[
				return Zotero.Promise.try(function () {
					var libraryID = this.item.libraryID;
					var firstName = fields.firstName;
					var lastName = fields.lastName;
					var fieldMode = fields.fieldMode;
					var creatorTypeID = fields.creatorTypeID;
					
					var oldCreator = this.item.getCreator(index);
					
					// Don't save empty creators
					if (!firstName && !lastName){
						if (!oldCreator) {
							return;
						}
						this.item.removeCreator(index);
						if (this.saveOnEdit && !skipSave) {
							return this.item.saveTx();
						}
						return;
					}
					
					var changed = this.item.setCreator(index, fields);
					if (changed && this.saveOnEdit && !skipSave) {
						return this.item.saveTx();
					}
				}.bind(this));
				]]></body>
			</method>
			
			
			<!--
			  @return {Promise}
			-->
			<method name="swapNames">
				<body><![CDATA[
				return Zotero.Promise.try(function () {
					var row = Zotero.getAncestorByTagName(document.popupNode, 'row');
					var typeBox = row.getElementsByAttribute('popup', 'creator-type-menu')[0];
					var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
					var fields = this.getCreatorFields(row);
					var lastName = fields.lastName;
					var firstName = fields.firstName;
					fields.lastName = firstName;
					fields.firstName = lastName;
					return this.modifyCreator(creatorIndex, fields);
				}.bind(this));
				]]></body>
			</method>
			
			<!--
			  @return {Promise}
			-->
			<method name="moveCreator">
				<parameter name="index"/>
				<parameter name="moveUp"/>
				<body>
				<![CDATA[
				return Zotero.Promise.try(function () {
					if (index == 0 && moveUp) {
						Zotero.debug("Can't move up creator 0");
						return;
					}
					else if (index + 1 == this.item.numCreators() && !moveUp) {
						Zotero.debug("Can't move down last creator");
						return;
					}
					
					var newIndex = moveUp ? index - 1 : index + 1;
					var a = this.item.getCreator(index);
					var b = this.item.getCreator(newIndex);
					this.item.setCreator(newIndex, a);
					this.item.setCreator(index, b);
					if (this.saveOnEdit) {
						return this.item.saveTx();
					}
				}.bind(this));
				]]>
				</body>
			</method>
			
			
			<method name="_updateAutoCompleteParams">
				<parameter name="row"/>
				<parameter name="changedParams"/>
				<body>
				<![CDATA[
					var textboxes = row.getElementsByTagName('textbox');
					if (textboxes.length) {
						var t = textboxes[0];
						var params = JSON.parse(t.getAttribute('autocompletesearchparam'));
						for (var param in changedParams) {
							params[param] = changedParams[param];
						}
						t.setAttribute('autocompletesearchparam', JSON.stringify(params));
					}
				]]>
				</body>
			</method>
			
			
			<method name="focusFirstField">
				<body>
				<![CDATA[
					this._focusNextField(this._dynamicFields, 0, false);
				]]>
				</body>
			</method>
			
			
			<!-- 
				Advance the field focus forward or backward
				
				Note: We're basically replicating the built-in tabindex functionality,
				which doesn't work well with the weird label/textbox stuff we're doing.
				(The textbox being tabbed away from is deleted before the blur()
				completes, so it doesn't know where it's supposed to go next.)
			-->
			<method name="_focusNextField">
				<parameter name="box"/>
				<parameter name="tabindex"/>
				<parameter name="back"/>
				<body>
				<![CDATA[
					tabindex = parseInt(tabindex);
					
					// Get all fields with ztabindex attributes
					var tabbableFields = box.querySelectorAll('label[ztabindex]');
					
					if (!tabbableFields.length) {
						Zotero.debug("No tabbable fields found");
						return false;
					}
					
					var next;
					if (back) {
						Zotero.debug('Looking for previous tabindex before ' + tabindex, 4);
						for (let i = tabbableFields.length - 1; i >= 0; i--) {
							if (parseInt(tabbableFields[i].getAttribute('ztabindex')) < tabindex) {
								next = tabbableFields[i];
								break;
							}
						}
					}
					else {
					Zotero.debug('Looking for next tabindex after ' + tabindex, 4);
						for (var pos = 0; pos < tabbableFields.length; pos++) {
							if (parseInt(tabbableFields[pos].getAttribute('ztabindex')) > tabindex) {
								next = tabbableFields[pos];
								break;
							}
						}
					}
					
					if (!next) {
						Zotero.debug("Next field not found");
						return false;
					}
					
					next.click();
					
					// 1) next.parentNode is always null for some reason
					// 2) For some reason it's necessary to scroll to the next element when
					// moving forward for the target element to be fully in view
					if (!back && tabbableFields[pos + 1]) {
						Zotero.debug("Scrolling to next field");
						var visElem = tabbableFields[pos + 1];
					}
					else {
						var visElem = next;
					}
					// DEBUG: This doesn't seem to work anymore
					this.ensureElementIsVisible(visElem);
					
					return true;
				]]>
				</body>
			</method>
			
			
			<method name="blurOpenField">
				<body><![CDATA[
					return Zotero.spawn(function* () {
						var textboxes = document.getAnonymousNodes(this)[0].getElementsByTagName('textbox');
						if (textboxes && textboxes.length) {
							yield this.blurHandler(textboxes[0]);
						}
					}, this);
				]]></body>
			</method>
			
			
			<!--
				Available handlers:
				
				  - 'itemtypechange'
				
				Note: 'this' in the function will be bound to the item box.
			-->
			<method name="addHandler">
				<parameter name="eventName"/>
				<parameter name="func"/>
				<body>
				<![CDATA[
					if (!this.eventHandlers[eventName]) {
						this.eventHandlers[eventName] = [];
					}
					this.eventHandlers[eventName].push(func);
				]]>
				</body>
			</method>
			
			<method name="removeHandler">
				<parameter name="eventName"/>
				<parameter name="func"/>
				<body>
				<![CDATA[
					if (!this.eventHandlers[eventName]) {
						return;
					}
					var pos = this.eventHandlers[eventName].indexOf(func);
					if (pos != -1) {
						this.eventHandlers[eventName].splice(pos, 1);
					}
				]]>
				</body>
			</method>
			
			
			<method name="_id">
				<parameter name="id"/>
				<body>
				<![CDATA[
					return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0];
				]]>
				</body>
			</method>
		</implementation>
		
		<content>
			<scrollbox id="item-box" flex="1" orient="vertical"
				 	xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
				<popupset>
					<menupopup id="creator-type-menu" position="after_start"
						onpopupshowing="var typeBox = document.popupNode.localName == 'hbox' ? document.popupNode : document.popupNode.parentNode;
								var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
								
								var item = document.getBindingParent(this).item;
								var exists = item.hasCreatorAt(index);
								var moreCreators = item.numCreators() > index + 1;
								
								var hideMoveUp = !exists || index == 0;
								var hideMoveDown = !exists || !moreCreators;
								var hideMoveSep = hideMoveUp &amp;&amp; hideMoveDown;
								
								document.getElementById('zotero-creator-move-sep').setAttribute('hidden', hideMoveSep);
								document.getElementById('zotero-creator-move-up').setAttribute('hidden', hideMoveUp);
								document.getElementById('zotero-creator-move-down').setAttribute('hidden', hideMoveDown);"
						oncommand="var typeBox = document.popupNode.localName == 'hbox' ? document.popupNode : document.popupNode.parentNode;
							var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
							
							var itemBox = document.getBindingParent(this);
							
							if (event.explicitOriginalTarget.className == 'zotero-creator-move') {
								var up = event.explicitOriginalTarget.id == 'zotero-creator-move-up';
								itemBox.moveCreator(index, up);
								return;
							}
							
							var typeID = event.explicitOriginalTarget.getAttribute('typeid');
							var row = typeBox.parentNode;
							var fields = itemBox.getCreatorFields(row);
							fields.creatorTypeID = typeID;
							typeBox.getElementsByTagName('label')[0].setAttribute(
								'value',
								Zotero.getString(
									'creatorTypes.' + Zotero.CreatorTypes.getName(typeID)
								) + ':'
							);
							typeBox.setAttribute('typeid', typeID);
							
							/* If a creator textbox is already open, we need to
							change its autocomplete parameters so that it
							completes on a creator with a different creator type */
							var changedParams = {
								creatorTypeID: typeID
							};
							itemBox._updateAutoCompleteParams(row, changedParams);
							
							itemBox.modifyCreator(index, fields);"/>
					<menupopup id="zotero-field-transform-menu">
						<menu label="&zotero.item.textTransform;">
							<menupopup>
								<menuitem label="&zotero.item.textTransform.titlecase;" class="menuitem-non-iconic"
									oncommand="document.getBindingParent(this).textTransform(document.popupNode, 'title')"/>
								<menuitem label="&zotero.item.textTransform.sentencecase;" class="menuitem-non-iconic"
									oncommand="document.getBindingParent(this).textTransform(document.popupNode, 'sentence')"/>
							</menupopup>
						</menu>
					</menupopup>
					<menupopup id="zotero-creator-transform-menu"
						onpopupshowing="var row = Zotero.getAncestorByTagName(document.popupNode, 'row');
							var typeBox = row.getElementsByAttribute('popup', 'creator-type-menu')[0];
							var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
							var item = document.getBindingParent(this).item;
							var exists = item.hasCreatorAt(index);
							if (exists) {
								var fieldMode = item.getCreator(index).name !== undefined ? 1 : 0;
							}
							var hideTransforms = !exists || !!fieldMode;
							return !hideTransforms;">
						<menuitem label="&zotero.item.creatorTransform.nameSwap;"
							oncommand="document.getBindingParent(this).swapNames();"/>
					</menupopup>
					<zoteroguidancepanel id="zotero-author-guidance" about="authorMenu" position="after_end" x="-25"/>
				</popupset>
				<grid flex="1">
					<columns>
						<column/>
						<column flex="1"/>
					</columns>
					<rows id="dynamic-fields" flex="1">
						<row class="zotero-item-first-row">
							<label value="&zotero.items.itemType;:"/>
							<menulist class="zotero-clicky" id="item-type-menu" oncommand="document.getBindingParent(this).changeTypeTo(this.value, this)" flex="1"
								onfocus="document.getBindingParent(this).ensureElementIsVisible(this)"
								onkeypress="if (event.keyCode == event.DOM_VK_TAB) { document.getBindingParent(this).itemTypeMenuTab(event); }">
								<menupopup/>
							</menulist>
						</row>
					</rows>
				</grid>
			</scrollbox>
		</content>
	</binding>
</bindings>
