www

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

tagsbox.xml (31362B)


      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 
     29 <bindings 	xmlns="http://www.mozilla.org/xbl"
     30 			xmlns:xbl="http://www.mozilla.org/xbl"
     31 			xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
     32 	<binding id="tags-box">
     33 		<resources>
     34 			<stylesheet src="chrome://zotero/skin/bindings/tagsbox.css"/>
     35 		</resources>
     36 		
     37 		<implementation>
     38 			<field name="clickHandler"/>
     39 			
     40 			<field name="_lastTabIndex">false</field>
     41 			<field name="_tabDirection"/>
     42 			<field name="_tagColors"/>
     43 			<field name="_notifierID"/>
     44 			
     45 			<!-- Modes are predefined settings groups for particular tasks -->
     46 			<field name="_mode">"view"</field>
     47 			<property name="mode" onget="return this._mode;">
     48 				<setter>
     49 				<![CDATA[
     50 					this.clickable = false;
     51 					this.editable = false;
     52 					
     53 					switch (val) {
     54 						case 'view':
     55 						case 'merge':
     56 						case 'mergeedit':
     57 							break;
     58 						
     59 						case 'edit':
     60 							this.clickable = true;
     61 							this.editable = true;
     62 							this.clickHandler = this.showEditor;
     63 							this.blurHandler = this.hideEditor;
     64 							break;
     65 						
     66 						default:
     67 							throw ("Invalid mode '" + val + "' in tagsbox.xml");
     68 					}
     69 					
     70 					this._mode = val;
     71 					document.getAnonymousNodes(this)[0].setAttribute('mode', val);
     72 				]]>
     73 				</setter>
     74 			</property>
     75 			
     76 			<field name="_item"/>
     77 			<property name="item" onget="return this._item;">
     78 				<setter>
     79 					<![CDATA[
     80 						// Don't reload if item hasn't changed
     81 						if (this._item == val) {
     82 							return;
     83 						}
     84 						this._item = val;
     85 						this._lastTabIndex = false;
     86 						this.reload();
     87 					]]>
     88 				</setter>
     89 			</property>
     90 			
     91 			<property name="count"/>
     92 			
     93 			<property name="summary">
     94 				<getter><![CDATA[
     95 					var r = "";
     96 					
     97 					if (this.item) {
     98 						var tags = this.item.getTags();
     99 						
    100 						// Sort tags alphabetically
    101 						var collation = Zotero.getLocaleCollation();
    102 						tags.sort((a, b) => collation.compareString(1, a.tag, b.tag));
    103 						
    104 						for (let i = 0; i < tags.length; i++) {
    105 							r = r + tags[i].tag + ", ";
    106 						}
    107 						r = r.substr(0,r.length-2);
    108 					}
    109 				
    110 					return r;
    111 				]]></getter>
    112 			</property>
    113 			
    114 			<constructor>
    115 			<![CDATA[
    116 				if (this.hasAttribute('mode')) {
    117 					this.mode = this.getAttribute('mode');
    118 				}
    119 				
    120 				this._notifierID = Zotero.Notifier.registerObserver(
    121 					this, ['item-tag', 'setting'], 'tagsbox'
    122 				);
    123 			]]>
    124 			</constructor>
    125 			
    126 			
    127 			<destructor>
    128 			<![CDATA[
    129 				Zotero.Notifier.unregisterObserver(this._notifierID);
    130 			]]>
    131 			</destructor>
    132 			
    133 			
    134 			<method name="notify">
    135 				<parameter name="event"/>
    136 				<parameter name="type"/>
    137 				<parameter name="ids"/>
    138 				<parameter name="extraData"/>
    139 				<body><![CDATA[
    140 					return Zotero.spawn(function* () {
    141 						if (type == 'setting') {
    142 							if (ids.some(function (val) val.split("/")[1] == 'tagColors') && this.item) {
    143 								this.reload();
    144 								return;
    145 							}
    146 						}
    147 						else if (type == 'item-tag') {
    148 							let itemID, tagID;
    149 							
    150 							for (let i=0; i<ids.length; i++) {
    151 								[itemID, tagID] = ids[i].split('-').map(x => parseInt(x));
    152 								if (!this.item || itemID != this.item.id) {
    153 									continue;
    154 								}
    155 								let data = extraData[ids[i]];
    156 								let tagName = data.tag;
    157 								let tagType = data.type;
    158 								
    159 								if (event == 'add') {
    160 									var newTabIndex = this.add(tagName, tagType);
    161 									if (newTabIndex == -1) {
    162 										return;
    163 									}
    164 									if (this._tabDirection == -1) {
    165 										if (this._lastTabIndex > newTabIndex) {
    166 											this._lastTabIndex++;
    167 										}
    168 									}
    169 									else if (this._tabDirection == 1) {
    170 										if (this._lastTabIndex > newTabIndex) {
    171 											this._lastTabIndex++;
    172 										}
    173 									}
    174 								}
    175 								else if (event == 'modify') {
    176 									let oldTagName = data.old.tag;
    177 									this.remove(oldTagName);
    178 									this.add(tagName, tagType);
    179 								}
    180 								else if (event == 'remove') {
    181 									var oldTabIndex = this.remove(tagName);
    182 									if (oldTabIndex == -1) {
    183 										return;
    184 									}
    185 									if (this._tabDirection == -1) {
    186 										if (this._lastTabIndex > oldTabIndex) {
    187 											this._lastTabIndex--;
    188 										}
    189 									}
    190 									else if (this._tabDirection == 1) {
    191 										if (this._lastTabIndex >= oldTabIndex) {
    192 											this._lastTabIndex--;
    193 										}
    194 									}
    195 								}
    196 							}
    197 							
    198 							this.updateCount();
    199 						}
    200 						else if (type == 'tag') {
    201 							if (event == 'modify') {
    202 								this.reload();
    203 								return;
    204 							}
    205 						}
    206 					}.bind(this));
    207 				]]></body>
    208 			</method>
    209 			
    210 			
    211 			<method name="reload">
    212 				<body><![CDATA[
    213 					Zotero.debug('Reloading tags box');
    214 					
    215 					// Cancel field focusing while we're updating
    216 					this._reloading = true;
    217 					
    218 					this.id('addButton').hidden = !this.editable;
    219 					
    220 					this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
    221 					
    222 					var rows = this.id('tagRows');
    223 					while(rows.hasChildNodes()) {
    224 						rows.removeChild(rows.firstChild);
    225 					}
    226 					var tags = this.item.getTags();
    227 					
    228 					// Sort tags alphabetically
    229 					var collation = Zotero.getLocaleCollation();
    230 					tags.sort(function (a, b) collation.compareString(1, a.tag, b.tag));
    231 					
    232 					for (let i=0; i<tags.length; i++) {
    233 						this.addDynamicRow(tags[i], i+1);
    234 					}
    235 					this.updateCount(tags.length);
    236 					
    237 					this._reloading = false;
    238 					this._focusField();
    239 				]]></body>
    240 			</method>
    241 			
    242 			
    243 			<method name="addDynamicRow">
    244 				<parameter name="tagData"/>
    245 				<parameter name="tabindex"/>
    246 				<parameter name="skipAppend"/>
    247 				<body>
    248 					<![CDATA[
    249 						var isNew = !tagData;
    250 						var name = tagData ? tagData.tag : "";
    251 						var type = tagData ? tagData.type : 0;
    252 						
    253 						if (!tabindex) {
    254 							tabindex = this.id('tagRows').childNodes.length + 1;
    255 						}
    256 						
    257 						var icon = document.createElement("image");
    258 						icon.className = "zotero-box-icon";
    259 						
    260 						// DEBUG: Why won't just this.nextSibling.blur() work?
    261 						icon.setAttribute('onclick','if (this.nextSibling.inputField){ this.nextSibling.inputField.blur() }');
    262 						
    263 						var label = this.createValueElement(name, tabindex);
    264 						
    265 						if (this.editable) {
    266 							var remove = document.createElement("label");
    267 							remove.setAttribute('value','-');
    268 							remove.setAttribute('class','zotero-clicky zotero-clicky-minus');
    269 							remove.setAttribute('tabindex', -1);
    270 						}
    271 						
    272 						var row = document.createElement("row");
    273 						if (isNew) {
    274 							row.setAttribute('isNew', true);
    275 						}
    276 						row.appendChild(icon);
    277 						row.appendChild(label);
    278 						if (this.editable) {
    279 							row.appendChild(remove);
    280 						}
    281 						
    282 						this.updateRow(row, tagData);
    283 						
    284 						if (!skipAppend) {
    285 							this.id('tagRows').appendChild(row);
    286 						}
    287 						
    288 						return row;
    289 					]]>
    290 				</body>
    291 			</method>
    292 			
    293 			
    294 			<!--
    295 				Update various attributes of a row to match the given tag
    296 				and current editability
    297 			-->
    298 			<method name="updateRow">
    299 				<parameter name="row"/>
    300 				<parameter name="tagData"/>
    301 				<body><![CDATA[
    302 					var tagName = tagData ? tagData.tag : "";
    303 					var tagType = (tagData && tagData.type) ? tagData.type : 0;
    304 					
    305 					var icon = row.firstChild;
    306 					var label = row.firstChild.nextSibling;
    307 					if (this.editable) {
    308 						var remove = row.lastChild;
    309 					}
    310 					
    311 					// Row
    312 					row.setAttribute('tagName', tagName);
    313 					row.setAttribute('tagType', tagType);
    314 					
    315 					// Icon
    316 					var iconFile = 'tag';
    317 					if (!tagData || tagType == 0) {
    318 						icon.setAttribute('tooltiptext', Zotero.getString('pane.item.tags.icon.user'));
    319 					}
    320 					else if (tagType == 1) {
    321 						iconFile += '-automatic';
    322 						icon.setAttribute('tooltiptext', Zotero.getString('pane.item.tags.icon.automatic'));
    323 					}
    324 					icon.setAttribute('src', `chrome://zotero/skin/${iconFile}${Zotero.hiDPISuffix}.png`);
    325 					
    326 					// "-" button
    327 					if (this.editable) {
    328 						remove.setAttribute('disabled', false);
    329 						remove.addEventListener('click', function (event) {
    330 							Zotero.spawn(function* () {
    331 								this._lastTabIndex = false;
    332 								if (tagData) {
    333 									let item = this.item;
    334 									this.remove(tagName);
    335 									try {
    336 										item.removeTag(tagName);
    337 										yield item.saveTx()
    338 									}
    339 									catch (e) {
    340 										this.reload();
    341 										throw e;
    342 									}
    343 								}
    344 								// Remove empty textbox row
    345 								else {
    346 									row.parentNode.removeChild(row);
    347 								}
    348 								
    349 								// Return focus to items pane
    350 								var tree = document.getElementById('zotero-items-tree');
    351 								if (tree) {
    352 									tree.focus();
    353 								}
    354 							}.bind(this));
    355 						}.bind(this));
    356 					}
    357 				]]></body>
    358 			</method>
    359 			
    360 			
    361 			<method name="createValueElement">
    362 				<parameter name="valueText"/>
    363 				<parameter name="tabindex"/>
    364 				<body>
    365 				<![CDATA[
    366 					var valueElement = document.createElement("label");
    367 					valueElement.setAttribute('fieldname', 'tag');
    368 					valueElement.setAttribute('flex', 1);
    369 					valueElement.className = 'zotero-box-label';
    370 					
    371 					if (this.clickable) {
    372 						if (tabindex) {
    373 							valueElement.setAttribute('ztabindex', tabindex);
    374 						}
    375 						valueElement.addEventListener('click', function (event) {
    376 							/* Skip right-click on Windows */
    377 							if (event.button) {
    378 								return;
    379 							}
    380 							document.getBindingParent(this).clickHandler(this, 1, valueText);
    381 						}, false);
    382 						valueElement.className += ' zotero-clicky';
    383 					}
    384 					
    385 					var firstSpace;
    386 					if (typeof valueText == 'string') {
    387 						firstSpace = valueText.indexOf(" ");
    388 					}
    389 					
    390 					// 29 == arbitrary length at which to chop uninterrupted text
    391 					if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29) {
    392 						valueElement.setAttribute('crop', 'end');
    393 						valueElement.setAttribute('value',valueText);
    394 					}
    395 					else {
    396 						// Wrap to multiple lines
    397 						valueElement.appendChild(document.createTextNode(valueText));
    398 					}
    399 					
    400 					// Tag color
    401 					var colorData = this._tagColors.get(valueText);
    402 					if (colorData) {
    403 						valueElement.style.color = colorData.color;
    404 						valueElement.style.fontWeight = 'bold';
    405 					}
    406 					
    407 					return valueElement;
    408 				]]>
    409 				</body>
    410 			</method>
    411 			
    412 			
    413 			<method name="showEditor">
    414 				<parameter name="elem"/>
    415 				<parameter name="rows"/>
    416 				<parameter name="value"/>
    417 				<body><![CDATA[
    418 					// Blur any active fields
    419 					/*
    420 					if (this._dynamicFields) {
    421 						this._dynamicFields.focus();
    422 					}
    423 					*/
    424 					
    425 					Zotero.debug('Showing editor');
    426 					
    427 					var fieldName = 'tag';
    428 					var tabindex = elem.getAttribute('ztabindex');
    429 					
    430 					var itemID = Zotero.getAncestorByTagName(elem, 'tagsbox').item.id;
    431 					
    432 					var t = document.createElement("textbox");
    433 					t.setAttribute('value', value);
    434 					t.setAttribute('fieldname', fieldName);
    435 					t.setAttribute('ztabindex', tabindex);
    436 					t.setAttribute('flex', '1');
    437 					t.setAttribute('newlines','pasteintact');
    438 					// Multi-line
    439 					if (rows > 1) {
    440 						t.setAttribute('multiline', true);
    441 						t.setAttribute('rows', rows);
    442 					}
    443 					// Add auto-complete
    444 					else {
    445 						t.setAttribute('type', 'autocomplete');
    446 						t.setAttribute('autocompletesearch', 'zotero');
    447 						let params = {
    448 							fieldName: fieldName,
    449 							libraryID: this.item.libraryID
    450 						};
    451 						params.itemID = itemID ? itemID : '';
    452 						t.setAttribute(
    453 							'autocompletesearchparam', JSON.stringify(params)
    454 						);
    455 						t.setAttribute('completeselectedindex', true);
    456 					}
    457 					
    458 					var box = elem.parentNode;
    459 					box.replaceChild(t, elem);
    460 					
    461 					t.setAttribute('onblur', "return document.getBindingParent(this).blurHandler(event)");
    462 					t.setAttribute('onkeypress', "return document.getBindingParent(this).handleKeyPress(event)");
    463 					t.setAttribute('onpaste', "return document.getBindingParent(this).handlePaste(event)");
    464 					
    465 					this._tabDirection = false;
    466 					this._lastTabIndex = tabindex;
    467 					
    468 					// Prevent error when clicking between a changed field
    469 					// and another -- there's probably a better way
    470 					if (!t.select) {
    471 						return;
    472 					}
    473 					t.select();
    474 					
    475 					return t;
    476 				]]></body>
    477 			</method>
    478 			
    479 			
    480 			<method name="handleKeyPress">
    481 				<parameter name="event"/>
    482 				<body><![CDATA[
    483 					return Zotero.spawn(function* () {
    484 						var target = event.target;
    485 						var focused = document.commandDispatcher.focusedElement;
    486 						
    487 						switch (event.keyCode) {
    488 							case event.DOM_VK_RETURN:
    489 								var multiline = target.getAttribute('multiline');
    490 								var empty = target.value == "";
    491 								if (event.shiftKey) {
    492 									if (!multiline) {
    493 										var self = this;
    494 										setTimeout(function () {
    495 											var val = target.value;
    496 											if (val !== "") {
    497 												val += "\n";
    498 											}
    499 											self.makeMultiline(target, val, 6);
    500 										}, 0);
    501 										return false;
    502 									}
    503 									// Submit
    504 								}
    505 								else if (multiline) {
    506 									return true;
    507 								}
    508 								
    509 								var fieldname = 'tag';
    510 								
    511 								var row = Zotero.getAncestorByTagName(target, 'row');
    512 								let blurOnly = false;
    513 								
    514 								// If non-empty last row, only blur, because the open textbox will
    515 								// be cleared in hideEditor() and remain in place
    516 								if (row == row.parentNode.lastChild && !empty) {
    517 									blurOnly = true;
    518 								}
    519 								// If empty non-last row, refocus current row
    520 								else if (row != row.parentNode.lastChild && empty) {
    521 									var focusField = true;
    522 								}
    523 								// If non-empty non-last row, return focus to items pane
    524 								else {
    525 									var focusField = false;
    526 									this._lastTabIndex = false;
    527 								}
    528 								
    529 								yield this.blurHandler(event);
    530 								
    531 								if (blurOnly) {
    532 									return false;
    533 								}
    534 								if (focusField) {
    535 									this._focusField();
    536 								}
    537 								// Return focus to items pane
    538 								else {
    539 									var tree = document.getElementById('zotero-items-tree');
    540 									if (tree) {
    541 										tree.focus();
    542 									}
    543 								}
    544 								
    545 								return false;
    546 								
    547 							case event.DOM_VK_ESCAPE:
    548 								// Reset field to original value
    549 								target.value = target.getAttribute('value');
    550 								
    551 								var tagsbox = Zotero.getAncestorByTagName(focused, 'tagsbox');
    552 								
    553 								this._lastTabIndex = false;
    554 								yield this.blurHandler(event);
    555 								
    556 								if (tagsbox) {
    557 									tagsbox.closePopup();
    558 								}
    559 								
    560 								// Return focus to items pane
    561 								var tree = document.getElementById('zotero-items-tree');
    562 								if (tree) {
    563 									tree.focus();
    564 								}
    565 								
    566 								return false;
    567 								
    568 							case event.DOM_VK_TAB:
    569 								// If already an empty last row, ignore forward tab
    570 								if (target.value == "" && !event.shiftKey) {
    571 									var row = Zotero.getAncestorByTagName(target, 'row');
    572 									if (row == row.parentNode.lastChild) {
    573 										return false;
    574 									}
    575 								}
    576 								
    577 								this._tabDirection = event.shiftKey ? -1 : 1;
    578 								yield this.blurHandler(event);
    579 								this._focusField();
    580 								return false;
    581 						}
    582 						
    583 						return true;
    584 					}.bind(this));
    585 				]]></body>
    586 			</method>
    587 			
    588 			<!--
    589 				Intercept paste, check for newlines, and convert textbox
    590 				to multiline if necessary
    591 			-->
    592 			<method name="handlePaste">
    593 				<parameter name="event"/>
    594 				<body>
    595 				<![CDATA[
    596 					var textbox = event.target;
    597 					
    598 					var clip = Components.classes["@mozilla.org/widget/clipboard;1"]
    599 						.getService(Components.interfaces.nsIClipboard);
    600 					var trans = Components.classes["@mozilla.org/widget/transferable;1"]
    601 						.createInstance(Components.interfaces.nsITransferable);
    602 					trans.addDataFlavor("text/unicode");
    603 					clip.getData(trans, clip.kGlobalClipboard);
    604 					var str = {};
    605 					try {
    606 						trans.getTransferData("text/unicode", str, {});
    607 						str = str.value.QueryInterface(Components.interfaces.nsISupportsString).data;
    608 					}
    609 					catch (e) {
    610 						Zotero.debug(e);
    611 						return true;
    612 					}
    613 					
    614 					var multiline = !!str.trim().match(/\n/);
    615 					if (multiline) {
    616 						var self = this;
    617 						setTimeout(function () {
    618 							self.makeMultiline(textbox, str.trim());
    619 						}, 0);
    620 						// Cancel paste
    621 						return false;
    622 					}
    623 					
    624 					return true;
    625 				]]>
    626 				</body>
    627 			</method>
    628 			
    629 			
    630 			<method name="makeMultiline">
    631 				<parameter name="textbox"/>
    632 				<parameter name="value"/>
    633 				<parameter name="rows"/>
    634 				<body><![CDATA[
    635 					// If rows not specified, use one more than lines in input
    636 					if (!rows) {
    637 						rows = value.match(/\n/g).length + 1;
    638 					}
    639 					textbox = this.showEditor(textbox, rows, textbox.getAttribute('value'));
    640 					textbox.value = value;
    641 					// Move cursor to end
    642 					textbox.selectionStart = value.length;
    643 				]]></body>
    644 			</method>
    645 			
    646 			
    647 			<method name="hideEditor">
    648 				<parameter name="event"/>
    649 				<body><![CDATA[
    650 					var textbox = event.target;
    651 					
    652 					return Zotero.spawn(function* () {
    653 						Zotero.debug('Hiding editor');
    654 						
    655 						var fieldName = 'tag';
    656 						var tabindex = textbox.getAttribute('ztabindex');
    657 						
    658 						var oldValue = textbox.getAttribute('value');
    659 						var value = textbox.value = textbox.value.trim();
    660 						
    661 						var tagsbox = Zotero.getAncestorByTagName(textbox, 'tagsbox');
    662 						if (!tagsbox)
    663 						{
    664 							Zotero.debug('Tagsbox not found', 1);
    665 							return;
    666 						}
    667 						
    668 						var row = textbox.parentNode;
    669 						var rows = row.parentNode;
    670 						
    671 						var isNew = row.getAttribute('isNew');
    672 						
    673 						// Remove empty row at end
    674 						if (isNew && value === "") {
    675 							row.parentNode.removeChild(row);
    676 							return;
    677 						}
    678 						
    679 						// If row hasn't changed, change back to label
    680 						if (oldValue == value) {
    681 							this.textboxToLabel(textbox);
    682 							return;
    683 						}
    684 						
    685 						var tags = value.split(/\r\n?|\n/).map(function (val) val.trim());
    686 						
    687 						// Modifying existing tag with a single new one
    688 						if (!isNew && tags.length < 2) {
    689 							if (value !== "") {
    690 								if (oldValue !== value) {
    691 									// The existing textbox will be removed in notify()
    692 									this.removeRow(row);
    693 									this.add(value);
    694 									if (event.type != 'blur') {
    695 										this._focusField();
    696 									}
    697 									try {
    698 										this.item.replaceTag(oldValue, value);
    699 										yield this.item.saveTx();
    700 									}
    701 									catch (e) {
    702 										this.reload();
    703 										throw e;
    704 									}
    705 								}
    706 							}
    707 							// Existing tag cleared
    708 							else {
    709 								try {
    710 									this.removeRow(row);
    711 									if (event.type != 'blur') {
    712 										this._focusField();
    713 									}
    714 									this.item.removeTag(oldValue);
    715 									yield this.item.saveTx();
    716 								}
    717 								catch (e) {
    718 									this.reload();
    719 									throw e;
    720 								}
    721 							}
    722 						}
    723 						// Multiple tags
    724 						else if (tags.length > 1) {
    725 							var lastTag = row == row.parentNode.lastChild;
    726 							
    727 							if (!isNew) {
    728 								// If old tag isn't in array, remove it
    729 								if (tags.indexOf(oldValue) == -1) {
    730 									this.item.removeTag(oldValue);
    731 								}
    732 								// If old tag is staying, restore the textbox
    733 								// immediately. This isn't strictly necessary, but it
    734 								// makes the transition nicer.
    735 								else {
    736 									textbox.value = textbox.getAttribute('value');
    737 									this.textboxToLabel(textbox);
    738 								}
    739 							}
    740 							
    741 							tags.forEach(tag => this.item.addTag(tag));
    742 							yield this.item.saveTx();
    743 							
    744 							if (lastTag) {
    745 								this._lastTabIndex = this.item.getTags().length;
    746 							}
    747 							
    748 							this.reload();
    749 						}
    750 						// Single tag at end
    751 						else {
    752 							if (event.type == 'blur') {
    753 								this.removeRow(row);
    754 							}
    755 							else {
    756 								textbox.value = '';
    757 							}
    758 							this.add(value);
    759 							this.item.addTag(value);
    760 							try {
    761 								yield this.item.saveTx();
    762 							}
    763 							catch (e) {
    764 								this.reload();
    765 								throw e;
    766 							}
    767 						}
    768 					}.bind(this));
    769 				]]></body>
    770 			</method>
    771 			
    772 			
    773 			<method name="newTag">
    774 				<body>
    775 					<![CDATA[
    776 						var rowsElement = this.id('tagRows');
    777 						var rows = rowsElement.childNodes;
    778 						
    779 						// Don't add new row if there already is one
    780 						if (rows.length && rows[rows.length - 1].querySelector('textbox')) {
    781 							return;
    782 						}
    783 						
    784 						var row = this.addDynamicRow();
    785 						row.firstChild.nextSibling.click();
    786 						return row;
    787 					]]>
    788 				</body>
    789 			</method>
    790 			
    791 			
    792 			<method name="textboxToLabel">
    793 				<parameter name="textbox"/>
    794 				<body><![CDATA[
    795 					var elem = this.createValueElement(
    796 						textbox.value, textbox.getAttribute('ztabindex')
    797 					);
    798 					var row = textbox.parentNode;
    799 					row.replaceChild(elem, textbox);
    800 				]]></body>
    801 			</method>
    802 			
    803 			
    804 			<method name="add">
    805 				<parameter name="tagName"/>
    806 				<parameter name="tagType"/>
    807 				<body><![CDATA[
    808 					var rowsElement = this.id('tagRows');
    809 					var rows = rowsElement.childNodes;
    810 					
    811 					// Get this tag's existing row, if there is one
    812 					var row = false;
    813 					for (let i=0; i<rows.length; i++) {
    814 						if (rows[i].getAttribute('tagName') === tagName) {
    815 							return rows[i].getAttribute('ztabindex');
    816 						}
    817 					}
    818 					
    819 					var tagData = {
    820 						tag: tagName,
    821 						type: tagType
    822 					};
    823 					
    824 					if (row) {
    825 						// Update row and label
    826 						this.updateRow(row, tagData);
    827 						var elem = this.createValueElement(tagName);
    828 						
    829 						// Remove the old row, which we'll reinsert at the correct place
    830 						rowsElement.removeChild(row);
    831 						
    832 						// Find the current label or textbox within the row
    833 						// and replace it with the new element -- this is used
    834 						// both when creating new rows and when hiding the
    835 						// entry textbox
    836 						var oldElem = row.getElementsByAttribute('fieldname', 'tag')[0];
    837 						row.replaceChild(elem, oldElem);
    838 					}
    839 					else {
    840 						// Create new row, but don't insert it
    841 						row = this.addDynamicRow(tagData, false, true);
    842 						var elem = row.getElementsByAttribute('fieldname', 'tag')[0];
    843 					}
    844 					
    845 					// Move row to appropriate place, alphabetically
    846 					var collation = Zotero.getLocaleCollation();
    847 					var labels = rowsElement.getElementsByAttribute('fieldname', 'tag');
    848 					
    849 					var before = null;
    850 					var inserted = false;
    851 					var newTabIndex = false;
    852 					for (var i=0; i<labels.length; i++) {
    853 						let index = i + 1;
    854 						if (inserted) {
    855 							labels[i].setAttribute('ztabindex', index);
    856 							continue;
    857 						}
    858 						
    859 						if (collation.compareString(1, tagName, labels[i].textContent) > 0
    860 								// Ignore textbox at end
    861 								&& labels[i].tagName != 'textbox') {
    862 							labels[i].setAttribute('ztabindex', index);
    863 							continue;
    864 						}
    865 						
    866 						elem.setAttribute('ztabindex', index);
    867 						rowsElement.insertBefore(row, labels[i].parentNode);
    868 						newTabIndex = index;
    869 						inserted = true;
    870 					}
    871 					if (!inserted) {
    872 						newTabIndex = i + 1;
    873 						elem.setAttribute('ztabindex', newTabIndex);
    874 						rowsElement.appendChild(row);
    875 					}
    876 					
    877 					this.updateCount(this.count + 1);
    878 					
    879 					return newTabIndex;
    880 				]]></body>
    881 			</method>
    882 			
    883 			
    884 			<method name="remove">
    885 				<parameter name="tagName"/>
    886 				<body><![CDATA[
    887 					var rowsElement = this.id('tagRows');
    888 					var rows = rowsElement.childNodes;
    889 					var removed = false;
    890 					var oldTabIndex = -1;
    891 					for (var i=0; i<rows.length; i++) {
    892 						let value = rows[i].getAttribute('tagName');
    893 						if (value === tagName) {
    894 							oldTabIndex = this.removeRow(rows[i]);
    895 							break;
    896 						}
    897 					}
    898 					return oldTabIndex;
    899 				]]></body>
    900 			</method>
    901 			
    902 			
    903 			<!--
    904 				Remove the row and update tab indexes
    905 			-->
    906 			<method name="removeRow">
    907 				<parameter name="row"/>
    908 				<body><![CDATA[
    909 					var origTabIndex = row.getElementsByAttribute('fieldname', 'tag')[0]
    910 						.getAttribute('ztabindex');
    911 					var origRow = row;
    912 					var i = origTabIndex;
    913 					while (row = row.nextSibling) {
    914 						let elem = row.getElementsByAttribute('fieldname', 'tag')[0];
    915 						elem.setAttribute('ztabindex', i++);
    916 					}
    917 					origRow.parentNode.removeChild(origRow);
    918 					this.updateCount(this.count - 1);
    919 					return origTabIndex;
    920 				]]></body>
    921 			</method>
    922 			
    923 			
    924 			<method name="removeAll">
    925 				<body><![CDATA[
    926 					if (Services.prompt.confirm(null, "", Zotero.getString('pane.item.tags.removeAll'))) {
    927 						this.item.setTags([]);
    928 						this.item.saveTx();
    929 					}
    930 				]]></body>
    931 			</method>
    932 			
    933 			
    934 			<method name="updateCount">
    935 				<parameter name="count"/>
    936 				<body>
    937 					<![CDATA[
    938 						if (!this.item) {
    939 							return;
    940 						}
    941 						
    942 						if(typeof count == 'undefined') {
    943 							var tags = this.item.getTags();
    944 							if (tags) {
    945 								count = tags.length;
    946 							}
    947 							else {
    948 								count = 0;
    949 							}
    950 						}
    951 						
    952 						var str = 'pane.item.tags.count.';
    953 						switch (count){
    954 							case 0:
    955 								str += 'zero';
    956 								break;
    957 							case 1:
    958 								str += 'singular';
    959 								break;
    960 							default:
    961 								str += 'plural';
    962 								break;
    963 						}
    964 						
    965 						this.id('tagsNum').value = Zotero.getString(str, [count]);
    966 						this.count = count;
    967 					]]>
    968 				</body>
    969 			</method>
    970 			
    971 			<method name="closePopup">
    972 				<body>
    973 					<![CDATA[
    974 						if (this.parentNode.hidePopup) {
    975 							this.parentNode.hidePopup()
    976 						}
    977 					]]>
    978 				</body>
    979 			</method>
    980 			
    981 			
    982 			<!-- 
    983 				Open the textbox for a particular label
    984 				
    985 				Note: We're basically replicating the built-in tabindex functionality,
    986 				which doesn't work well with the weird label/textbox stuff we're doing.
    987 				(The textbox being tabbed away from is deleted before the blur()
    988 				completes, so it doesn't know where it's supposed to go next.)
    989 			-->
    990 			<method name="_focusField">
    991 				<body>
    992 				<![CDATA[
    993 					if (this._reloading) {
    994 						return;
    995 					}
    996 					
    997 					if (this._lastTabIndex === false) {
    998 						return;
    999 					}
   1000 					
   1001 					var maxIndex = this.id('tagRows').childNodes.length + 1;
   1002 					
   1003 					var tabindex = parseInt(this._lastTabIndex);
   1004 					var dir = this._tabDirection;
   1005 					
   1006 					if (dir == 1) {
   1007 						var nextIndex = tabindex + 1;
   1008 					}
   1009 					else if (dir == -1) {
   1010 						if (tabindex == 1) {
   1011 							// Focus Add button
   1012 							this.id('addButton').focus();
   1013 							return false;
   1014 						}
   1015 						var nextIndex = tabindex - 1;
   1016 					}
   1017 					else {
   1018 						var nextIndex = tabindex;
   1019 					}
   1020 					
   1021 					nextIndex = Math.min(nextIndex, maxIndex);
   1022 					
   1023 					Zotero.debug('Looking for tabindex ' + nextIndex, 4);
   1024 					
   1025 					var next = document.getAnonymousNodes(this)[0]
   1026 						.getElementsByAttribute('ztabindex', nextIndex);
   1027 					if (next.length) {
   1028 						next = next[0];
   1029 						next.click();
   1030 					}
   1031 					else {
   1032 						next = this.newTag();
   1033 						next = next.firstChild.nextSibling;
   1034 					}
   1035 					
   1036 					if (!next) {
   1037 						Components.utils.reportError('Next row not found');
   1038 						return;
   1039 					}
   1040 					
   1041 					this.ensureElementIsVisible(next);
   1042 				]]>
   1043 				</body>
   1044 			</method>
   1045 			
   1046 			
   1047 			<method name="_onAddButtonKeypress">
   1048 				<parameter name="event"/>
   1049 				<body><![CDATA[
   1050 					if (event.keyCode != event.DOM_VK_TAB || event.shiftKey) {
   1051 						return true;
   1052 					}
   1053 					
   1054 					this._lastTabIndex = 0;
   1055 					this._tabDirection = 1;
   1056 					this._focusField();
   1057 					return false;
   1058 				]]></body>
   1059 			</method>
   1060 			
   1061 			<method name="_onAddButtonPress">
   1062 				<parameter name="event"/>
   1063 				<body><![CDATA[
   1064 					return async function () {
   1065 						await this.blurOpenField();
   1066 						this.newTag();
   1067 					}.bind(this)();
   1068 				]]></body>
   1069 			</method>
   1070 			
   1071 			
   1072 			<method name="_onBackgroundContextMenuShowing">
   1073 				<body><![CDATA[
   1074 					var removeAllTags = this.id('remove-all-item-tags');
   1075 					removeAllTags.disabled = this.count == 0;
   1076 				]]></body>
   1077 			</method>
   1078 			
   1079 			
   1080 			<!-- unused -->
   1081 			<method name="getTagIndex">
   1082 				<parameter name="id"/>
   1083 				<body><![CDATA[
   1084 					var rows = this.id('tagRows').getElementsByTagName('row');
   1085 					for (let i=0; i<rows.length; i++) {
   1086 						var row = rows[i].getAttribute('id');
   1087 						if (row && row.split("-")[1] == id) {
   1088 							return i;
   1089 						}
   1090 					}
   1091 					return -1;
   1092 				]]></body>
   1093 			</method>
   1094 			
   1095 			
   1096 			<method name="scrollToTop">
   1097 				<body>
   1098 				<![CDATA[
   1099 					if (!this._activeScrollbox) {
   1100 						return;
   1101 					}
   1102 					var sbo = this._activeScrollbox.boxObject;
   1103 					sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject);
   1104 					sbo.scrollTo(0,0);
   1105 				]]>
   1106 				</body>
   1107 			</method>
   1108 			
   1109 			
   1110 			<method name="ensureElementIsVisible">
   1111 				<parameter name="elem"/>
   1112 				<body>
   1113 				<![CDATA[
   1114 					var sbo = document.getAnonymousNodes(this)[0].boxObject;
   1115 					sbo.ensureElementIsVisible(elem);
   1116 				]]>
   1117 				</body>
   1118 			</method>
   1119 			
   1120 			
   1121 			<method name="blurOpenField">
   1122 				<parameter name="stayOpen"/>
   1123 				<body><![CDATA[
   1124 					return Zotero.spawn(function* () {
   1125 						this._lastTabIndex = false;
   1126 						
   1127 						var textboxes = document.getAnonymousNodes(this)[0].getElementsByTagName('textbox');
   1128 						if (textboxes && textboxes.length) {
   1129 							yield this.blurHandler({
   1130 								target: textboxes[0],
   1131 								// If coming from the Add button, pretend user pressed return
   1132 								type: stayOpen ? 'keypress' : 'blur',
   1133 								// DOM_VK_RETURN
   1134 								keyCode: stayOpen ? 13 : undefined
   1135 							});
   1136 						}
   1137 					}.bind(this));
   1138 				]]>
   1139 				</body>
   1140 			</method>
   1141 			
   1142 			
   1143 			<method name="id">
   1144 				<parameter name="id"/>
   1145 				<body>
   1146 					<![CDATA[
   1147 						return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
   1148 					]]>
   1149 				</body>
   1150 			</method>
   1151 		</implementation>
   1152 		<content>
   1153 			<xul:scrollbox xbl:inherits="flex" orient="vertical" style="overflow:auto" class="zotero-box"
   1154 					context="tags-context-menu">
   1155 				<xul:popupset>
   1156 					<xul:menupopup id="tags-context-menu"
   1157 							onpopupshowing="document.getBindingParent(this)._onBackgroundContextMenuShowing()">
   1158 						<xul:menuitem id="remove-all-item-tags" label="&zotero.item.tags.removeAll;"
   1159 							oncommand="document.getBindingParent(this).removeAll()"/>
   1160 					</xul:menupopup>
   1161 				</xul:popupset>
   1162 				<xul:hbox align="center">
   1163 					<xul:label id="tagsNum"/>
   1164 					<xul:button id="addButton" label="&zotero.item.add;"
   1165 						onkeypress="return document.getBindingParent(this)._onAddButtonKeypress(event)"
   1166 						oncommand="return document.getBindingParent(this)._onAddButtonPress(event)"/>
   1167 				</xul:hbox>
   1168 				<xul:grid>
   1169 					<xul:columns>
   1170 						<xul:column/>
   1171 						<xul:column flex="1"/>
   1172 						<xul:column/>
   1173 					</xul:columns>
   1174 					<xul:rows id="tagRows"/>
   1175 				</xul:grid>
   1176 			</xul:scrollbox>
   1177 		</content>
   1178 	</binding>
   1179 </bindings>