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>