www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

itembox.xml (78199B)


      1 <?xml version="1.0"?>
      2 <!--
      3     ***** BEGIN LICENSE BLOCK *****
      4     
      5     Copyright © 2009 Center for History and New Media
      6                      George Mason University, Fairfax, Virginia, USA
      7                      http://zotero.org
      8     
      9     This file is part of Zotero.
     10     
     11     Zotero is free software: you can redistribute it and/or modify
     12     it under the terms of the GNU Affero General Public License as published by
     13     the Free Software Foundation, either version 3 of the License, or
     14     (at your option) any later version.
     15     
     16     Zotero is distributed in the hope that it will be useful,
     17     but WITHOUT ANY WARRANTY; without even the implied warranty of
     18     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     19     GNU Affero General Public License for more details.
     20     
     21     You should have received a copy of the GNU Affero General Public License
     22     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     23     
     24     ***** END LICENSE BLOCK *****
     25 -->
     26 
     27 <!DOCTYPE bindings SYSTEM "chrome://zotero/locale/zotero.dtd">
     28 <!-- <!DOCTYPE bindings SYSTEM "chrome://zotero/locale/itembox.dtd"> -->
     29 
     30 <bindings xmlns="http://www.mozilla.org/xbl"
     31 		  xmlns:xbl="http://www.mozilla.org/xbl"
     32 		  xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
     33 	
     34 	<binding id="item-box">
     35 		<resources>
     36 			<stylesheet src="chrome://zotero/skin/bindings/itembox.css"/>
     37 			<stylesheet src="chrome://zotero-platform/content/itembox.css"/>
     38 		</resources>
     39 		
     40 		<implementation>
     41 			<!--
     42 				Public properties
     43 			-->
     44 			<field name="clickable">false</field>
     45 			<field name="editable">false</field>
     46 			<field name="saveOnEdit">false</field>
     47 			<field name="showTypeMenu">false</field>
     48 			<field name="hideEmptyFields">false</field>
     49 			<field name="clickByRow">false</field> <!-- Click entire row rather than just field value -->
     50 			<field name="clickByItem">false</field>
     51 			
     52 			<field name="clickHandler"/>
     53 			<field name="blurHandler"/>
     54 			<field name="eventHandlers">[]</field>
     55 			
     56 			<field name="_initialVisibleCreators">5</field>
     57 			<field name="_displayAllCreators"/>
     58 			
     59 			<!-- Modes are predefined settings groups for particular tasks -->
     60 			<field name="_mode">"view"</field>
     61 			<property name="mode" onget="return this._mode;">
     62 				<setter>
     63 				<![CDATA[
     64 					this.clickable = false;
     65 					this.editable = false;
     66 					this.saveOnEdit = false;
     67 					this.showTypeMenu = false;
     68 					this.hideEmptyFields = false;
     69 					this.clickByRow = false;
     70 					this.clickByItem = false;
     71 					
     72 					switch (val) {
     73 						case 'view':
     74 						case 'merge':
     75 							break;
     76 						
     77 						case 'edit':
     78 							this.clickable = true;
     79 							this.editable = true;
     80 							this.saveOnEdit = true
     81 							this.showTypeMenu = true;
     82 							this.clickHandler = this.showEditor;
     83 							this.blurHandler = this.hideEditor;
     84 							break;
     85 						
     86 						case 'fieldmerge':
     87 							this.hideEmptyFields = true;
     88 							this._fieldAlternatives = {};
     89 							break;
     90 						
     91 						default:
     92 							throw ("Invalid mode '" + val + "' in itembox.xml");
     93 					}
     94 					
     95 					this._mode = val;
     96 					document.getAnonymousNodes(this)[0].setAttribute('mode', val);
     97 				]]>
     98 				</setter>
     99 			</property>
    100 			
    101 			<field name="_item"/>
    102 			<property name="item" onget="return this._item;">
    103 				<setter><![CDATA[
    104 					if (!(val instanceof Zotero.Item)) {
    105 						throw new Error("'item' must be a Zotero.Item");
    106 					}
    107 					
    108 					// When changing items, reset truncation of creator list
    109 					if (!this._item || val.id != this._item.id) {
    110 						this._displayAllCreators = false;
    111 					}
    112 					
    113 					this._item = val;
    114 					this.refresh();
    115 				]]></setter>
    116 			</property>
    117 			
    118 			<!-- .ref is an alias for .item -->
    119 			<property name="ref"
    120 				onget="return this._item;"
    121 				onset="this.item = val; this.refresh();">
    122 			</property>
    123 			
    124 			
    125 			<!--
    126 				 An array of field names that should be shown
    127 				 even if they're empty and hideEmptyFields is set
    128 			-->
    129 			<field name="_visibleFields">[]</field>
    130 			<property name="visibleFields">
    131 				<setter>
    132 				<![CDATA[
    133 					if (val.constructor.name != 'Array') {
    134 						throw ('visibleFields must be an array in <itembox>.visibleFields');
    135 					}
    136 					
    137 					this._visibleFields = val;
    138 				]]>
    139 				</setter>
    140 			</property>
    141 			
    142 			<!--
    143 				 An array of field names that should be hidden
    144 			-->
    145 			<field name="_hiddenFields">[]</field>
    146 			<property name="hiddenFields">
    147 				<setter>
    148 				<![CDATA[
    149 					if (val.constructor.name != 'Array') {
    150 						throw ('hiddenFields must be an array in <itembox>.visibleFields');
    151 					}
    152 					
    153 					this._hiddenFields = val;
    154 				]]>
    155 				</setter>
    156 			</property>
    157 			
    158 			<!--
    159 				 An array of field names that should be clickable
    160 				 even if this.clickable is false
    161 			-->
    162 			<field name="_clickableFields">[]</field>
    163 			<property name="clickableFields">
    164 				<setter>
    165 				<![CDATA[
    166 					if (val.constructor.name != 'Array') {
    167 						throw ('clickableFields must be an array in <itembox>.clickableFields');
    168 					}
    169 					
    170 					this._clickableFields = val;
    171 				]]>
    172 				</setter>
    173 			</property>
    174 			
    175 			<!--
    176 				 An array of field names that should be editable
    177 				 even if this.editable is false
    178 			-->
    179 			<field name="_editableFields">[]</field>
    180 			<property name="editableFields">
    181 				<setter>
    182 				<![CDATA[
    183 					if (val.constructor.name != 'Array') {
    184 						throw ('editableFields must be an array in <itembox>.editableFields');
    185 					}
    186 					
    187 					this._editableFields = val;
    188 				]]>
    189 				</setter>
    190 			</property>
    191 			
    192 			<!--
    193 				 An object of alternative values for keyed fields
    194 				 
    195 			-->
    196 			<field name="_fieldAlternatives">{}</field>
    197 			<property name="fieldAlternatives">
    198 				<setter>
    199 				<![CDATA[
    200 					if (val.constructor.name != 'Object') {
    201 						throw ('fieldAlternatives must be an Object in <itembox>.fieldAlternatives');
    202 					}
    203 					
    204 					if (this.mode != 'fieldmerge') {
    205 						throw ('fieldAlternatives is valid only in fieldmerge mode in <itembox>.fieldAlternatives');
    206 					}
    207 					
    208 					this._fieldAlternatives = val;
    209 				]]>
    210 				</setter>
    211 			</property>
    212 			
    213 			<!--
    214 				 An array of field names in the order they should appear
    215 				 in the list; empty spaces can be created with null
    216 			-->
    217 			<field name="_fieldOrder">[]</field>
    218 			<property name="fieldOrder">
    219 				<setter>
    220 				<![CDATA[
    221 					if (val.constructor.name != 'Array') {
    222 						throw ('fieldOrder must be an array in <itembox>.fieldOrder');
    223 					}
    224 					
    225 					this._fieldOrder = val;
    226 				]]>
    227 				</setter>
    228 			</property>
    229 			
    230 			<property name="itemTypeMenu" onget="return this._id('item-type-menu')"/>
    231 			
    232 			<!-- Private properties -->
    233 			<property name="_dynamicFields" onget="return this._id('dynamic-fields')"/>
    234 			<property name="_creatorTypeMenu" onget="return this._id('creator-type-menu')"/>
    235 			
    236 			<field name="_selectField"/>
    237 			<field name="_beforeRow"/>
    238 			<field name="_addCreatorRow"/>
    239 			<field name="_creatorCount"/>
    240 			
    241 			<field name="_lastTabIndex"/>
    242 			<field name="_tabDirection"/>
    243 			<field name="_tabIndexMinCreators" readonly="true">10</field>
    244 			<field name="_tabIndexMaxCreators">0</field>
    245 			<field name="_tabIndexMinFields" readonly="true">1000</field>
    246 			<field name="_tabIndexMaxFields">0</field>
    247 			
    248 			<property name="_defaultFirstName"
    249 				onget="return '(' + Zotero.getString('pane.item.defaultFirstName') + ')'"/>
    250 			<property name="_defaultLastName"
    251 				onget="return '(' + Zotero.getString('pane.item.defaultLastName') + ')'"/>
    252 			<property name="_defaultFullName"
    253 				onget="return '(' + Zotero.getString('pane.item.defaultFullName') + ')'"/>
    254 			
    255 			<constructor>
    256 			<![CDATA[
    257 				this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itembox');
    258 			]]>
    259 			</constructor>
    260 			
    261 			<destructor>
    262 			<![CDATA[
    263 				Zotero.Notifier.unregisterObserver(this._notifierID);
    264 			]]>
    265 			</destructor>
    266 			
    267 			<method name="notify">
    268 				<parameter name="event"/>
    269 				<parameter name="type"/>
    270 				<parameter name="ids"/>
    271 				<body><![CDATA[
    272 					if (event != 'modify' || !this.item || !this.item.id) return;
    273 					for (let i = 0; i < ids.length; i++) {
    274 						let id = ids[i];
    275 						if (id != this.item.id) {
    276 							continue;
    277 						}
    278 						this.refresh();
    279 						break;
    280 					}
    281 				]]></body>
    282 			</method>
    283 			
    284 			<method name="refresh">
    285 				<body>
    286 				<![CDATA[
    287 					Zotero.debug('Refreshing item box');
    288 					
    289 					if (!this.item) {
    290 						Zotero.debug('No item to refresh', 2);
    291 						return;
    292 					}
    293 					
    294 					if (this.clickByItem) {
    295 						var itemBox = document.getAnonymousNodes(this)[0];
    296 						itemBox.setAttribute('onclick',
    297 							'document.getBindingParent(this).clickHandler(this)');
    298 					}
    299 					
    300 					// Item type menu
    301 					if (this.showTypeMenu) {
    302 						// Build item type menu if it hasn't been built yet
    303 						if (this.itemTypeMenu.itemCount == 0) {
    304 							this.buildItemTypeMenu();
    305 						}
    306 						else {
    307 							this.updateItemTypeMenuSelection();
    308 						}
    309 						
    310 						this.itemTypeMenu.parentNode.hidden = false;
    311 					}
    312 					else {
    313 						this.itemTypeMenu.parentNode.hidden = true;
    314 					}
    315 					
    316 					//
    317 					// Clear and rebuild metadata fields
    318 					//
    319 					while (this._dynamicFields.childNodes.length > 1) {
    320 						this._dynamicFields.removeChild(this._dynamicFields.lastChild);
    321 					}
    322 					
    323 					var fieldNames = [];
    324 					
    325 					// Manual field order
    326 					if (this._fieldOrder.length) {
    327 						for (let field of this._fieldOrder) {
    328 							fieldNames.push(field);
    329 						}
    330 					}
    331 					// Get field order from database
    332 					else {
    333 						if (!this.showTypeMenu) {
    334 							fieldNames.push("itemType");
    335 						}
    336 						
    337 						var fields = Zotero.ItemFields.getItemTypeFields(this.item.getField("itemTypeID"));
    338 						
    339 						for (var i=0; i<fields.length; i++) {
    340 							fieldNames.push(Zotero.ItemFields.getName(fields[i]));
    341 						}
    342 						
    343 						if (!(this.item instanceof Zotero.FeedItem)) {
    344 							fieldNames.push("dateAdded", "dateModified");
    345 						}
    346 					}
    347 					
    348 					for (var i=0; i<fieldNames.length; i++) {
    349 						var fieldName = fieldNames[i];
    350 						var val = '';
    351 						
    352 						if (fieldName) {
    353 							var fieldID = Zotero.ItemFields.getID(fieldName);
    354 							if (fieldID && !Zotero.ItemFields.isValidForType(fieldID, this.item.itemTypeID)) {
    355 								fieldName = null;
    356 							}
    357 						}
    358 						
    359 						if (fieldName) {
    360 							if (this._hiddenFields.indexOf(fieldName) != -1) {
    361 								continue;
    362 							}
    363 							
    364 							// createValueElement() adds the itemTypeID as an attribute
    365 							// and converts it to a localized string for display
    366 							if (fieldName == 'itemType') {
    367 								val = this.item.itemTypeID;
    368 							}
    369 							else {
    370 								val = this.item.getField(fieldName);
    371 							}
    372 							
    373 							if (!val && this.hideEmptyFields
    374 									&& this._visibleFields.indexOf(fieldName) == -1
    375 									&& (this.mode != 'fieldmerge' || typeof this._fieldAlternatives[fieldName] == 'undefined')) {
    376 								continue;
    377 							}
    378 							
    379 							var fieldIsClickable = this._fieldIsClickable(fieldName);
    380 							
    381 							// Start tabindex at 1001 after creators
    382 							var tabindex = fieldIsClickable
    383 								? (i>0 ? this._tabIndexMinFields + i : 1) : 0;
    384 							this._tabIndexMaxFields = Math.max(this._tabIndexMaxFields, tabindex);
    385 							
    386 							if (fieldIsClickable
    387 									&& !Zotero.Items.isPrimaryField(fieldName)
    388 									&& (Zotero.ItemFields.isFieldOfBase(Zotero.ItemFields.getID(fieldName), 'date')
    389 										// TEMP - filingDate
    390 										|| fieldName == 'filingDate')
    391 									// TEMP - NSF
    392 									&& fieldName != 'dateSent') {
    393 								this.addDateRow(fieldNames[i], this.item.getField(fieldName, true), tabindex);
    394 								continue;
    395 							}
    396 						}
    397 						
    398 						let label = document.createElement("label");
    399 						label.setAttribute('fieldname', fieldName);
    400 						
    401 						let valueElement = this.createValueElement(
    402 							val, fieldName, tabindex
    403 						);
    404 						
    405 						var prefix = '';
    406 						// Add '(...)' before 'Abstract' for collapsed abstracts
    407 						if (fieldName == 'abstractNote') {
    408 							if (val && !Zotero.Prefs.get('lastAbstractExpand')) {
    409 								prefix = '(\u2026) ';
    410 							}
    411 						}
    412 						
    413 						if (fieldName) {
    414 							label.setAttribute("value", prefix +
    415 								Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, fieldName));
    416 						}
    417 						
    418 						// TEMP - NSF (homepage)
    419 						if ((fieldName == 'url' || fieldName == 'homepage') && val) {
    420 							label.classList.add("pointer");
    421 							// TODO: make getFieldValue non-private and use below instead
    422 							label.setAttribute("onclick", "ZoteroPane_Local.loadURI(this.nextSibling.firstChild ? this.nextSibling.firstChild.nodeValue : this.nextSibling.value, event)");
    423 							label.setAttribute("tooltiptext", Zotero.getString('locate.online.tooltip'));
    424 						}
    425 						else if (fieldName == 'DOI' && val && typeof val == 'string') {
    426 							// Pull out DOI, in case there's a prefix
    427 							var doi = Zotero.Utilities.cleanDOI(val);
    428 							if (doi) {
    429 								doi = "https://doi.org/" + encodeURIComponent(doi);
    430 								label.classList.add("pointer");
    431 								label.setAttribute("onclick", "ZoteroPane_Local.loadURI('" + doi + "', event)");
    432 								label.setAttribute("tooltiptext", Zotero.getString('locate.online.tooltip'));
    433 								valueElement.setAttribute('contextmenu', 'zotero-doi-menu');
    434 								
    435 								var openURLMenuItem = document.getElementById('zotero-doi-menu-view-online');
    436 								openURLMenuItem.setAttribute("oncommand", "ZoteroPane_Local.loadURI('" + doi + "', event)");
    437 								
    438 								var copyMenuItem = document.getElementById('zotero-doi-menu-copy');
    439 								copyMenuItem.setAttribute("oncommand", "Zotero.Utilities.Internal.copyTextToClipboard('" + doi + "')");
    440 							}
    441 						}
    442 						else if (fieldName == 'abstractNote') {
    443 							if (val.length) {
    444 								label.classList.add("pointer");
    445 							}
    446 							label.addEventListener('click', function () {
    447 								if (this.nextSibling.inputField) {
    448 									this.nextSibling.inputField.blur();
    449 								}
    450 								else {
    451 									document.getBindingParent(this).toggleAbstractExpand(
    452 										this, this.nextSibling
    453 									);
    454 								}
    455 							});
    456 						}
    457 						else {
    458 							label.setAttribute("onclick",
    459 								"if (this.nextSibling.inputField) { this.nextSibling.inputField.blur(); }");
    460 						}
    461 						
    462 						var row = this.addDynamicRow(label, valueElement);
    463 						
    464 						if (fieldName && this._selectField == fieldName) {
    465 							this.showEditor(valueElement);
    466 						}
    467 						
    468 						// In field merge mode, add a button to switch field versions
    469 						else if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') {
    470 							var button = document.createElement("toolbarbutton");
    471 							button.className = 'zotero-field-version-button';
    472 							button.setAttribute('image', 'chrome://zotero/skin/treesource-duplicates.png');
    473 							button.setAttribute('type', 'menu');
    474 							
    475 							var popup = button.appendChild(document.createElement("menupopup"));
    476 							
    477 							for (let v of this._fieldAlternatives[fieldName]) {
    478 								var menuitem = document.createElement("menuitem");
    479 								var sv = Zotero.Utilities.ellipsize(v, 60);
    480 								menuitem.setAttribute('label', sv);
    481 								if (v != sv) {
    482 									menuitem.setAttribute('tooltiptext', v);
    483 								}
    484 								menuitem.setAttribute('fieldName', fieldName);
    485 								menuitem.setAttribute('originalValue', v);
    486 								menuitem.setAttribute(
    487 									'oncommand',
    488 									"var binding = document.getBindingParent(this); "
    489 									+ "var item = binding.item; "
    490 									+ "item.setField(this.getAttribute('fieldName'), this.getAttribute('originalValue')); "
    491 									+ "var row = Zotero.getAncestorByTagName(this, 'row'); "
    492 									+ "binding.refresh();"
    493 								);
    494 								popup.appendChild(menuitem);
    495 							}
    496 							
    497 							row.appendChild(button);
    498 						}
    499 					}
    500 					this._selectField = false;
    501 					
    502 					//
    503 					// Creators
    504 					//
    505 					
    506 					// Creator type menu
    507 					if (this.editable) {
    508 						while (this._creatorTypeMenu.hasChildNodes()) {
    509 							this._creatorTypeMenu.removeChild(this._creatorTypeMenu.firstChild);
    510 						}
    511 						
    512 						var creatorTypes = Zotero.CreatorTypes.getTypesForItemType(this.item.itemTypeID);
    513 
    514 						var localized = {};
    515 						for (var i=0; i<creatorTypes.length; i++) {
    516 							localized[creatorTypes[i]['name']]
    517 								= Zotero.getString('creatorTypes.' + creatorTypes[i]['name']);
    518 						}
    519 						
    520 						for (var i in localized) {
    521 							var menuitem = document.createElement("menuitem");
    522 							menuitem.setAttribute("label", localized[i]);
    523 							menuitem.setAttribute("typeid", Zotero.CreatorTypes.getID(i));
    524 							this._creatorTypeMenu.appendChild(menuitem);
    525 						}
    526 						
    527 						var moveSep = document.createElement("menuseparator");
    528 						var moveUp = document.createElement("menuitem");
    529 						var moveDown = document.createElement("menuitem");
    530 						moveSep.id = "zotero-creator-move-sep";
    531 						moveUp.id = "zotero-creator-move-up";
    532 						moveDown.id = "zotero-creator-move-down";
    533 						moveUp.className = "zotero-creator-move";
    534 						moveDown.className = "zotero-creator-move";
    535 						moveUp.setAttribute("label", Zotero.getString('pane.item.creator.moveUp'));
    536 						moveDown.setAttribute("label", Zotero.getString('pane.item.creator.moveDown'));
    537 						this._creatorTypeMenu.appendChild(moveSep);
    538 						this._creatorTypeMenu.appendChild(moveUp);
    539 						this._creatorTypeMenu.appendChild(moveDown);
    540 					}
    541 					
    542 					// Creator rows
    543 					
    544 					// Place, in order of preference, after title, after type,
    545 					// or at beginning
    546 					var titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
    547 					var field = this._dynamicFields.getElementsByAttribute('fieldname', Zotero.ItemFields.getName(titleFieldID)).item(0);
    548 					if (!field) {
    549 						var field = this._dynamicFields.getElementsByAttribute('fieldname', 'itemType').item(0);
    550 					}
    551 					if (field) {
    552 						this._beforeRow = field.parentNode.nextSibling;
    553 					}
    554 					else {
    555 						this._beforeRow = this._dynamicFields.firstChild;
    556 					}
    557 					
    558 					this._creatorCount = 0;
    559 					var num = this.item.numCreators();
    560 					if (num > 0) {
    561 						// Limit number of creators display
    562 						var max = Math.min(num, this._initialVisibleCreators);
    563 						// If only 1 or 2 more, just display
    564 						if (num < max + 3 || this._displayAllCreators) {
    565 							max = num;
    566 						}
    567 						for (var i = 0; i < max; i++) {
    568 							let data = this.item.getCreator(i);
    569 							this.addCreatorRow(data, data.creatorTypeID);
    570 							
    571 							// Display "+" button on all but last row
    572 							if (i == max - 2) {
    573 								this.disableCreatorAddButtons();
    574 							}
    575 						}
    576 						
    577 						// Additional creators not displayed
    578 						if (num > max) {
    579 							this.addMoreCreatorsRow(num - max);
    580 							
    581 							this.disableCreatorAddButtons();
    582 						}
    583 						else {
    584 							// If we didn't start with creators truncated,
    585 							// don't truncate for as long as we're viewing
    586 							// this item, so that added creators aren't
    587 							// immediately hidden
    588 							this._displayAllCreators = true;
    589 							
    590 							if (this._addCreatorRow) {
    591 								this.addCreatorRow(false, this.item.getCreator(max-1).creatorTypeID, true);
    592 								this._addCreatorRow = false;
    593 								this.disableCreatorAddButtons();
    594 							}
    595 						}
    596 					}
    597 					else if (this.editable && Zotero.CreatorTypes.itemTypeHasCreators(this.item.itemTypeID)) {
    598 						// Add default row
    599 						this.addCreatorRow(false, false, true, true);
    600 						this.disableCreatorAddButtons();
    601 					}
    602 					
    603 					// Move to next or previous field if (shift-)tab was pressed
    604 					if (this._lastTabIndex && this._lastTabIndex != -1) {
    605 						this._focusNextField(this._lastTabIndex);
    606 					}
    607 					
    608 					this._refreshed = true;
    609 				]]>
    610 				</body>
    611 			</method>
    612 			
    613 			
    614 			<method name="buildItemTypeMenu">
    615 				<body>
    616 				<![CDATA[
    617 					if (!this.item) {
    618 						return;
    619 					}
    620 					
    621 					this.itemTypeMenu.removeAllItems();
    622 					
    623 					var t = Zotero.ItemTypes.getTypes();
    624 							
    625 					// Sort by localized name
    626 					var itemTypes = [];
    627 					for (var i=0; i<t.length; i++) {
    628 						itemTypes.push({
    629 							id: t[i].id,
    630 							name: t[i].name,
    631 							localized: Zotero.ItemTypes.getLocalizedString(t[i].id)
    632 						});
    633 					}
    634 					var collation = Zotero.getLocaleCollation();
    635 					itemTypes.sort(function(a, b) {
    636 						return collation.compareString(1, a.localized, b.localized);
    637 					});
    638 					
    639 					for (var i=0; i<itemTypes.length; i++) {
    640 						var name = itemTypes[i].name;
    641 						if (name != 'attachment' && name != 'note') {
    642 							this.itemTypeMenu.appendItem(itemTypes[i].localized, itemTypes[i].id);
    643 						}
    644 					}
    645 					
    646 					this.updateItemTypeMenuSelection();
    647 				]]>
    648 				</body>
    649 			</method>
    650 			
    651 			
    652 			<method name="updateItemTypeMenuSelection">
    653 				<body>
    654 				<![CDATA[
    655 					var listitems = this.itemTypeMenu.firstChild.childNodes;
    656 					for (var i=0, len=listitems.length; i < len; i++) {
    657 						if (listitems[i].getAttribute('value') == this.item.itemTypeID) {
    658 							this.itemTypeMenu.selectedIndex = i;
    659 						}
    660 					}
    661 				]]>
    662 				</body>
    663 			</method>
    664 			
    665 			
    666 			<method name="addDynamicRow">
    667 				<parameter name="label"/>
    668 				<parameter name="value"/>
    669 				<parameter name="beforeElement"/>
    670 				<body>
    671 				<![CDATA[
    672 					var row = document.createElement("row");
    673 					
    674 					// Add click event to row
    675 					if (this._rowIsClickable(value.getAttribute('fieldname'))) {
    676 						row.className = 'zotero-clicky';
    677 						row.addEventListener('click', function (event) {
    678 							document.getBindingParent(this).clickHandler(this);
    679 						}, false);
    680 					}
    681 					
    682 					row.appendChild(label);
    683 					row.appendChild(value);
    684 					if (beforeElement) {
    685 						this._dynamicFields.insertBefore(row, this._beforeRow);
    686 					}
    687 					else {
    688 						this._dynamicFields.appendChild(row);
    689 					}
    690 					
    691 					return row;
    692 				]]>
    693 				</body>
    694 			</method>
    695 			
    696 			
    697 			<method name="addCreatorRow">
    698 				<parameter name="creatorData"/>
    699 				<parameter name="creatorTypeIDOrName"/>
    700 				<parameter name="unsaved"/>
    701 				<parameter name="defaultRow"/>
    702 				<body>
    703 				<![CDATA[
    704 					// getCreatorFields(), switchCreatorMode() and handleCreatorAutoCompleteSelect()
    705 					// may need need to be adjusted if this DOM structure changes
    706 					
    707 					var fieldMode = Zotero.Prefs.get('lastCreatorFieldMode');
    708 					var firstName = '';
    709 					var lastName = '';
    710 					if (creatorData) {
    711 						fieldMode = creatorData.fieldMode;
    712 						firstName = creatorData.firstName;
    713 						lastName = creatorData.lastName;
    714 					}
    715 					
    716 					// Sub in placeholder text for empty fields
    717 					if (fieldMode == 1) {
    718 						if (lastName === "") {
    719 							lastName = this._defaultFullName;
    720 						}
    721 					}
    722 					else {
    723 						if (firstName === "") {
    724 							firstName = this._defaultFirstName;
    725 						}
    726 						if (lastName === "") {
    727 							lastName = this._defaultLastName;
    728 						}
    729 					}
    730 					
    731 					// Use the first entry in the drop-down for the default type if none specified
    732 					var typeID = creatorTypeIDOrName
    733 						? Zotero.CreatorTypes.getID(creatorTypeIDOrName)
    734 						: this._creatorTypeMenu.childNodes[0].getAttribute('typeid');
    735 					
    736 					var typeBox = document.createElement("hbox");
    737 					typeBox.setAttribute("typeid", typeID);
    738 					typeBox.setAttribute("popup", "creator-type-menu");
    739 					typeBox.setAttribute("fieldname", 'creator-' + this._creatorCount + '-typeID');
    740 					if (this.editable) {
    741 						typeBox.className = 'creator-type-label zotero-clicky';
    742 						var img = document.createElement('image');
    743 						typeBox.appendChild(img);
    744 					}
    745 					else {
    746 						typeBox.className = 'creator-type-label';
    747 					}
    748 					
    749 					var label = document.createElement("label");
    750 					label.setAttribute('value',
    751 						Zotero.getString('creatorTypes.' + Zotero.CreatorTypes.getName(typeID)));
    752 					typeBox.appendChild(label);
    753 					
    754 					var hbox = document.createElement("hbox");
    755 					hbox.className = 'creator-type-value';
    756 					
    757 					// Name
    758 					var firstlast = document.createElement("hbox");
    759 					firstlast.className = 'creator-name-box';
    760 					firstlast.setAttribute("flex","1");
    761 					var tabindex = this._tabIndexMinCreators + (this._creatorCount * 2);
    762 					var fieldName = 'creator-' + this._creatorCount + '-lastName';
    763 					var lastNameLabel = firstlast.appendChild(
    764 						this.createValueElement(
    765 							lastName,
    766 							fieldName,
    767 							tabindex
    768 						)
    769 					);
    770 					
    771 					// Comma
    772 					var comma = document.createElement('label');
    773 					comma.setAttribute('value', ',');
    774 					comma.className = 'comma';
    775 					firstlast.appendChild(comma);
    776 					
    777 					var fieldName = 'creator-' + this._creatorCount + '-firstName';
    778 					firstlast.appendChild(
    779 						this.createValueElement(
    780 							firstName,
    781 							fieldName,
    782 							tabindex + 1
    783 						)
    784 					);
    785 					if (fieldMode) {
    786 						firstlast.lastChild.setAttribute('hidden', true);
    787 					}
    788 					
    789 					if (this.editable && fieldMode == 0) {
    790 						firstlast.setAttribute('contextmenu', 'zotero-creator-transform-menu');
    791 					}
    792 					
    793 					this._tabIndexMaxCreators = Math.max(this._tabIndexMaxCreators, tabindex);
    794 					
    795 					hbox.appendChild(firstlast);
    796 					
    797 					// Single/double field toggle
    798 					var toggleButton = document.createElement('label');
    799 					toggleButton.setAttribute('fieldname',
    800 						'creator-' + this._creatorCount + '-fieldMode');
    801 					toggleButton.className = 'zotero-field-toggle zotero-clicky';
    802 					hbox.appendChild(toggleButton);
    803 					
    804 					// Minus (-) button
    805 					var removeButton = document.createElement('label');
    806 					removeButton.setAttribute("value","-");
    807 					removeButton.setAttribute("class","zotero-clicky zotero-clicky-minus");
    808 					// If default first row, don't let user remove it
    809 					if (defaultRow) {
    810 						this.disableButton(removeButton);
    811 					}
    812 					else {
    813 						removeButton.setAttribute("onclick",
    814 							"document.getBindingParent(this).removeCreator("
    815 							+ this._creatorCount
    816 							+ ", this.parentNode.parentNode)");
    817 					}
    818 					hbox.appendChild(removeButton);
    819 					
    820 					// Plus (+) button
    821 					var addButton = document.createElement('label');
    822 					addButton.setAttribute("value","+");
    823 					addButton.setAttribute("class", "zotero-clicky zotero-clicky-plus");
    824 					// If row isn't saved, don't let user add more
    825 					if (unsaved) {
    826 						this.disableButton(addButton);
    827 					}
    828 					else {
    829 						this._enablePlusButton(addButton, typeID, fieldMode);
    830 					}
    831 					hbox.appendChild(addButton);
    832 					
    833 					this._creatorCount++;
    834 					
    835 					if (!this.editable) {
    836 						toggleButton.hidden = true;
    837 						removeButton.hidden = true;
    838 						addButton.hidden = true;
    839 					}
    840 					
    841 					this.addDynamicRow(typeBox, hbox, true);
    842 					
    843 					// Set single/double field toggle mode
    844 					if (fieldMode) {
    845 						this.switchCreatorMode(hbox.parentNode, 1, true);
    846 					}
    847 					else {
    848 						this.switchCreatorMode(hbox.parentNode, 0, true);
    849 					}
    850 					
    851 					// Focus new rows
    852 					if (unsaved && !defaultRow){
    853 						lastNameLabel.click();
    854 					}
    855 				]]>
    856 				</body>
    857 			</method>
    858 			
    859 			
    860 			<method name="addMoreCreatorsRow">
    861 				<parameter name="num"/>
    862 				<body>
    863 				<![CDATA[
    864 					var box = document.createElement('box');
    865 					
    866 					var label = document.createElement('label');
    867 					label.id = 'more-creators-label';
    868 					label.setAttribute('value', Zotero.getString('general.numMore', num));
    869 					label.setAttribute('onclick',
    870 						"var binding = document.getBindingParent(this); "
    871 						+ "binding._displayAllCreators = true; "
    872 						+ "binding.refresh()"
    873 					);
    874 					
    875 					this.addDynamicRow(box, label, true);
    876 				]]>
    877 				</body>
    878 			</method>
    879 			
    880 			<method name="addDateRow">
    881 				<parameter name="field"/>
    882 				<parameter name="value"/>
    883 				<parameter name="tabindex"/>
    884 				<body>
    885 				<![CDATA[
    886 					var label = document.createElement("label");
    887 					label.setAttribute("value", Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, field));
    888 					label.setAttribute("fieldname", field);
    889 					label.setAttribute("onclick", "this.nextSibling.firstChild.blur()");
    890 					
    891 					var elem = this.createValueElement(
    892 						Zotero.Date.multipartToStr(value),
    893 						field,
    894 						tabindex
    895 					);
    896 					elem.setAttribute('flex', 1);
    897 					
    898 					// y-m-d status indicator
    899 					var ymd = document.createElement('label');
    900 					ymd.id = 'zotero-date-field-status';
    901 					ymd.setAttribute(
    902 						'value',
    903 						Zotero.Date.strToDate(Zotero.Date.multipartToStr(value))
    904 							.order.split('').join(' ')
    905 					);
    906 					
    907 					var hbox = document.createElement('hbox');
    908 					hbox.setAttribute('flex', 1);
    909 					hbox.className = "date-box";
    910 					hbox.appendChild(elem);
    911 					hbox.appendChild(ymd);
    912 					
    913 					this.addDynamicRow(label, hbox);
    914 				]]>
    915 				</body>
    916 			</method>
    917 			
    918 			
    919 			<method name="switchCreatorMode">
    920 				<parameter name="row"/>
    921 				<parameter name="fieldMode"/>
    922 				<parameter name="initial"/>
    923 				<parameter name="updatePref"/>
    924 				<body>
    925 				<![CDATA[
    926 					// Change if button position changes
    927 					// row->hbox->label->label->toolbarbutton
    928 					var button = row.lastChild.lastChild.previousSibling.previousSibling;
    929 					var hbox = button.previousSibling;
    930 					var lastName = hbox.firstChild;
    931 					var comma = hbox.firstChild.nextSibling;
    932 					var firstName = hbox.lastChild;
    933 					
    934 					// Switch to single-field mode
    935 					if (fieldMode == 1) {
    936 						button.style.background = `url("chrome://zotero/skin/textfield-dual${Zotero.hiDPISuffix}.png") center/21px auto no-repeat`;
    937 						button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.two'));
    938 						lastName.setAttribute('fieldMode', '1');
    939 						button.setAttribute('onclick', "document.getBindingParent(this).switchCreatorMode(Zotero.getAncestorByTagName(this, 'row'), 0, false, true)");
    940 						lastName.setAttribute('flex', '1');
    941 						delete lastName.style.width;
    942 						delete lastName.style.maxWidth;
    943 						
    944 						// Remove firstname field from tabindex
    945 						var tab = parseInt(firstName.getAttribute('ztabindex'));
    946 						firstName.setAttribute('ztabindex', -1);
    947 						if (this._tabIndexMaxCreators == tab) {
    948 							this._tabIndexMaxCreators--;
    949 						}
    950 						
    951 						// Hide first name field and prepend to last name field
    952 						firstName.setAttribute('hidden', true);
    953 						comma.setAttribute('hidden', true);
    954 						
    955 						if (!initial) {
    956 							var first = this._getFieldValue(firstName);
    957 							if (first && first != this._defaultFirstName) {
    958 								var last = this._getFieldValue(lastName);
    959 								this._setFieldValue(lastName, first + ' ' + last);
    960 							}
    961 						}
    962 						
    963 						if (this._getFieldValue(lastName) == this._defaultLastName) {
    964 							this._setFieldValue(lastName, this._defaultFullName);
    965 						}
    966 					}
    967 					// Switch to two-field mode
    968 					else {
    969 						button.style.background = `url("chrome://zotero/skin/textfield-single${Zotero.hiDPISuffix}.png") center/21px auto no-repeat`;
    970 						button.setAttribute('tooltiptext', Zotero.getString('pane.item.switchFieldMode.one'));
    971 						lastName.setAttribute('fieldMode', '0');
    972 						button.setAttribute('onclick', "document.getBindingParent(this).switchCreatorMode(Zotero.getAncestorByTagName(this, 'row'), 1, false, true)");
    973 						lastName.setAttribute('flex', '0');
    974 						
    975 						// appropriately truncate lastName
    976 						
    977 						// get item box width
    978 						var computedStyle = window.getComputedStyle(this, null);
    979 						var boxWidth = computedStyle.getPropertyValue('width');
    980 						// get field label width
    981 						var computedStyle = window.getComputedStyle(row.firstChild, null);
    982 						var leftHboxWidth = computedStyle.getPropertyValue('width');
    983 						// get last name width
    984 						computedStyle = window.getComputedStyle(lastName, null);
    985 						var lastNameWidth = computedStyle.getPropertyValue('width');
    986 						if(boxWidth.substr(-2) === 'px'
    987 								&& leftHboxWidth.substr(-2) === 'px'
    988 								&& lastNameWidth.substr(-2) === "px") {
    989 							// compute a maximum width
    990 							boxWidth = parseInt(boxWidth);
    991 							leftHboxWidth = parseInt(leftHboxWidth);
    992 							lastNameWidth = parseInt(lastNameWidth);
    993 							var maxWidth = boxWidth-leftHboxWidth-140;
    994 							if(lastNameWidth > maxWidth) {
    995 								lastName.style.width = maxWidth+"px";
    996 								lastName.style.maxWidth = maxWidth+"px";
    997 							} else {
    998 								delete lastName.style.width;
    999 								delete lastName.style.maxWidth;
   1000 							}
   1001 						}
   1002 						
   1003 						// Add firstname field to tabindex
   1004 						var tab = parseInt(lastName.getAttribute('ztabindex'));
   1005 						firstName.setAttribute('ztabindex', tab + 1);
   1006 						if (this._tabIndexMaxCreators == tab)
   1007 						{
   1008 							this._tabIndexMaxCreators++;
   1009 						}
   1010 						
   1011 						if (!initial) {
   1012 							// Move all but last word to first name field and show it
   1013 							var last = this._getFieldValue(lastName);
   1014 							if (last && last != this._defaultFullName) {
   1015 								var lastNameRE = /(.*?)[ ]*([^ ]+[ ]*)$/;
   1016 								var parts = lastNameRE.exec(last);
   1017 								if (parts[2] && parts[2] != last)
   1018 								{
   1019 									this._setFieldValue(lastName, parts[2]);
   1020 									this._setFieldValue(firstName, parts[1]);
   1021 								}
   1022 							}
   1023 						}
   1024 						
   1025 						if (!this._getFieldValue(firstName)) {
   1026 							this._setFieldValue(firstName, this._defaultFirstName);
   1027 						}
   1028 						
   1029 						if (this._getFieldValue(lastName) == this._defaultFullName) {
   1030 							this._setFieldValue(lastName, this._defaultLastName);
   1031 						}
   1032 						
   1033 						firstName.setAttribute('hidden', false);
   1034 						comma.setAttribute('hidden', false);
   1035 					}
   1036 					
   1037 					// Save the last-used field mode
   1038 					if (updatePref) {
   1039 						Zotero.debug("Switching lastCreatorFieldMode to " + fieldMode);
   1040 						Zotero.Prefs.set('lastCreatorFieldMode', fieldMode);
   1041 					}
   1042 					
   1043 					if (!initial)
   1044 					{
   1045 						var index = button.getAttribute('fieldname').split('-')[1];
   1046 						var fields = this.getCreatorFields(row);
   1047 						fields.fieldMode = fieldMode;
   1048 						this.modifyCreator(index, fields);
   1049 						if (this.saveOnEdit) {
   1050 							// See note in transformText()
   1051 							this.blurOpenField().then(() => this.item.saveTx());
   1052 						}
   1053 					}
   1054 				]]>
   1055 				</body>
   1056 			</method>
   1057 			
   1058 			
   1059 			<method name="scrollToTop">
   1060 				<body>
   1061 				<![CDATA[
   1062 					// DEBUG: Valid nsIScrollBoxObject but methods return errors
   1063 					try {
   1064 						var sbo = document.getAnonymousNodes(this)[0].boxObject;
   1065 						sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject);
   1066 						sbo.scrollTo(0,0);
   1067 					}
   1068 					catch (e) {
   1069 						Zotero.logError(e);
   1070 					}
   1071 				]]>
   1072 				</body>
   1073 			</method>
   1074 			
   1075 			
   1076 			<method name="ensureElementIsVisible">
   1077 				<parameter name="elem"/>
   1078 				<body>
   1079 				<![CDATA[
   1080 					var sbo = document.getAnonymousNodes(this)[0].boxObject;
   1081 					sbo.ensureElementIsVisible(elem);
   1082 				]]>
   1083 				</body>
   1084 			</method>
   1085 			
   1086 			
   1087 			<method name="changeTypeTo">
   1088 				<parameter name="itemTypeID"/>
   1089 				<parameter name="menu"/>
   1090 				<body><![CDATA[
   1091 				return (async function () {
   1092 					if (itemTypeID == this.item.itemTypeID) {
   1093 						return true;
   1094 					}
   1095 					
   1096 					if (this.saveOnEdit) {
   1097 						await this.blurOpenField();
   1098 						await this.item.saveTx();
   1099 					}
   1100 					
   1101 					var fieldsToDelete = this.item.getFieldsNotInType(itemTypeID, true);
   1102 					
   1103 					// Special cases handled below
   1104 					var bookTypeID = Zotero.ItemTypes.getID('book');
   1105 					var bookSectionTypeID = Zotero.ItemTypes.getID('bookSection');
   1106 					
   1107 					// Add warning for shortTitle when moving from book to bookSection
   1108 					// when title will be transferred
   1109 					if (this.item.itemTypeID == bookTypeID && itemTypeID == bookSectionTypeID) {
   1110 						var titleFieldID = Zotero.ItemFields.getID('title');
   1111 						var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
   1112 						if (this.item.getField(titleFieldID) && this.item.getField(shortTitleFieldID)) {
   1113 							if (!fieldsToDelete) {
   1114 								fieldsToDelete = [];
   1115 							}
   1116 							fieldsToDelete.push(shortTitleFieldID);
   1117 						}
   1118 					}
   1119 					
   1120 					// Generate list of localized field names for display in pop-up
   1121 					if (fieldsToDelete) {
   1122 						// Ignore warning for bookTitle when going from bookSection to book
   1123 						// if there's not also a title, since the book title is transferred
   1124 						// to title automatically in Zotero.Item.setType()
   1125 						if (this.item.itemTypeID == bookSectionTypeID && itemTypeID == bookTypeID) {
   1126 							var titleFieldID = Zotero.ItemFields.getID('title');
   1127 							var bookTitleFieldID = Zotero.ItemFields.getID('bookTitle');
   1128 							var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
   1129 							if (this.item.getField(bookTitleFieldID) && !this.item.getField(titleFieldID)) {
   1130 								var index = fieldsToDelete.indexOf(bookTitleFieldID);
   1131 								fieldsToDelete.splice(index, 1);
   1132 								// But warn for short title, which will be removed
   1133 								if (this.item.getField(shortTitleFieldID)) {
   1134 									fieldsToDelete.push(shortTitleFieldID);
   1135 								}
   1136 							}
   1137 						}
   1138 						
   1139 						var fieldNames = "";
   1140 						for (var i=0; i<fieldsToDelete.length; i++) {
   1141 							fieldNames += "\n - " +
   1142 								Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, fieldsToDelete[i]);
   1143 						}
   1144 						
   1145 						var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
   1146 							.getService(Components.interfaces.nsIPromptService);
   1147 					}
   1148 					
   1149 					if (!fieldsToDelete || fieldsToDelete.length == 0 ||
   1150 							promptService.confirm(null,
   1151 								Zotero.getString('pane.item.changeType.title'),
   1152 								Zotero.getString('pane.item.changeType.text') + "\n" + fieldNames)) {
   1153 						this.item.setType(itemTypeID);
   1154 						
   1155 						if (this.saveOnEdit) {
   1156 							// See note in transformText()
   1157 							await this.blurOpenField();
   1158 							await this.item.saveTx();
   1159 						}
   1160 						else {
   1161 							this.refresh();
   1162 						}
   1163 						
   1164 						if (this.eventHandlers['itemtypechange'] && this.eventHandlers['itemtypechange'].length) {
   1165 							this.eventHandlers['itemtypechange'].forEach(f => f.bind(this)());
   1166 						}
   1167 						
   1168 						return true;
   1169 					}
   1170 					
   1171 					// Revert the menu (which changes before the pop-up)
   1172 					if (menu) {
   1173 						menu.value = this.item.itemTypeID;
   1174 					}
   1175 					
   1176 					return false;
   1177 				}.bind(this))();
   1178 				]]></body>
   1179 			</method>
   1180 			
   1181 			
   1182 			<method name="toggleAbstractExpand">
   1183 				<parameter name="label"/>
   1184 				<parameter name="valueElement"/>
   1185 				<body>
   1186 				<![CDATA[
   1187 					var cur = Zotero.Prefs.get('lastAbstractExpand');
   1188 					Zotero.Prefs.set('lastAbstractExpand', !cur);
   1189 					
   1190 					var valueText = this.item.getField('abstractNote');
   1191 					var tabindex = valueElement.getAttribute('ztabindex');
   1192 					var newValueElement = this.createValueElement(
   1193 						valueText,
   1194 						'abstractNote',
   1195 						tabindex
   1196 					);
   1197 					valueElement.parentNode.replaceChild(newValueElement, valueElement);
   1198 					
   1199 					var text = Zotero.ItemFields.getLocalizedString(this.item.itemTypeID, 'abstractNote');
   1200 					// Add '(...)' before "Abstract" for collapsed abstracts
   1201 					if (valueText && cur) {
   1202 						text = '(\u2026) ' + text;
   1203 					}
   1204 					label.setAttribute('value', text);
   1205 				]]>
   1206 				</body>
   1207 			</method>
   1208 			
   1209 			
   1210 			<method name="disableButton">
   1211 				<parameter name="button"/>
   1212 				<body>
   1213 				<![CDATA[
   1214 					button.setAttribute('disabled', true);
   1215 					button.setAttribute('onclick', false); 
   1216 				]]>
   1217 				</body>
   1218 			</method>
   1219 			
   1220 			
   1221 			<method name="_enablePlusButton">
   1222 				<parameter name="button"/>
   1223 				<parameter name="creatorTypeID"/>
   1224 				<parameter name="fieldMode"/>
   1225 				<body>
   1226 				<![CDATA[
   1227 					button.setAttribute('disabled', false);
   1228 					button.onclick = function () {
   1229 						var parent = document.getBindingParent(this);
   1230 						parent.disableButton(this);
   1231 						parent.addCreatorRow(null, creatorTypeID, true);
   1232 					};
   1233 				]]>
   1234 				</body>
   1235 			</method>
   1236 			
   1237 			
   1238 			<method name="disableCreatorAddButtons">
   1239 				<body>
   1240 				<![CDATA[
   1241 					// Disable the "+" button on all creator rows
   1242 					var elems = this._dynamicFields.getElementsByAttribute('value', '+');
   1243 					for (var i = 0, len = elems.length; i < len; i++) {
   1244 						this.disableButton(elems[i]);
   1245 					}
   1246 				]]>
   1247 				</body>
   1248 			</method>
   1249 			
   1250 			
   1251 			<method name="createValueElement">
   1252 				<parameter name="valueText"/>
   1253 				<parameter name="fieldName"/>
   1254 				<parameter name="tabindex"/>
   1255 				<body>
   1256 				<![CDATA[
   1257 					valueText = valueText + '';
   1258 					
   1259 					if (fieldName) {
   1260 						var fieldID = Zotero.ItemFields.getID(fieldName);
   1261 					}
   1262 					
   1263 					// If an abstract, check last expand state
   1264 					var abstractAsVbox = fieldName == 'abstractNote' && Zotero.Prefs.get('lastAbstractExpand');
   1265 					
   1266 					// Use a vbox for multiline fields (but Abstract only if it's expanded)
   1267 					var useVbox = (fieldName != 'abstractNote' || abstractAsVbox)
   1268 						&& Zotero.ItemFields.isMultiline(fieldName);
   1269 					
   1270 					if (useVbox) {
   1271 						var valueElement = document.createElement("vbox");
   1272 					}
   1273 					else {
   1274 						var valueElement = document.createElement("label");
   1275 					}
   1276 					
   1277 					valueElement.setAttribute('id', `itembox-field-value-${fieldName}`);
   1278 					valueElement.setAttribute('fieldname', fieldName);
   1279 					valueElement.setAttribute('flex', 1);
   1280 					
   1281 					if (this._fieldIsClickable(fieldName)) {
   1282 						valueElement.setAttribute('ztabindex', tabindex);
   1283 						valueElement.addEventListener('click', function (event) {
   1284 							/* Skip right-click on Windows */
   1285 							if (event.button) {
   1286 								return;
   1287 							}
   1288 							document.getBindingParent(this).clickHandler(this);
   1289 						}, false);
   1290 						valueElement.className = 'zotero-clicky';
   1291 					}
   1292 					
   1293 					switch (fieldName) {
   1294 						case 'itemType':
   1295 							valueElement.setAttribute('itemTypeID', valueText);
   1296 							valueText = Zotero.ItemTypes.getLocalizedString(valueText);
   1297 							break;
   1298 						
   1299 						// Convert dates from UTC
   1300 						case 'dateAdded':
   1301 						case 'dateModified':
   1302 						case 'accessDate':
   1303 						case 'date':
   1304 						
   1305 						// TEMP - NSF
   1306 						case 'dateSent':
   1307 						case 'dateDue':
   1308 						case 'accepted':
   1309 							if (fieldName == 'date' && this.item._objectType != 'feedItem') {
   1310 								break;
   1311 							}
   1312 							if (valueText) {
   1313 								var date = Zotero.Date.sqlToDate(valueText, true);
   1314 								if (date) {
   1315 									// If no time, interpret as local, not UTC
   1316 									if (Zotero.Date.isSQLDate(valueText)) {
   1317 										// Add time to avoid showing previous day if date is in
   1318 										// DST (including the current date at 00:00:00) and we're
   1319 										// in standard time
   1320 										date = Zotero.Date.sqlToDate(valueText + ' 12:00:00');
   1321 										valueText = date.toLocaleDateString();
   1322 									}
   1323 									else {
   1324 										valueText = date.toLocaleString();
   1325 									}
   1326 								}
   1327 								else {
   1328 									valueText = '';
   1329 								}
   1330 							}
   1331 							break;
   1332 					}
   1333 					
   1334 					if (fieldID) {
   1335 						// Display the SQL date as a tooltip for date fields
   1336 						// TEMP - filingDate
   1337 						if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') || fieldName == 'filingDate') {
   1338 							valueElement.setAttribute('tooltiptext',
   1339 								Zotero.Date.multipartToSQL(this.item.getField(fieldName, true)));
   1340 						}
   1341 						
   1342 						// Display a context menu for certain fields
   1343 						if (this.editable && (fieldName == 'seriesTitle' || fieldName == 'shortTitle' ||
   1344 								Zotero.ItemFields.isFieldOfBase(fieldID, 'title') ||
   1345 								Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) {
   1346 							valueElement.setAttribute('contextmenu', 'zotero-field-transform-menu');
   1347 						}
   1348 					}
   1349 					
   1350 					
   1351 					if (fieldName && fieldName.indexOf('firstName') != -1) {
   1352 						valueElement.setAttribute('flex', '1');
   1353 					}
   1354 					
   1355 					var firstSpace = valueText.indexOf(" ");
   1356 					
   1357 					// To support newlines in Abstract and Extra fields, use multiple
   1358 					// <description> elements inside a vbox
   1359 					if (useVbox) {
   1360 						var lines = valueText.split("\n");
   1361 						for (var i = 0; i < lines.length; i++) {
   1362 							var descriptionNode = document.createElement("description");
   1363 							// Add non-breaking space to empty lines to prevent them from collapsing.
   1364 							// (Just using CSS min-height results in overflow in some cases.)
   1365 							if (lines[i] === "") {
   1366 								lines[i] = "\u00a0";
   1367 							}
   1368 							var linetext = document.createTextNode(lines[i]);
   1369 							descriptionNode.appendChild(linetext);
   1370 							valueElement.appendChild(descriptionNode);
   1371 						}
   1372 					}
   1373 					// 29 == arbitrary length at which to chop uninterrupted text
   1374 					else if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29
   1375 						|| (fieldName &&
   1376 							(fieldName.substr(0, 7) == 'creator') || fieldName == 'abstractNote')) {
   1377 						if (fieldName == 'abstractNote') {
   1378 							valueText = valueText.replace(/[\t\n]/g, ' ');
   1379 						}
   1380 						valueElement.setAttribute('crop', 'end');
   1381 						valueElement.setAttribute('value',valueText);
   1382 					}
   1383 					else {
   1384 						// Wrap to multiple lines
   1385 						valueElement.appendChild(document.createTextNode(valueText));
   1386 					}
   1387 					
   1388 					// Allow toggling non-editable Abstract open and closed with click
   1389 					if (fieldName == 'abstractNote' && !this.editable) {
   1390 						valueElement.classList.add("pointer");
   1391 						valueElement.addEventListener('click', function () {
   1392 							this.toggleAbstractExpand(valueElement.previousSibling, valueElement);
   1393 						}.bind(this));
   1394 					}
   1395 					
   1396 					return valueElement;
   1397 				]]>
   1398 				</body>
   1399 			</method>
   1400 			
   1401 			
   1402 			<method name="removeCreator">
   1403 				<parameter name="index"/>
   1404 				<parameter name="labelToDelete"/>
   1405 				<body>
   1406 				<![CDATA[
   1407 					// If unsaved row, just remove element
   1408 					if (!this.item.hasCreatorAt(index)) {
   1409 						labelToDelete.parentNode.removeChild(labelToDelete);
   1410 						
   1411 						// Enable the "+" button on the previous row
   1412 						var elems = this._dynamicFields.getElementsByAttribute('value', '+');
   1413 						var button = elems[elems.length-1];
   1414 						var creatorFields = this.getCreatorFields(Zotero.getAncestorByTagName(button, 'row'));
   1415 						this._enablePlusButton(button, creatorFields.creatorTypeID, creatorFields.fieldMode);
   1416 						
   1417 						this._creatorCount--;
   1418 						return;
   1419 					}
   1420 					this.item.removeCreator(index);
   1421 					this.item.saveTx();
   1422 				]]>
   1423 				</body>
   1424 			</method>
   1425 			
   1426 			
   1427 			<method name="showEditor">
   1428 				<parameter name="elem"/>
   1429 				<body><![CDATA[
   1430 				return (async function () {
   1431 					Zotero.debug(`Showing editor for ${elem.getAttribute('fieldname')}`);
   1432 					
   1433 					var label = Zotero.getAncestorByTagName(elem, 'row').querySelector('label');
   1434 					var lastTabIndex = this._lastTabIndex = parseInt(elem.getAttribute('ztabindex'));
   1435 					
   1436 					// If a field is open, hide it before selecting the new field, which might
   1437 					// trigger a refresh
   1438 					var activeField = this._dynamicFields.querySelector('textbox');
   1439 					if (activeField) {
   1440 						this._refreshed = false;
   1441 						await this.blurOpenField();
   1442 						this._lastTabIndex = lastTabIndex;
   1443 						// If the box was refreshed, the clicked element is no longer valid,
   1444 						// so just focus by tab index
   1445 						if (this._refreshed) {
   1446 							this._focusNextField(this._lastTabIndex);
   1447 							return;
   1448 						}
   1449 					}
   1450 					
   1451 					// In Firefox 45, when clicking a multiline field such as Extra, the event is
   1452 					// triggered on the inner 'description' element instead of the 'vbox'.
   1453 					if (elem.tagName == 'description') {
   1454 						elem = elem.parentNode;
   1455 					}
   1456 					
   1457 					var fieldName = elem.getAttribute('fieldname');
   1458 					var tabindex = elem.getAttribute('ztabindex');
   1459 					
   1460 					var [field, creatorIndex, creatorField] = fieldName.split('-');
   1461 					if (field == 'creator') {
   1462 						var value = this.item.getCreator(creatorIndex)[creatorField];
   1463 						if (value === undefined) {
   1464 							value = "";
   1465 						}
   1466 						var itemID = this.item.id;
   1467 					}
   1468 					else {
   1469 						var value = this.item.getField(fieldName);
   1470 						var itemID = this.item.id;
   1471 						
   1472 						// Access date needs to be converted from UTC
   1473 						if (value != '') {
   1474 							switch (fieldName) {
   1475 								case 'accessDate':
   1476 								
   1477 								// TEMP - NSF
   1478 								case 'dateSent':
   1479 								case 'dateDue':
   1480 								case 'accepted':
   1481 									// If no time, interpret as local, not UTC
   1482 									if (Zotero.Date.isSQLDate(value)) {
   1483 										var localDate = Zotero.Date.sqlToDate(value);
   1484 									}
   1485 									else {
   1486 										var localDate = Zotero.Date.sqlToDate(value, true);
   1487 									}
   1488 									var value = Zotero.Date.dateToSQL(localDate);
   1489 									
   1490 									// Don't show time in editor
   1491 									value = value.replace(' 00:00:00', '');
   1492 									break;
   1493 							}
   1494 						}
   1495 					}
   1496 					
   1497 					var t = document.createElement("textbox");
   1498 					t.setAttribute('id', `itembox-field-textbox-${fieldName}`);
   1499 					t.setAttribute('value', value);
   1500 					t.setAttribute('fieldname', fieldName);
   1501 					t.setAttribute('ztabindex', tabindex);
   1502 					t.setAttribute('flex', '1');
   1503 					
   1504 					if (creatorField=='lastName') {
   1505 						t.setAttribute('fieldMode', elem.getAttribute('fieldMode'));
   1506 						t.setAttribute('newlines','pasteintact');
   1507 					}
   1508 					
   1509 					if (Zotero.ItemFields.isMultiline(fieldName) || Zotero.ItemFields.isLong(fieldName)) {
   1510 						t.setAttribute('multiline', true);
   1511 						t.setAttribute('rows', 8);
   1512 					}
   1513 					else {
   1514 						// Add auto-complete for certain fields
   1515 						if (Zotero.ItemFields.isAutocompleteField(fieldName)
   1516 								|| fieldName == 'creator') {
   1517 							t.setAttribute('type', 'autocomplete');
   1518 							t.setAttribute('autocompletesearch', 'zotero');
   1519 							
   1520 							let params = {
   1521 								fieldName: fieldName,
   1522 								libraryID: this.item.libraryID
   1523 							};
   1524 							if (field == 'creator') {
   1525 								params.fieldMode = parseInt(elem.getAttribute('fieldMode'));
   1526 								
   1527 								// Include itemID and creatorTypeID so the autocomplete can
   1528 								// avoid showing results for creators already set on the item
   1529 								let row = Zotero.getAncestorByTagName(elem, 'row');
   1530 								let creatorTypeID = parseInt(
   1531 									row.getElementsByClassName('creator-type-label')[0]
   1532 									.getAttribute('typeid')
   1533 								);
   1534 								if (itemID) {
   1535 									params.itemID = itemID;
   1536 									params.creatorTypeID = creatorTypeID;
   1537 								}
   1538 								
   1539 								// Return
   1540 								t.setAttribute('ontextentered',
   1541 									'document.getBindingParent(this).handleCreatorAutoCompleteSelect(this, true)');
   1542 								// Tab/Shift-Tab
   1543 								t.setAttribute('onchange',
   1544 									'document.getBindingParent(this).handleCreatorAutoCompleteSelect(this)');
   1545 							};
   1546 							t.setAttribute(
   1547 								'autocompletesearchparam', JSON.stringify(params)
   1548 							);
   1549 							t.setAttribute('completeselectedindex', true);
   1550 						}
   1551 					}
   1552 					var box = elem.parentNode;
   1553 					box.replaceChild(t, elem);
   1554 					
   1555 					// Associate textbox with label
   1556 					label.setAttribute('control', t.getAttribute('id'));
   1557 					
   1558 					// Prevent error when clicking between a changed field
   1559 					// and another -- there's probably a better way
   1560 					if (!t.select) {
   1561 						return;
   1562 					}
   1563 					
   1564 					t.select();
   1565 					
   1566 					// Leave text field open when window loses focus
   1567 					var ignoreBlur = function () {
   1568 						this.ignoreBlur = true;
   1569 					}.bind(this);
   1570 					var unignoreBlur = function () {
   1571 						this.ignoreBlur = false;
   1572 					}.bind(this);
   1573 					addEventListener("deactivate", ignoreBlur);
   1574 					addEventListener("activate", unignoreBlur);
   1575 					
   1576 					t.addEventListener('blur', function () {
   1577 						var self = document.getBindingParent(this);
   1578 						if (self.ignoreBlur) return;
   1579 						
   1580 						removeEventListener("deactivate", ignoreBlur);
   1581 						removeEventListener("activate", unignoreBlur);
   1582 						self.blurHandler(this);
   1583 					});
   1584 					t.setAttribute('onkeypress', "return document.getBindingParent(this).handleKeyPress(event)");
   1585 					
   1586 					return t;
   1587 				}.bind(this))();
   1588 				]]></body>
   1589 			</method>
   1590 			
   1591 			
   1592 			<!--
   1593 			 Save a multiple-field selection for the creator autocomplete
   1594 			 (e.g. "Shakespeare, William")
   1595 			-->
   1596 			<method name="handleCreatorAutoCompleteSelect">
   1597 				<parameter name="textbox"/>
   1598 				<parameter name="stayFocused"/>
   1599 				<body><![CDATA[
   1600 					var comment = false;
   1601 					var controller = textbox.controller;
   1602 					if (!controller.matchCount) return;
   1603 					
   1604 					for (var i=0; i<controller.matchCount; i++)
   1605 					{
   1606 						if (controller.getValueAt(i) == textbox.value)
   1607 						{
   1608 							comment = controller.getCommentAt(i);
   1609 							break;
   1610 						}
   1611 					}
   1612 					
   1613 					// No result selected
   1614 					if (!comment) {
   1615 						return;
   1616 					}
   1617 					
   1618 					var [creatorID, numFields] = comment.split('-');
   1619 					
   1620 					// If result uses two fields, save both
   1621 					if (numFields==2)
   1622 					{
   1623 						// Manually clear autocomplete controller's reference to
   1624 						// textbox to prevent error next time around
   1625 						textbox.mController.input = null;
   1626 						
   1627 						var [field, creatorIndex, creatorField] =
   1628 							textbox.getAttribute('fieldname').split('-');
   1629 						
   1630 						if (stayFocused) {
   1631 							this._lastTabIndex = parseInt(textbox.getAttribute('ztabindex'));
   1632 							this._tabDirection = false;
   1633 						}
   1634 						
   1635 						var creator = Zotero.Creators.get(creatorID);
   1636 						
   1637 						var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName';
   1638 						
   1639 						// Update this textbox
   1640 						textbox.setAttribute('value', creator[creatorField]);
   1641 						textbox.value = creator[creatorField];
   1642 						
   1643 						// Update the other label
   1644 						if (otherField=='firstName'){
   1645 							var label = textbox.nextSibling.nextSibling;
   1646 						}
   1647 						else if (otherField=='lastName'){
   1648 							var label = textbox.previousSibling.previousSibling;
   1649 						}
   1650 						
   1651 						//this._setFieldValue(label, creator[otherField]);
   1652 						if (label.firstChild){
   1653 							label.firstChild.nodeValue = creator[otherField];
   1654 						}
   1655 						else {
   1656 							label.value = creator[otherField];
   1657 						}
   1658 						
   1659 						var row = Zotero.getAncestorByTagName(textbox, 'row');
   1660 						
   1661 						var fields = this.getCreatorFields(row);
   1662 						fields[creatorField] = creator[creatorField];
   1663 						fields[otherField] = creator[otherField];
   1664 						
   1665 						this.modifyCreator(creatorIndex, fields);
   1666 						if (this.saveOnEdit) {
   1667 							this.ignoreBlur = true;
   1668 							this.item.saveTx().then(() => {
   1669 								this.ignoreBlur = false;
   1670 							});
   1671 						}
   1672 					}
   1673 					
   1674 					// Otherwise let the autocomplete popup handle matters
   1675 				]]></body>
   1676 			</method>
   1677 			
   1678 			
   1679 			<method name="handleKeyPress">
   1680 				<parameter name="event"/>
   1681 				<body>
   1682 				<![CDATA[
   1683 					var target = event.target;
   1684 					var focused = document.commandDispatcher.focusedElement;
   1685 					
   1686 					switch (event.keyCode)
   1687 					{
   1688 						case event.DOM_VK_RETURN:
   1689 							var fieldname = target.getAttribute('fieldname');
   1690 							// Use shift-enter as the save action for the larger fields
   1691 							if (Zotero.ItemFields.isMultiline(fieldname) && !event.shiftKey) {
   1692 								break;
   1693 							}
   1694 							
   1695 							// Prevent blur on containing textbox
   1696 							// DEBUG: what happens if this isn't present?
   1697 							event.preventDefault();
   1698 							
   1699 							// Shift-enter adds new creator row
   1700 							if (fieldname.indexOf('creator-') == 0 && event.shiftKey) {
   1701 								// Value hasn't changed
   1702 								if (target.getAttribute('value') == target.value) {
   1703 									Zotero.debug("Value hasn't changed");
   1704 									// If + button is disabled, just focus next creator row
   1705 									if (Zotero.getAncestorByTagName(target, 'row').lastChild.lastChild.disabled) {
   1706 										this._focusNextField(this._lastTabIndex);
   1707 									}
   1708 									else {
   1709 										var creatorFields = this.getCreatorFields(Zotero.getAncestorByTagName(target, 'row'));
   1710 										this.addCreatorRow(false, creatorFields.creatorTypeID, true);
   1711 									}
   1712 								}
   1713 								// Value has changed
   1714 								else {
   1715 									this._tabDirection = 1;
   1716 									this._addCreatorRow = true;
   1717 									focused.blur();
   1718 								}
   1719 								return false;
   1720 							}
   1721 							focused.blur();
   1722 							
   1723 							// Return focus to items pane
   1724 							var tree = document.getElementById('zotero-items-tree');
   1725 							if (tree) {
   1726 								tree.focus();
   1727 							}
   1728 							
   1729 							return false;
   1730 							
   1731 						case event.DOM_VK_ESCAPE:
   1732 							// Reset field to original value
   1733 							target.value = target.getAttribute('value');
   1734 							
   1735 							focused.blur();
   1736 							
   1737 							// Return focus to items pane
   1738 							var tree = document.getElementById('zotero-items-tree');
   1739 							if (tree) {
   1740 								tree.focus();
   1741 							}
   1742 							
   1743 							return false;
   1744 							
   1745 						case event.DOM_VK_TAB:
   1746 							if (event.shiftKey) {
   1747 								this._focusNextField(this._lastTabIndex, true);
   1748 							}
   1749 							else {
   1750 								this._focusNextField(++this._lastTabIndex);
   1751 							}
   1752 							return false;
   1753 					}
   1754 					
   1755 					return true;
   1756 				]]>
   1757 				</body>
   1758 			</method>
   1759 			
   1760 			
   1761 			<method name="itemTypeMenuTab">
   1762 				<parameter name="event"/>
   1763 				<body>
   1764 				<![CDATA[
   1765 					if (!event.shiftKey) {
   1766 						this.focusFirstField();
   1767 						event.preventDefault();
   1768 					}
   1769 					// Shift-tab
   1770 					else {
   1771 						this._tabDirection = false;
   1772 					}
   1773 				]]>
   1774 				</body>
   1775 			</method>
   1776 			
   1777 			
   1778 			<method name="hideEditor">
   1779 				<parameter name="textbox"/>
   1780 				<body><![CDATA[
   1781 				return (async function () {
   1782 					Zotero.debug(`Hiding editor for ${textbox.getAttribute('fieldname')}`);
   1783 					
   1784 					var label = Zotero.getAncestorByTagName(textbox, 'row').querySelector('label');
   1785 					this._lastTabIndex = -1;
   1786 					
   1787 					// Prevent autocomplete breakage in Firefox 3
   1788 					if (textbox.mController) {
   1789 						textbox.mController.input = null;
   1790 					}
   1791 					
   1792 					var fieldName = textbox.getAttribute('fieldname');
   1793 					var tabindex = textbox.getAttribute('ztabindex');
   1794 					
   1795 					//var value = t.value;
   1796 					var value = textbox.value;
   1797 					
   1798 					var elem;
   1799 					var [field, creatorIndex, creatorField] = fieldName.split('-');
   1800 					var newVal;
   1801 					
   1802 					// Creator fields
   1803 					if (field == 'creator') {
   1804 						var row = Zotero.getAncestorByTagName(textbox, 'row');
   1805 						
   1806 						var otherFields = this.getCreatorFields(row);
   1807 						otherFields[creatorField] = value;
   1808 						var lastName = otherFields.lastName.trim();
   1809 						
   1810 						//Handle \n\r and \n delimited entries
   1811 						var rawNameArray = lastName.split(/\r\n?|\n/);
   1812 						if (rawNameArray.length > 1) {
   1813 							//Save tab direction and add creator flags since they are reset in the 
   1814 							//process of adding multiple authors
   1815 							var tabDirectionBuffer = this._tabDirection;
   1816 							var addCreatorRowBuffer = this._addCreatorRow;
   1817 							var tabIndexBuffer = this._lastTabIndex;
   1818 							this._tabDirection = false;
   1819 							this._addCreatorRow = false;
   1820 							
   1821 							//Filter out bad names
   1822 							var nameArray = rawNameArray.filter(name => name);
   1823 							
   1824 							//If not adding names at the end of the creator list, make new creator 
   1825 							//entries and then shift down existing creators.
   1826 							var initNumCreators = this.item.numCreators();
   1827 							var creatorsToShift = initNumCreators - creatorIndex;
   1828 							if (creatorsToShift > 0) { 
   1829 								//Add extra creators
   1830 								for (var i=0;i<nameArray.length;i++) {
   1831 									this.modifyCreator(i + initNumCreators, otherFields);
   1832 								}
   1833 								
   1834 								//Shift existing creators
   1835 								for (var i=initNumCreators-1; i>=creatorIndex; i--) {
   1836 									let shiftedCreatorData = this.item.getCreator(i);
   1837 									this.item.setCreator(nameArray.length + i, shiftedCreatorData);
   1838 								}
   1839 							}
   1840 							
   1841 							//Add the creators in lastNameArray one at a time
   1842 							for (let tempName of nameArray) {
   1843 								// Check for tab to determine creator name format
   1844 								otherFields.fieldMode = (tempName.indexOf('\t') == -1) ? 1 : 0;
   1845 								if (otherFields.fieldMode == 0) {
   1846 									otherFields.lastName=tempName.split('\t')[0];
   1847 									otherFields.firstName=tempName.split('\t')[1];
   1848 								} 
   1849 								else {
   1850 									otherFields.lastName=tempName;
   1851 									otherFields.firstName='';
   1852 								}
   1853 								this.modifyCreator(creatorIndex, otherFields);
   1854 								creatorIndex++;
   1855 							}
   1856 							this._tabDirection = tabDirectionBuffer;
   1857 							this._addCreatorRow = (creatorsToShift==0) ? addCreatorRowBuffer : false;
   1858 							if (this._tabDirection == 1) {
   1859 								this._lastTabIndex = parseInt(tabIndexBuffer,10) + 2*(nameArray.length-1);
   1860 								if (otherFields.fieldMode == 0) {
   1861 									this._lastTabIndex++;
   1862 								}
   1863 							}
   1864 						}
   1865 						else {
   1866 							this.modifyCreator(creatorIndex, otherFields);
   1867 						}
   1868 						
   1869 						var val = this.item.getCreator(creatorIndex);
   1870 						val = val ? val[creatorField] : null;
   1871 						
   1872 						if (!val) {
   1873 							// Reset to '(first)'/'(last)'/'(name)'
   1874 							if (creatorField == 'lastName') {
   1875 								val = otherFields.fieldMode
   1876 									? this._defaultFullName : this._defaultLastName;
   1877 							}
   1878 							else if (creatorField == 'firstName') {
   1879 								val = this._defaultFirstName;
   1880 							}
   1881 						}
   1882 						
   1883 						newVal = val;
   1884 						
   1885 						// Reset creator mode settings here so that flex attribute gets reset
   1886 						this.switchCreatorMode(row, (otherFields.fieldMode ? 1 : 0), true);
   1887 						if (Zotero.ItemTypes.getName(this.item.itemTypeID) === "bookSection") {
   1888 							var creatorTypeLabels = document.getAnonymousNodes(this)[0].getElementsByClassName("creator-type-label");
   1889 							Zotero.debug(creatorTypeLabels[creatorTypeLabels.length-1] + "");
   1890 							document.getElementById("zotero-author-guidance").show({
   1891 								forEl: creatorTypeLabels[creatorTypeLabels.length-1]
   1892 							});
   1893 						}
   1894 					}
   1895 					
   1896 					// Fields
   1897 					else {
   1898 						// Access date needs to be parsed and converted to UTC SQL date
   1899 						if (value != '') {
   1900 							switch (fieldName) {
   1901 								case 'accessDate':
   1902 									// Allow "now" to use current time
   1903 									if (value == 'now') {
   1904 										value = Zotero.Date.dateToSQL(new Date(), true);
   1905 									}
   1906 									// If just date, don't convert to UTC
   1907 									else if (Zotero.Date.isSQLDate(value)) {
   1908 										var localDate = Zotero.Date.sqlToDate(value);
   1909 										value = Zotero.Date.dateToSQL(localDate).replace(' 00:00:00', '');
   1910 									}
   1911 									else if (Zotero.Date.isSQLDateTime(value)) {
   1912 										var localDate = Zotero.Date.sqlToDate(value);
   1913 										value = Zotero.Date.dateToSQL(localDate, true);
   1914 									}
   1915 									else {
   1916 										var d = Zotero.Date.strToDate(value);
   1917 										value = null;
   1918 										if (d.year && d.month != undefined && d.day) {
   1919 											d = new Date(d.year, d.month, d.day);
   1920 											value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', '');
   1921 										}
   1922 									}
   1923 									break;
   1924 								
   1925 								// TEMP - NSF
   1926 								case 'dateSent':
   1927 								case 'dateDue':
   1928 								case 'accepted':
   1929 									if (Zotero.Date.isSQLDate(value)) {
   1930 										var localDate = Zotero.Date.sqlToDate(value);
   1931 										value = Zotero.Date.dateToSQL(localDate).replace(' 00:00:00', '');
   1932 									}
   1933 									else {
   1934 										var d = Zotero.Date.strToDate(value);
   1935 										value = null;
   1936 										if (d.year && d.month != undefined && d.day) {
   1937 											d = new Date(d.year, d.month, d.day);
   1938 											value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', '');
   1939 										}
   1940 									}
   1941 									break;
   1942 								
   1943 								default:
   1944 									// TODO: generalize to all date rows/fields
   1945 									if (Zotero.ItemFields.isFieldOfBase(fieldName, 'date')) {
   1946 										// Parse 'yesterday'/'today'/'tomorrow' and convert to dates,
   1947 										// since it doesn't make sense for those to be actual metadata values
   1948 										var lc = value.toLowerCase();
   1949 										if (lc == 'yesterday' || lc == Zotero.getString('date.yesterday')) {
   1950 											value = Zotero.Date.dateToSQL(new Date(new Date().getTime() - 86400000)).substr(0, 10);
   1951 										}
   1952 										else if (lc == 'today' || lc == Zotero.getString('date.today')) {
   1953 											value = Zotero.Date.dateToSQL(new Date()).substr(0, 10);
   1954 										}
   1955 										else if (lc == 'tomorrow' || lc == Zotero.getString('date.tomorrow')) {
   1956 											value = Zotero.Date.dateToSQL(new Date(new Date().getTime() + 86400000)).substr(0, 10);
   1957 										}
   1958 									}
   1959 							}
   1960 						}
   1961 						
   1962 						this._modifyField(fieldName, value);
   1963 						newVal = this.item.getField(fieldName);
   1964 					}
   1965 					
   1966 					// Close box
   1967 					elem = this.createValueElement(
   1968 						newVal,
   1969 						fieldName,
   1970 						tabindex
   1971 					);
   1972 					var box = textbox.parentNode;
   1973 					box.replaceChild(elem, textbox);
   1974 					
   1975 					// Disassociate textbox from label
   1976 					label.setAttribute('control', elem.getAttribute('id'));
   1977 					
   1978 					if (this.saveOnEdit) {
   1979 						await this.item.saveTx();
   1980 					}
   1981 				}.bind(this))();
   1982 				]]></body>
   1983 			</method>
   1984 			
   1985 			
   1986 			<method name="_rowIsClickable">
   1987 				<parameter name="fieldName"/>
   1988 				<body>
   1989 				<![CDATA[
   1990 				 	return this.clickByRow &&
   1991 						(this.clickable ||
   1992 							this._clickableFields.indexOf(fieldName) != -1);
   1993 				]]>
   1994 				</body>
   1995 			</method>
   1996 			
   1997 			
   1998 			<method name="_fieldIsClickable">
   1999 				<parameter name="fieldName"/>
   2000 				<body>
   2001 				<![CDATA[
   2002 					return !this.clickByRow &&
   2003 						((this.clickable && !Zotero.Items.isPrimaryField(fieldName))
   2004 							|| this._clickableFields.indexOf(fieldName) != -1);
   2005 				]]>
   2006 				</body>
   2007 			</method>
   2008 			
   2009 			<method name="_modifyField">
   2010 				<parameter name="field"/>
   2011 				<parameter name="value"/>
   2012 				<body><![CDATA[
   2013 					this.item.setField(field, value);
   2014 				]]></body>
   2015 			</method>
   2016 			
   2017 			
   2018 			<method name="_getFieldValue">
   2019 				<parameter name="label"/>
   2020 				<body>
   2021 				<![CDATA[
   2022 					return label.firstChild
   2023 						? label.firstChild.nodeValue : label.value;
   2024 				]]>
   2025 				</body>
   2026 			</method>
   2027 			
   2028 			
   2029 			<method name="_setFieldValue">
   2030 				<parameter name="label"/>
   2031 				<parameter name="value"/>
   2032 				<body>
   2033 				<![CDATA[
   2034 					if (label.firstChild) {
   2035 						label.firstChild.nodeValue = value;
   2036 					}
   2037 					else {
   2038 						label.value = value;
   2039 					}
   2040 				]]>
   2041 				</body>
   2042 			</method>
   2043 			
   2044 			
   2045 			<!-- TODO: work with textboxes too -->
   2046 			<method name="textTransform">
   2047 				<parameter name="label"/>
   2048 				<parameter name="mode"/>
   2049 				<body><![CDATA[
   2050 					return (async function () {
   2051 						var val = this._getFieldValue(label);
   2052 						switch (mode) {
   2053 							case 'title':
   2054 								var newVal = Zotero.Utilities.capitalizeTitle(val.toLowerCase(), true);
   2055 								break;
   2056 							case 'sentence':
   2057 								// capitalize the first letter, including after beginning punctuation
   2058 								// capitalize after ?, ! and remove space(s) before those as well as colon analogous to capitalizeTitle function
   2059 								// also deal with initial punctuation here - open quotes and Spanish beginning punctuation marks
   2060 								newVal = val.toLowerCase().replace(/\s*:/, ":");
   2061 								newVal = newVal.replace(/(([\?!]\s*|^)([\'\"¡¿“‘„«\s]+)?[^\s])/g, function (x) {
   2062 									return x.replace(/\s+/m, " ").toUpperCase();});
   2063 								break;
   2064 							default:
   2065 								throw ("Invalid transform mode '" + mode + "' in zoteroitembox.textTransform()");
   2066 						}
   2067 						this._setFieldValue(label, newVal);
   2068 						this._modifyField(label.getAttribute('fieldname'), newVal);
   2069 						if (this.saveOnEdit) {
   2070 							// If a field is open, blur it, which will trigger a save and cause
   2071 							// the saveTx() to be a no-op
   2072 							await this.blurOpenField();
   2073 							await this.item.saveTx();
   2074 						}
   2075 					}.bind(this))();
   2076 				]]></body>
   2077 			</method>
   2078 			
   2079 			
   2080 			<method name="getCreatorFields">
   2081 				<parameter name="row"/>
   2082 				<body>
   2083 				<![CDATA[
   2084 					var typeID = row.getElementsByClassName('creator-type-label')[0].getAttribute('typeid');
   2085 					var label1 = row.getElementsByClassName('creator-name-box')[0].firstChild;
   2086 					var label2 = label1.parentNode.lastChild;
   2087 					
   2088 					var fields = {
   2089 						lastName: label1.firstChild ? label1.firstChild.nodeValue : label1.value,
   2090 						firstName: label2.firstChild ? label2.firstChild.nodeValue : label2.value,
   2091 						fieldMode: label1.getAttribute('fieldMode')
   2092 							? parseInt(label1.getAttribute('fieldMode')) : 0,
   2093 						creatorTypeID: parseInt(typeID),
   2094 					};
   2095 					
   2096 					// Ignore '(first)'
   2097 					if (fields.fieldMode == 1 || fields.firstName == this._defaultFirstName) {
   2098 						fields.firstName = '';
   2099 					}
   2100 					// Ignore '(last)' or '(name)'
   2101 					if (fields.lastName == this._defaultFullName
   2102 							|| fields.lastName == this._defaultLastName) {
   2103 						fields.lastName = '';
   2104 					}
   2105 					
   2106 					return fields;
   2107 				]]>
   2108 				</body>
   2109 			</method>
   2110 			
   2111 			
   2112 			<method name="modifyCreator">
   2113 				<parameter name="index"/>
   2114 				<parameter name="fields"/>
   2115 				<body><![CDATA[
   2116 					var libraryID = this.item.libraryID;
   2117 					var firstName = fields.firstName;
   2118 					var lastName = fields.lastName;
   2119 					var fieldMode = fields.fieldMode;
   2120 					var creatorTypeID = fields.creatorTypeID;
   2121 					
   2122 					var oldCreator = this.item.getCreator(index);
   2123 					
   2124 					// Don't save empty creators
   2125 					if (!firstName && !lastName){
   2126 						if (!oldCreator) {
   2127 							return false;
   2128 						}
   2129 						return this.item.removeCreator(index);
   2130 					}
   2131 					
   2132 					return this.item.setCreator(index, fields);
   2133 				]]></body>
   2134 			</method>
   2135 			
   2136 			
   2137 			<!--
   2138 			  @return {Promise}
   2139 			-->
   2140 			<method name="swapNames">
   2141 				<parameter name="event"/>
   2142 				<body><![CDATA[
   2143 				return (async function () {
   2144 					var row = Zotero.getAncestorByTagName(document.popupNode, 'row');
   2145 					var typeBox = row.getElementsByAttribute('popup', 'creator-type-menu')[0];
   2146 					var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
   2147 					var fields = this.getCreatorFields(row);
   2148 					var lastName = fields.lastName;
   2149 					var firstName = fields.firstName;
   2150 					fields.lastName = firstName;
   2151 					fields.firstName = lastName;
   2152 					this.modifyCreator(creatorIndex, fields);
   2153 					if (this.saveOnEdit) {
   2154 						// See note in transformText()
   2155 						await this.blurOpenField();
   2156 						await this.item.saveTx();
   2157 					}
   2158 				}.bind(this))();
   2159 				]]></body>
   2160 			</method>
   2161 			
   2162 			<!--
   2163 			  @return {Promise}
   2164 			-->
   2165 			<method name="moveCreator">
   2166 				<parameter name="index"/>
   2167 				<parameter name="moveUp"/>
   2168 				<body><![CDATA[
   2169 				return Zotero.spawn(function* () {
   2170 					if (index == 0 && moveUp) {
   2171 						Zotero.debug("Can't move up creator 0");
   2172 						return;
   2173 					}
   2174 					else if (index + 1 == this.item.numCreators() && !moveUp) {
   2175 						Zotero.debug("Can't move down last creator");
   2176 						return;
   2177 					}
   2178 					
   2179 					var newIndex = moveUp ? index - 1 : index + 1;
   2180 					var a = this.item.getCreator(index);
   2181 					var b = this.item.getCreator(newIndex);
   2182 					this.item.setCreator(newIndex, a);
   2183 					this.item.setCreator(index, b);
   2184 					if (this.saveOnEdit) {
   2185 						// See note in transformText()
   2186 						yield this.blurOpenField();
   2187 						return this.item.saveTx();
   2188 					}
   2189 				}, this);
   2190 				]]></body>
   2191 			</method>
   2192 			
   2193 			
   2194 			<method name="_updateAutoCompleteParams">
   2195 				<parameter name="row"/>
   2196 				<parameter name="changedParams"/>
   2197 				<body>
   2198 				<![CDATA[
   2199 					var textboxes = row.getElementsByTagName('textbox');
   2200 					if (textboxes.length) {
   2201 						var t = textboxes[0];
   2202 						var params = JSON.parse(t.getAttribute('autocompletesearchparam'));
   2203 						for (var param in changedParams) {
   2204 							params[param] = changedParams[param];
   2205 						}
   2206 						t.setAttribute('autocompletesearchparam', JSON.stringify(params));
   2207 					}
   2208 				]]>
   2209 				</body>
   2210 			</method>
   2211 			
   2212 			
   2213 			<method name="focusFirstField">
   2214 				<body>
   2215 				<![CDATA[
   2216 					this._focusNextField(1);
   2217 				]]>
   2218 				</body>
   2219 			</method>
   2220 			
   2221 			
   2222 			<!-- 
   2223 				Advance the field focus forward or backward
   2224 				
   2225 				Note: We're basically replicating the built-in tabindex functionality,
   2226 				which doesn't work well with the weird label/textbox stuff we're doing.
   2227 				(The textbox being tabbed away from is deleted before the blur()
   2228 				completes, so it doesn't know where it's supposed to go next.)
   2229 			-->
   2230 			<method name="_focusNextField">
   2231 				<parameter name="tabindex"/>
   2232 				<parameter name="back"/>
   2233 				<body>
   2234 				<![CDATA[
   2235 					var box = this._dynamicFields;
   2236 					tabindex = parseInt(tabindex);
   2237 					
   2238 					// Get all fields with ztabindex attributes
   2239 					var tabbableFields = box.querySelectorAll('*[ztabindex]');
   2240 					
   2241 					if (!tabbableFields.length) {
   2242 						Zotero.debug("No tabbable fields found");
   2243 						return false;
   2244 					}
   2245 					
   2246 					var next;
   2247 					if (back) {
   2248 						Zotero.debug('Looking for previous tabindex before ' + tabindex, 4);
   2249 						for (let i = tabbableFields.length - 1; i >= 0; i--) {
   2250 							if (parseInt(tabbableFields[i].getAttribute('ztabindex')) < tabindex) {
   2251 								next = tabbableFields[i];
   2252 								break;
   2253 							}
   2254 						}
   2255 					}
   2256 					else {
   2257 						Zotero.debug('Looking for tabindex ' + tabindex, 4);
   2258 						for (var pos = 0; pos < tabbableFields.length; pos++) {
   2259 							if (parseInt(tabbableFields[pos].getAttribute('ztabindex')) >= tabindex) {
   2260 								next = tabbableFields[pos];
   2261 								break;
   2262 							}
   2263 						}
   2264 					}
   2265 					
   2266 					if (!next) {
   2267 						Zotero.debug("Next field not found");
   2268 						return false;
   2269 					}
   2270 					
   2271 					next.click();
   2272 					
   2273 					// 1) next.parentNode is always null for some reason
   2274 					// 2) For some reason it's necessary to scroll to the next element when
   2275 					// moving forward for the target element to be fully in view
   2276 					if (!back && tabbableFields[pos + 1]) {
   2277 						Zotero.debug("Scrolling to next field");
   2278 						var visElem = tabbableFields[pos + 1];
   2279 					}
   2280 					else {
   2281 						var visElem = next;
   2282 					}
   2283 					// DEBUG: This doesn't seem to work anymore
   2284 					this.ensureElementIsVisible(visElem);
   2285 					
   2286 					return true;
   2287 				]]>
   2288 				</body>
   2289 			</method>
   2290 			
   2291 			
   2292 			<method name="blurOpenField">
   2293 				<body><![CDATA[
   2294 					return (async function () {
   2295 						var activeField = this._dynamicFields.querySelector('textbox');
   2296 						if (!activeField) {
   2297 							return false;
   2298 						}
   2299 						return this.blurHandler(activeField);
   2300 					}.bind(this))();
   2301 				]]></body>
   2302 			</method>
   2303 			
   2304 			
   2305 			<!--
   2306 				Available handlers:
   2307 				
   2308 				  - 'itemtypechange'
   2309 				
   2310 				Note: 'this' in the function will be bound to the item box.
   2311 			-->
   2312 			<method name="addHandler">
   2313 				<parameter name="eventName"/>
   2314 				<parameter name="func"/>
   2315 				<body>
   2316 				<![CDATA[
   2317 					if (!this.eventHandlers[eventName]) {
   2318 						this.eventHandlers[eventName] = [];
   2319 					}
   2320 					this.eventHandlers[eventName].push(func);
   2321 				]]>
   2322 				</body>
   2323 			</method>
   2324 			
   2325 			<method name="removeHandler">
   2326 				<parameter name="eventName"/>
   2327 				<parameter name="func"/>
   2328 				<body>
   2329 				<![CDATA[
   2330 					if (!this.eventHandlers[eventName]) {
   2331 						return;
   2332 					}
   2333 					var pos = this.eventHandlers[eventName].indexOf(func);
   2334 					if (pos != -1) {
   2335 						this.eventHandlers[eventName].splice(pos, 1);
   2336 					}
   2337 				]]>
   2338 				</body>
   2339 			</method>
   2340 			
   2341 			
   2342 			<method name="_id">
   2343 				<parameter name="id"/>
   2344 				<body>
   2345 				<![CDATA[
   2346 					return document.getAnonymousNodes(this)[0].getElementsByAttribute('id', id)[0];
   2347 				]]>
   2348 				</body>
   2349 			</method>
   2350 		</implementation>
   2351 		
   2352 		<content>
   2353 			<scrollbox id="item-box" flex="1" orient="vertical"
   2354 				 	xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   2355 				<popupset>
   2356 					<menupopup id="creator-type-menu" position="after_start"
   2357 						onpopupshowing="var typeBox = document.popupNode.localName == 'hbox' ? document.popupNode : document.popupNode.parentNode;
   2358 								var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
   2359 								
   2360 								var item = document.getBindingParent(this).item;
   2361 								var exists = item.hasCreatorAt(index);
   2362 								var moreCreators = item.numCreators() > index + 1;
   2363 								
   2364 								var hideMoveUp = !exists || index == 0;
   2365 								var hideMoveDown = !exists || !moreCreators;
   2366 								var hideMoveSep = hideMoveUp &amp;&amp; hideMoveDown;
   2367 								
   2368 								document.getElementById('zotero-creator-move-sep').setAttribute('hidden', hideMoveSep);
   2369 								document.getElementById('zotero-creator-move-up').setAttribute('hidden', hideMoveUp);
   2370 								document.getElementById('zotero-creator-move-down').setAttribute('hidden', hideMoveDown);"
   2371 						oncommand="var typeBox = document.popupNode.localName == 'hbox' ? document.popupNode : document.popupNode.parentNode;
   2372 							var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
   2373 							
   2374 							var itemBox = document.getBindingParent(this);
   2375 							
   2376 							if (event.explicitOriginalTarget.className == 'zotero-creator-move') {
   2377 								var up = event.explicitOriginalTarget.id == 'zotero-creator-move-up';
   2378 								itemBox.moveCreator(index, up);
   2379 								return;
   2380 							}
   2381 							
   2382 							var typeID = event.explicitOriginalTarget.getAttribute('typeid');
   2383 							var row = typeBox.parentNode;
   2384 							var fields = itemBox.getCreatorFields(row);
   2385 							fields.creatorTypeID = typeID;
   2386 							typeBox.getElementsByTagName('label')[0].setAttribute(
   2387 								'value',
   2388 								Zotero.getString(
   2389 									'creatorTypes.' + Zotero.CreatorTypes.getName(typeID)
   2390 								)
   2391 							);
   2392 							typeBox.setAttribute('typeid', typeID);
   2393 							
   2394 							/* If a creator textbox is already open, we need to
   2395 							change its autocomplete parameters so that it
   2396 							completes on a creator with a different creator type */
   2397 							var changedParams = {
   2398 								creatorTypeID: typeID
   2399 							};
   2400 							itemBox._updateAutoCompleteParams(row, changedParams);
   2401 							
   2402 							itemBox.modifyCreator(index, fields);
   2403 							if (itemBox.saveOnEdit) {
   2404 								itemBox.item.saveTx();
   2405 							}
   2406 							"/>
   2407 					<menupopup id="zotero-field-transform-menu">
   2408 						<menu label="&zotero.item.textTransform;">
   2409 							<menupopup>
   2410 								<menuitem label="&zotero.item.textTransform.titlecase;" class="menuitem-non-iconic"
   2411 									oncommand="document.getBindingParent(this).textTransform(document.popupNode, 'title')"/>
   2412 								<menuitem label="&zotero.item.textTransform.sentencecase;" class="menuitem-non-iconic"
   2413 									oncommand="document.getBindingParent(this).textTransform(document.popupNode, 'sentence')"/>
   2414 							</menupopup>
   2415 						</menu>
   2416 					</menupopup>
   2417 					<menupopup id="zotero-creator-transform-menu"
   2418 						onpopupshowing="var row = Zotero.getAncestorByTagName(document.popupNode, 'row');
   2419 							var typeBox = row.getElementsByAttribute('popup', 'creator-type-menu')[0];
   2420 							var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
   2421 							var item = document.getBindingParent(this).item;
   2422 							var exists = item.hasCreatorAt(index);
   2423 							if (exists) {
   2424 								var fieldMode = item.getCreator(index).name !== undefined ? 1 : 0;
   2425 							}
   2426 							var hideTransforms = !exists || !!fieldMode;
   2427 							return !hideTransforms;">
   2428 						<menuitem label="&zotero.item.creatorTransform.nameSwap;"
   2429 							oncommand="document.getBindingParent(this).swapNames(event);"/>
   2430 					</menupopup>
   2431 					<menupopup id="zotero-doi-menu">
   2432 						<menuitem id="zotero-doi-menu-view-online" label="&zotero.item.viewOnline;"/>
   2433 						<menuitem id="zotero-doi-menu-copy" label="&zotero.item.copyAsURL;"/>
   2434 					</menupopup>
   2435 					<zoteroguidancepanel id="zotero-author-guidance" about="authorMenu" position="after_end" x="-25"/>
   2436 				</popupset>
   2437 				<grid flex="1">
   2438 					<columns>
   2439 						<column/>
   2440 						<column flex="1"/>
   2441 					</columns>
   2442 					<rows id="dynamic-fields" flex="1">
   2443 						<row class="zotero-item-first-row">
   2444 							<label value="&zotero.items.itemType;"/>
   2445 							<menulist class="zotero-clicky" id="item-type-menu" oncommand="document.getBindingParent(this).changeTypeTo(this.value, this)" flex="1"
   2446 								onfocus="document.getBindingParent(this).ensureElementIsVisible(this)"
   2447 								onkeypress="if (event.keyCode == event.DOM_VK_TAB) { document.getBindingParent(this).itemTypeMenuTab(event); }">
   2448 								<menupopup/>
   2449 							</menulist>
   2450 						</row>
   2451 					</rows>
   2452 				</grid>
   2453 			</scrollbox>
   2454 		</content>
   2455 	</binding>
   2456 </bindings>