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 && 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>