
- Fix missing styling in Quick Format dialog - Fix Book Section panel being immediately hidden - Remove low-res Zotero icon - Increase font size and tweak padding
2589 lines
78 KiB
JavaScript
2589 lines
78 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2020 Corporation for Digital Scholarship
|
|
Vienna, Virginia, USA
|
|
https://www.zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
{
|
|
class ItemBox extends XULElement {
|
|
constructor() {
|
|
super();
|
|
|
|
this.clickable = false;
|
|
this.editable = false;
|
|
this.saveOnEdit = false;
|
|
this.showTypeMenu = false;
|
|
this.hideEmptyFields = false;
|
|
this.clickByRow = false;
|
|
this.clickByItem = false;
|
|
|
|
this.clickHandler = null;
|
|
this.blurHandler = null;
|
|
this.eventHandlers = [];
|
|
|
|
this._mode = 'view';
|
|
this._visibleFields = [];
|
|
this._hiddenFields = [];
|
|
this._clickableFields = [];
|
|
this._editableFields = [];
|
|
this._fieldAlternatives = {};
|
|
this._fieldOrder = [];
|
|
this._tabIndexMinCreators = 10;
|
|
this._tabIndexMaxCreators = 0;
|
|
this._tabIndexMinFields = 1000;
|
|
this._tabIndexMaxFields = 0;
|
|
this._initialVisibleCreators = 5;
|
|
|
|
this.content = MozXULElement.parseXULToFragment(`
|
|
<div id="item-box" xmlns="http://www.w3.org/1999/xhtml">
|
|
<popupset xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
|
<menupopup id="creator-type-menu" position="after_start"/>
|
|
<menupopup id="zotero-field-transform-menu">
|
|
<menuitem id="creator-transform-title-case" label="&zotero.item.textTransform.titlecase;"
|
|
class="menuitem-non-iconic"/>
|
|
<menuitem id="creator-transform-sentence-case" label="&zotero.item.textTransform.sentencecase;"
|
|
class="menuitem-non-iconic"/>
|
|
</menupopup>
|
|
<menupopup id="zotero-creator-transform-menu">
|
|
<menuitem id="creator-transform-swap-names" label="&zotero.item.creatorTransform.nameSwap;"/>
|
|
<menuitem id="creator-transform-capitalize" label="&zotero.item.creatorTransform.fixCase;"/>
|
|
</menupopup>
|
|
<menupopup id="zotero-doi-menu">
|
|
<menuitem id="zotero-doi-menu-view-online" label="&zotero.item.viewOnline;"/>
|
|
<menuitem id="zotero-doi-menu-copy" label="&zotero.item.copyAsURL;"/>
|
|
</menupopup>
|
|
<guidance-panel id="zotero-author-guidance" about="authorMenu" position="after_end" x="-25"/>
|
|
</popupset>
|
|
<div id="retraction-box" hidden="hidden">
|
|
<div id="retraction-header">
|
|
<div id="retraction-header-text"/>
|
|
</div>
|
|
<div id="retraction-details">
|
|
<p id="retraction-date"/>
|
|
|
|
<dl id="retraction-reasons"/>
|
|
|
|
<p id="retraction-notice"/>
|
|
|
|
<div id="retraction-links"/>
|
|
|
|
<p id="retraction-credit"/>
|
|
<div id="retraction-hide"><button/></div>
|
|
</div>
|
|
</div>
|
|
<table id="info-table">
|
|
<tr>
|
|
<th><label class="key">&zotero.items.itemType;</label></th>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
`, ['chrome://zotero/locale/zotero.dtd']);
|
|
}
|
|
|
|
connectedCallback() {
|
|
this._destroyed = false;
|
|
window.addEventListener("unload", this.destroy);
|
|
|
|
this.appendChild(document.importNode(this.content, true));
|
|
|
|
this._creatorTypeMenu.addEventListener('popupshowing', () => {
|
|
var typeBox = document.popupNode.localName == 'th' ? document.popupNode : document.popupNode.parentNode;
|
|
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
|
|
|
var item = this.item;
|
|
var exists = item.hasCreatorAt(index);
|
|
var moreCreators = item.numCreators() > index + 1;
|
|
|
|
var hideMoveToTop = !exists || index < 2;
|
|
var hideMoveUp = !exists || index == 0;
|
|
var hideMoveDown = !exists || !moreCreators;
|
|
var hideMoveSep = hideMoveUp && hideMoveDown;
|
|
|
|
this._id('zotero-creator-move-sep').setAttribute('hidden', hideMoveSep);
|
|
this._id('zotero-creator-move-to-top').setAttribute('hidden', hideMoveToTop);
|
|
this._id('zotero-creator-move-up').setAttribute('hidden', hideMoveUp);
|
|
this._id('zotero-creator-move-down').setAttribute('hidden', hideMoveDown);
|
|
});
|
|
|
|
this._creatorTypeMenu.addEventListener('command', async (event) => {
|
|
var typeBox = document.popupNode.localName == 'th' ? document.popupNode : document.popupNode.parentNode;
|
|
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
|
|
|
if (event.explicitOriginalTarget.className == 'zotero-creator-move') {
|
|
let dir;
|
|
switch (event.explicitOriginalTarget.id) {
|
|
case 'zotero-creator-move-to-top':
|
|
dir = 'top';
|
|
break;
|
|
|
|
case 'zotero-creator-move-up':
|
|
dir = 'up';
|
|
break;
|
|
|
|
case 'zotero-creator-move-down':
|
|
dir = 'down';
|
|
break;
|
|
}
|
|
this.moveCreator(index, dir);
|
|
return;
|
|
}
|
|
|
|
var typeID = event.explicitOriginalTarget.getAttribute('typeid');
|
|
var row = typeBox.parentNode;
|
|
var fields = this.getCreatorFields(row);
|
|
fields.creatorTypeID = typeID;
|
|
typeBox.getElementsByTagName('label')[0].textContent = Zotero.getString(
|
|
'creatorTypes.' + Zotero.CreatorTypes.getName(typeID)
|
|
);
|
|
typeBox.setAttribute('typeid', typeID);
|
|
|
|
/* If a creator textbox is already open, we need to
|
|
change its autocomplete parameters so that it
|
|
completes on a creator with a different creator type */
|
|
var changedParams = {
|
|
creatorTypeID: typeID
|
|
};
|
|
this._updateAutoCompleteParams(row, changedParams);
|
|
|
|
this.modifyCreator(index, fields);
|
|
if (this.saveOnEdit) {
|
|
await this.blurOpenField();
|
|
await this.item.saveTx();
|
|
}
|
|
});
|
|
|
|
this._id('zotero-field-transform-menu').addEventListener('popupshowing', () => {
|
|
this._id('creator-transform-title-case').disabled = !this.canTextTransformField(document.popupNode, 'title');
|
|
this._id('creator-transform-sentence-case').disabled = !this.canTextTransformField(document.popupNode, 'sentence');
|
|
});
|
|
|
|
this._id('creator-transform-title-case').addEventListener('command',
|
|
() => this.textTransformField(document.popupNode, 'title'));
|
|
this._id('creator-transform-sentence-case').addEventListener('command',
|
|
() => this.textTransformField(document.popupNode, 'sentence'));
|
|
|
|
this._id('zotero-creator-transform-menu').addEventListener('popupshowing', (event) => {
|
|
var row = document.popupNode.closest('tr');
|
|
var typeBox = row.querySelector('.creator-type-label');
|
|
var index = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
|
var item = this.item;
|
|
var exists = item.hasCreatorAt(index);
|
|
if (exists) {
|
|
var fieldMode = item.getCreator(index).name !== undefined ? 1 : 0;
|
|
}
|
|
var hideTransforms = !exists || !!fieldMode;
|
|
if (hideTransforms) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
this._id('creator-transform-swap-names').addEventListener('command',
|
|
event => this.swapNames(event));
|
|
|
|
this._id('creator-transform-capitalize').addEventListener('command',
|
|
event => this.capitalizeCreatorName(event));
|
|
|
|
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'itemBox');
|
|
}
|
|
|
|
destroy() {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
window.removeEventListener("unload", this.destroy);
|
|
this._destroyed = true;
|
|
|
|
Zotero.Notifier.unregisterObserver(this._notifierID);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
// Empty the DOM. We will rebuild if reconnected.
|
|
while (this.lastChild) {
|
|
this.removeChild(this.lastChild);
|
|
}
|
|
this.destroy();
|
|
}
|
|
|
|
|
|
//
|
|
// Public properties
|
|
//
|
|
|
|
// Modes are predefined settings groups for particular tasks
|
|
get mode() {
|
|
return this._mode;
|
|
}
|
|
|
|
set mode(val) {
|
|
this.clickable = false;
|
|
this.editable = false;
|
|
this.saveOnEdit = false;
|
|
this.showTypeMenu = false;
|
|
this.hideEmptyFields = false;
|
|
this.clickByRow = false;
|
|
this.clickByItem = false;
|
|
|
|
switch (val) {
|
|
case 'view':
|
|
case 'merge':
|
|
break;
|
|
|
|
case 'edit':
|
|
this.clickable = true;
|
|
this.editable = true;
|
|
this.saveOnEdit = true;
|
|
this.showTypeMenu = true;
|
|
this.clickHandler = this.showEditor;
|
|
this.blurHandler = this.hideEditor;
|
|
break;
|
|
|
|
case 'fieldmerge':
|
|
this.hideEmptyFields = true;
|
|
this._fieldAlternatives = {};
|
|
break;
|
|
|
|
default:
|
|
throw new Error(`Invalid mode '${val}'`);
|
|
}
|
|
|
|
this._mode = val;
|
|
this.setAttribute('mode', val);
|
|
}
|
|
|
|
get item() {
|
|
return this._item;
|
|
}
|
|
|
|
set item(val) {
|
|
if (!(val instanceof Zotero.Item)) {
|
|
throw new Error("'item' must be a Zotero.Item");
|
|
}
|
|
|
|
// When changing items, reset truncation of creator list
|
|
if (!this._item || val.id != this._item.id) {
|
|
this._displayAllCreators = false;
|
|
}
|
|
|
|
// If switching items, save the current item first
|
|
// Before fx102, clicking an item in the item tree would send a blur event before ItemBox.item was updated.
|
|
// Now, ItemBox.item is set first, causing us to update this._item and remove the open field before it can
|
|
// receive a blur event and trigger a save.
|
|
if (this._item && val.id != this._item.id) {
|
|
// Not awaiting the blurOpenField() call here is not great practice, but it's unavoidable - setters
|
|
// can't be async and should immediately update their backing fields. Additionally, it matches the old
|
|
// behavior, as the blur event was triggered immediately before the item setter, with the
|
|
// Zotero.Item#saveTx() call continuing in the background.
|
|
this.blurOpenField();
|
|
}
|
|
|
|
this._item = val;
|
|
this._lastTabIndex = null;
|
|
this.scrollToTop();
|
|
this.refresh();
|
|
}
|
|
|
|
// .ref is an alias for .item
|
|
get ref() {
|
|
return this._item;
|
|
}
|
|
|
|
set ref(val) {
|
|
this.item = val;
|
|
}
|
|
|
|
|
|
/**
|
|
* An array of field names that should be shown
|
|
* even if they're empty and hideEmptyFields is set
|
|
*/
|
|
set visibleFields(val) {
|
|
if (val.constructor.name != 'Array') {
|
|
throw ('visibleFields must be an array in <itembox>.visibleFields');
|
|
}
|
|
|
|
this._visibleFields = val;
|
|
}
|
|
|
|
/**
|
|
* An array of field names that should be hidden
|
|
*/
|
|
set hiddenFields(val) {
|
|
if (val.constructor.name != 'Array') {
|
|
throw ('hiddenFields must be an array in <itembox>.visibleFields');
|
|
}
|
|
|
|
this._hiddenFields = val;
|
|
}
|
|
|
|
/**
|
|
* An array of field names that should be clickable
|
|
* even if this.clickable is false
|
|
*/
|
|
set clickableFields(val) {
|
|
if (val.constructor.name != 'Array') {
|
|
throw ('clickableFields must be an array in <itembox>.clickableFields');
|
|
}
|
|
|
|
this._clickableFields = val;
|
|
}
|
|
|
|
/**
|
|
* An array of field names that should be editable
|
|
* even if this.editable is false
|
|
*/
|
|
set editableFields(val) {
|
|
if (val.constructor.name != 'Array') {
|
|
throw ('editableFields must be an array in <itembox>.editableFields');
|
|
}
|
|
|
|
this._editableFields = val;
|
|
}
|
|
|
|
/**
|
|
* An object of alternative values for keyed fields
|
|
*/
|
|
set fieldAlternatives(val) {
|
|
if (val.constructor.name != 'Object') {
|
|
throw ('fieldAlternatives must be an Object in <itembox>.fieldAlternatives');
|
|
}
|
|
|
|
if (this.mode != 'fieldmerge') {
|
|
throw ('fieldAlternatives is valid only in fieldmerge mode in <itembox>.fieldAlternatives');
|
|
}
|
|
|
|
this._fieldAlternatives = val;
|
|
}
|
|
|
|
/**
|
|
* An array of field names in the order they should appear
|
|
* in the list; empty spaces can be created with null
|
|
*/
|
|
set fieldOrder(val) {
|
|
if (val.constructor.name != 'Array') {
|
|
throw ('fieldOrder must be an array in <itembox>.fieldOrder');
|
|
}
|
|
|
|
this._fieldOrder = val;
|
|
}
|
|
|
|
get itemTypeMenu() {
|
|
return this._id('item-type-menu');
|
|
}
|
|
|
|
//
|
|
// Private properties
|
|
//
|
|
get _infoTable() {
|
|
return this._id('info-table');
|
|
}
|
|
|
|
get _creatorTypeMenu() {
|
|
return this._id('creator-type-menu');
|
|
}
|
|
|
|
get _defaultFirstName() {
|
|
return '(' + Zotero.getString('pane.item.defaultFirstName') + ')';
|
|
}
|
|
|
|
get _defaultLastName() {
|
|
return '(' + Zotero.getString('pane.item.defaultLastName') + ')';
|
|
}
|
|
|
|
get _defaultFullName() {
|
|
return '(' + Zotero.getString('pane.item.defaultFullName') + ')';
|
|
}
|
|
|
|
|
|
//
|
|
// Methods
|
|
//
|
|
notify(event, type, ids) {
|
|
if (event != 'modify' || !this.item || !this.item.id) return;
|
|
for (let i = 0; i < ids.length; i++) {
|
|
let id = ids[i];
|
|
if (id != this.item.id) {
|
|
continue;
|
|
}
|
|
this.refresh();
|
|
break;
|
|
}
|
|
}
|
|
|
|
refresh() {
|
|
Zotero.debug('Refreshing item box');
|
|
|
|
if (!this.item) {
|
|
Zotero.debug('No item to refresh', 2);
|
|
return;
|
|
}
|
|
|
|
this.updateRetracted();
|
|
|
|
if (this.clickByItem) {
|
|
this.onclick = () => this.clickHandler(this);
|
|
}
|
|
|
|
// Item type menu
|
|
if (!this.itemTypeMenu) {
|
|
this.addItemTypeMenu();
|
|
}
|
|
if (this.showTypeMenu) {
|
|
this.updateItemTypeMenuSelection();
|
|
this.itemTypeMenu.parentNode.parentNode.style.display = 'contents';
|
|
this.itemTypeMenu.setAttribute('ztabindex', '0');
|
|
}
|
|
else {
|
|
this.itemTypeMenu.parentNode.parentNode.style.display = 'none';
|
|
}
|
|
|
|
//
|
|
// Clear and rebuild metadata fields
|
|
//
|
|
while (this._infoTable.childNodes.length > 1) {
|
|
this._infoTable.removeChild(this._infoTable.lastChild);
|
|
}
|
|
|
|
var fieldNames = [];
|
|
|
|
// Manual field order
|
|
if (this._fieldOrder.length) {
|
|
for (let field of this._fieldOrder) {
|
|
fieldNames.push(field);
|
|
}
|
|
}
|
|
// Get field order from database
|
|
else {
|
|
if (!this.showTypeMenu) {
|
|
fieldNames.push("itemType");
|
|
}
|
|
|
|
var fields = Zotero.ItemFields.getItemTypeFields(this.item.getField("itemTypeID"));
|
|
|
|
for (var i=0; i<fields.length; i++) {
|
|
fieldNames.push(Zotero.ItemFields.getName(fields[i]));
|
|
}
|
|
|
|
if (this.item instanceof Zotero.FeedItem) {
|
|
let row = ZoteroPane_Local.getCollectionTreeRow();
|
|
if (row && row.isFeeds()) {
|
|
fieldNames.unshift("feed");
|
|
}
|
|
}
|
|
else {
|
|
fieldNames.push("dateAdded", "dateModified");
|
|
}
|
|
}
|
|
|
|
for (var i=0; i<fieldNames.length; i++) {
|
|
var fieldName = fieldNames[i];
|
|
var val = '';
|
|
|
|
if (fieldName) {
|
|
var fieldID = Zotero.ItemFields.getID(fieldName);
|
|
if (fieldID && !Zotero.ItemFields.isValidForType(fieldID, this.item.itemTypeID)) {
|
|
fieldName = null;
|
|
}
|
|
}
|
|
|
|
if (fieldName) {
|
|
if (this._hiddenFields.indexOf(fieldName) != -1) {
|
|
continue;
|
|
}
|
|
|
|
// createValueElement() adds the itemTypeID as an attribute
|
|
// and converts it to a localized string for display
|
|
if (fieldName == 'itemType') {
|
|
val = this.item.itemTypeID;
|
|
}
|
|
// Fake "field" in the feeds global view that displays the name
|
|
// of the containing feed
|
|
else if (fieldName == 'feed') {
|
|
val = Zotero.Feeds.get(this.item.libraryID)?.name;
|
|
}
|
|
else {
|
|
val = this.item.getField(fieldName);
|
|
}
|
|
|
|
if (!val && this.hideEmptyFields
|
|
&& this._visibleFields.indexOf(fieldName) == -1
|
|
&& (this.mode != 'fieldmerge' || typeof this._fieldAlternatives[fieldName] == 'undefined')) {
|
|
continue;
|
|
}
|
|
|
|
var fieldIsClickable = this._fieldIsClickable(fieldName);
|
|
|
|
// Start tabindex at 1001 after creators
|
|
var tabindex = fieldIsClickable
|
|
? (i>0 ? this._tabIndexMinFields + i : 1) : 0;
|
|
this._tabIndexMaxFields = Math.max(this._tabIndexMaxFields, tabindex);
|
|
|
|
if (fieldIsClickable
|
|
&& !Zotero.Items.isPrimaryField(fieldName)
|
|
&& Zotero.ItemFields.isDate(fieldName)
|
|
// TEMP - NSF
|
|
&& fieldName != 'dateSent') {
|
|
this.addDateRow(fieldName, this.item.getField(fieldName, true), tabindex);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let th = document.createElement("th");
|
|
th.setAttribute('fieldname', fieldName);
|
|
|
|
let valueElement = this.createValueElement(
|
|
val, fieldName, tabindex
|
|
);
|
|
|
|
var prefix = '';
|
|
// Add '(...)' before 'Abstract' for collapsed abstracts
|
|
if (fieldName == 'abstractNote') {
|
|
if (val && !Zotero.Prefs.get('lastAbstractExpand')) {
|
|
prefix = '(\u2026) ';
|
|
}
|
|
}
|
|
|
|
if (fieldName) {
|
|
let label = document.createElement('label');
|
|
label.className = 'key';
|
|
label.textContent = prefix + Zotero.ItemFields.getLocalizedString(fieldName);
|
|
th.appendChild(label);
|
|
}
|
|
|
|
// TEMP - NSF (homepage)
|
|
if ((fieldName == 'url' || fieldName == 'homepage')
|
|
// Only make plausible HTTP URLs clickable
|
|
&& Zotero.Utilities.isHTTPURL(val, true)) {
|
|
th.classList.add("pointer");
|
|
// TODO: make getFieldValue non-private and use below instead
|
|
th.addEventListener('click', () => Zotero.launchURL(th.nextSibling.firstChild.value || th.nextSibling.firstChild.textContent));
|
|
th.setAttribute('title', Zotero.getString('pane.item.viewOnline.tooltip'));
|
|
}
|
|
else if (fieldName == 'DOI' && val && typeof val == 'string') {
|
|
// Pull out DOI, in case there's a prefix
|
|
let doi = Zotero.Utilities.cleanDOI(val);
|
|
if (doi) {
|
|
doi = "https://doi.org/"
|
|
// Encode some characters that are technically valid in DOIs,
|
|
// though generally not used. '/' doesn't need to be encoded.
|
|
+ doi.replace(/#/g, '%23')
|
|
.replace(/\?/g, '%3f')
|
|
.replace(/%/g, '%25')
|
|
.replace(/"/g, '%22');
|
|
th.classList.add("pointer");
|
|
th.addEventListener('click', event => ZoteroPane_Local.loadURI(doi, event));
|
|
th.setAttribute('title', Zotero.getString('pane.item.viewOnline.tooltip'));
|
|
|
|
var openURLMenuItem = this._id('zotero-doi-menu-view-online');
|
|
openURLMenuItem.addEventListener('command', event => ZoteroPane_Local.loadURI(doi, event));
|
|
|
|
var copyMenuItem = this._id('zotero-doi-menu-copy');
|
|
copyMenuItem.addEventListener('command', () => Zotero.Utilities.Internal.copyTextToClipboard(doi));
|
|
}
|
|
}
|
|
else if (fieldName == 'abstractNote') {
|
|
if (val.length) {
|
|
th.classList.add("pointer");
|
|
}
|
|
th.addEventListener('click', function () {
|
|
if (this.nextSibling.querySelector('input, textarea')) {
|
|
this.nextSibling.querySelector('input, textarea').blur();
|
|
}
|
|
else {
|
|
this.closest('item-box').toggleAbstractExpand(
|
|
this.firstElementChild, this.closest('tr').querySelector('.value')
|
|
);
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
th.addEventListener('click', function () {
|
|
if (this.nextSibling.querySelector('input, textarea')) {
|
|
this.nextSibling.querySelector('input, textarea').blur();
|
|
}
|
|
});
|
|
}
|
|
|
|
let td = document.createElement('td');
|
|
td.appendChild(valueElement);
|
|
|
|
this.addDynamicRow(th, td);
|
|
|
|
if (fieldName && this._selectField == fieldName) {
|
|
this.showEditor(valueElement);
|
|
}
|
|
|
|
// In field merge mode, add a button to switch field versions
|
|
else if (this.mode == 'fieldmerge' && typeof this._fieldAlternatives[fieldName] != 'undefined') {
|
|
var button = document.createXULElement("toolbarbutton");
|
|
button.className = 'zotero-field-version-button';
|
|
button.setAttribute('image', 'chrome://zotero/skin/treesource-duplicates.png');
|
|
button.setAttribute('type', 'menu');
|
|
button.setAttribute('wantdropmarker', true);
|
|
|
|
var popup = button.appendChild(document.createXULElement("menupopup"));
|
|
|
|
for (let v of this._fieldAlternatives[fieldName]) {
|
|
let menuitem = document.createXULElement("menuitem");
|
|
var sv = Zotero.Utilities.ellipsize(v, 60);
|
|
menuitem.setAttribute('label', sv);
|
|
if (v != sv) {
|
|
menuitem.setAttribute('tooltiptext', v);
|
|
}
|
|
menuitem.setAttribute('fieldName', fieldName);
|
|
menuitem.setAttribute('originalValue', v);
|
|
menuitem.addEventListener('command', () => {
|
|
this.item.setField(
|
|
menuitem.getAttribute('fieldName'),
|
|
menuitem.getAttribute('originalValue')
|
|
);
|
|
this.refresh();
|
|
});
|
|
popup.appendChild(menuitem);
|
|
}
|
|
|
|
td.appendChild(button);
|
|
}
|
|
}
|
|
this._selectField = false;
|
|
|
|
//
|
|
// Creators
|
|
//
|
|
|
|
// Creator type menu
|
|
if (this.editable) {
|
|
while (this._creatorTypeMenu.hasChildNodes()) {
|
|
this._creatorTypeMenu.removeChild(this._creatorTypeMenu.firstChild);
|
|
}
|
|
|
|
var creatorTypes = Zotero.CreatorTypes.getTypesForItemType(this.item.itemTypeID);
|
|
|
|
var localized = {};
|
|
for (var i=0; i<creatorTypes.length; i++) {
|
|
localized[creatorTypes[i]['name']]
|
|
= Zotero.getString('creatorTypes.' + creatorTypes[i]['name']);
|
|
}
|
|
|
|
for (var i in localized) {
|
|
var menuitem = document.createXULElement("menuitem");
|
|
menuitem.setAttribute("label", localized[i]);
|
|
menuitem.setAttribute("typeid", Zotero.CreatorTypes.getID(i));
|
|
this._creatorTypeMenu.appendChild(menuitem);
|
|
}
|
|
|
|
var moveSep = document.createXULElement("menuseparator");
|
|
var moveToTop = document.createXULElement("menuitem");
|
|
var moveUp = document.createXULElement("menuitem");
|
|
var moveDown = document.createXULElement("menuitem");
|
|
moveSep.id = "zotero-creator-move-sep";
|
|
moveToTop.id = "zotero-creator-move-to-top";
|
|
moveUp.id = "zotero-creator-move-up";
|
|
moveDown.id = "zotero-creator-move-down";
|
|
moveToTop.className = "zotero-creator-move";
|
|
moveUp.className = "zotero-creator-move";
|
|
moveDown.className = "zotero-creator-move";
|
|
moveToTop.setAttribute("label", Zotero.getString('pane.item.creator.moveToTop'));
|
|
moveUp.setAttribute("label", Zotero.getString('pane.item.creator.moveUp'));
|
|
moveDown.setAttribute("label", Zotero.getString('pane.item.creator.moveDown'));
|
|
this._creatorTypeMenu.appendChild(moveSep);
|
|
this._creatorTypeMenu.appendChild(moveToTop);
|
|
this._creatorTypeMenu.appendChild(moveUp);
|
|
this._creatorTypeMenu.appendChild(moveDown);
|
|
}
|
|
|
|
// Creator rows
|
|
|
|
// Place, in order of preference, after title, after type,
|
|
// or at beginning
|
|
var titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
|
|
var field = this._infoTable.querySelector(`[fieldname="${Zotero.ItemFields.getName(titleFieldID)}"]`);
|
|
if (!field) {
|
|
var field = this._infoTable.querySelector('[fieldName="itemType"]');
|
|
}
|
|
if (field) {
|
|
this._beforeRow = field.parentNode.nextSibling;
|
|
}
|
|
else {
|
|
this._beforeRow = this._infoTable.firstChild;
|
|
}
|
|
|
|
this._creatorCount = 0;
|
|
var num = this.item.numCreators();
|
|
if (num > 0) {
|
|
// Limit number of creators display
|
|
var max = Math.min(num, this._initialVisibleCreators);
|
|
// If only 1 or 2 more, just display
|
|
if (num < max + 3 || this._displayAllCreators) {
|
|
max = num;
|
|
}
|
|
for (var i = 0; i < max; i++) {
|
|
let data = this.item.getCreator(i);
|
|
this.addCreatorRow(data, data.creatorTypeID);
|
|
|
|
// Display "+" button on all but last row
|
|
if (i == max - 2) {
|
|
this.disableCreatorAddButtons();
|
|
}
|
|
}
|
|
|
|
// Additional creators not displayed
|
|
if (num > max) {
|
|
this.addMoreCreatorsRow(num - max);
|
|
|
|
this.disableCreatorAddButtons();
|
|
}
|
|
else {
|
|
// If we didn't start with creators truncated,
|
|
// don't truncate for as long as we're viewing
|
|
// this item, so that added creators aren't
|
|
// immediately hidden
|
|
this._displayAllCreators = true;
|
|
|
|
if (this._addCreatorRow) {
|
|
this.addCreatorRow(false, this.item.getCreator(max-1).creatorTypeID, true);
|
|
this._addCreatorRow = false;
|
|
this.disableCreatorAddButtons();
|
|
}
|
|
}
|
|
}
|
|
else if (this.editable && Zotero.CreatorTypes.itemTypeHasCreators(this.item.itemTypeID)) {
|
|
// Add default row
|
|
this.addCreatorRow(false, false, true, true);
|
|
this.disableCreatorAddButtons();
|
|
}
|
|
|
|
// Move to next or previous field if (shift-)tab was pressed
|
|
if (this._lastTabIndex && this._lastTabIndex != -1) {
|
|
this._focusNextField(this._lastTabIndex);
|
|
}
|
|
|
|
if (this._showCreatorTypeGuidance) {
|
|
let creatorTypeLabels = this.querySelectorAll(".creator-type-label");
|
|
this._id("zotero-author-guidance").show({
|
|
forEl: creatorTypeLabels[creatorTypeLabels.length - 1]
|
|
});
|
|
this._showCreatorTypeGuidance = false;
|
|
}
|
|
|
|
this._refreshed = true;
|
|
}
|
|
|
|
addItemTypeMenu() {
|
|
var td = document.createElement('td');
|
|
var menulist = document.createXULElement("menulist", { is: "menulist-item-types" });
|
|
menulist.id = "item-type-menu";
|
|
menulist.className = "zotero-clicky";
|
|
menulist.addEventListener('command', (event) => {
|
|
this.changeTypeTo(event.target.value, menulist);
|
|
});
|
|
menulist.addEventListener('focus', () => {
|
|
this.ensureElementIsVisible(menulist);
|
|
});
|
|
menulist.addEventListener('keypress', (event) => {
|
|
if (event.keyCode == event.DOM_VK_TAB) {
|
|
this.itemTypeMenuTab(event);
|
|
}
|
|
});
|
|
td.appendChild(menulist);
|
|
this._infoTable.firstChild.appendChild(td);
|
|
}
|
|
|
|
updateItemTypeMenuSelection() {
|
|
this.itemTypeMenu.value = this.item.itemTypeID;
|
|
}
|
|
|
|
addDynamicRow(label, value, beforeElement) {
|
|
var row = document.createElement("tr");
|
|
|
|
// Add click event to row
|
|
if (this._rowIsClickable(value.getAttribute('fieldname'))) {
|
|
row.className = 'zotero-clicky';
|
|
row.addEventListener('click', (event) => {
|
|
this.clickHandler(event.target);
|
|
}, false);
|
|
}
|
|
|
|
row.appendChild(label);
|
|
row.appendChild(value);
|
|
if (beforeElement) {
|
|
this._infoTable.insertBefore(row, this._beforeRow);
|
|
}
|
|
else {
|
|
this._infoTable.appendChild(row);
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
addCreatorRow(creatorData, creatorTypeIDOrName, unsaved, defaultRow) {
|
|
// getCreatorFields(), switchCreatorMode() and handleCreatorAutoCompleteSelect()
|
|
// may need need to be adjusted if this DOM structure changes
|
|
|
|
var fieldMode = Zotero.Prefs.get('lastCreatorFieldMode');
|
|
var firstName = '';
|
|
var lastName = '';
|
|
if (creatorData) {
|
|
fieldMode = creatorData.fieldMode;
|
|
firstName = creatorData.firstName;
|
|
lastName = creatorData.lastName;
|
|
}
|
|
|
|
// Sub in placeholder text for empty fields
|
|
if (fieldMode == 1) {
|
|
if (lastName === "") {
|
|
lastName = this._defaultFullName;
|
|
}
|
|
}
|
|
else {
|
|
if (firstName === "") {
|
|
firstName = this._defaultFirstName;
|
|
}
|
|
if (lastName === "") {
|
|
lastName = this._defaultLastName;
|
|
}
|
|
}
|
|
|
|
// Use the first entry in the drop-down for the default type if none specified
|
|
var typeID = creatorTypeIDOrName
|
|
? Zotero.CreatorTypes.getID(creatorTypeIDOrName)
|
|
: this._creatorTypeMenu.childNodes[0].getAttribute('typeid');
|
|
|
|
var rowIndex = this._creatorCount;
|
|
var tabindex = this._tabIndexMinCreators + ((rowIndex - 1) * 6);
|
|
|
|
var th = document.createElement("th");
|
|
th.setAttribute("typeid", typeID);
|
|
th.setAttribute("fieldname", 'creator-' + rowIndex + '-typeID');
|
|
if (this.editable) {
|
|
th.className = 'creator-type-label zotero-clicky zotero-focusable';
|
|
let span = document.createElement('span');
|
|
span.className = 'creator-type-dropmarker';
|
|
th.appendChild(span);
|
|
th.setAttribute('ztabindex', tabindex);
|
|
th.setAttribute('role', 'button');
|
|
th.setAttribute('aria-describedby', 'creator-type-label-inner');
|
|
th.addEventListener('click', () => {
|
|
document.popupNode = th;
|
|
this._creatorTypeMenu.openPopup(th);
|
|
});
|
|
}
|
|
else {
|
|
th.className = 'creator-type-label';
|
|
}
|
|
|
|
var label = document.createElement("label");
|
|
label.setAttribute('id', 'creator-type-label-inner');
|
|
label.className = 'key';
|
|
label.textContent = Zotero.getString('creatorTypes.' + Zotero.CreatorTypes.getName(typeID));
|
|
th.appendChild(label);
|
|
|
|
var td = document.createElement("td");
|
|
td.className = 'creator-type-value';
|
|
|
|
// Name
|
|
var firstlast = document.createElement("span");
|
|
firstlast.className = 'creator-name-box';
|
|
|
|
var fieldName = 'creator-' + rowIndex + '-lastName';
|
|
var lastNameElem = firstlast.appendChild(
|
|
this.createValueElement(
|
|
lastName,
|
|
fieldName,
|
|
tabindex + 1
|
|
)
|
|
);
|
|
|
|
// Comma
|
|
var comma = document.createElement("span");
|
|
comma.textContent = Zotero.getString('punctuation.comma');
|
|
comma.className = 'comma';
|
|
firstlast.appendChild(comma);
|
|
|
|
var fieldName = 'creator-' + rowIndex + '-firstName';
|
|
firstlast.appendChild(
|
|
this.createValueElement(
|
|
firstName,
|
|
fieldName,
|
|
tabindex + 2
|
|
)
|
|
);
|
|
if (fieldMode > 0) {
|
|
firstlast.lastChild.hidden = true;
|
|
}
|
|
|
|
if (this.editable) {
|
|
firstlast.oncontextmenu = (event) => {
|
|
document.popupNode = firstlast;
|
|
this._id('creator-transform-swap-names').hidden = fieldMode > 0;
|
|
this._id('creator-transform-capitalize').disabled = !this.canCapitalizeCreatorName(td.parentNode);
|
|
this._id('zotero-creator-transform-menu').openPopupAtScreen(
|
|
event.screenX + 1,
|
|
event.screenY + 1,
|
|
true
|
|
);
|
|
};
|
|
}
|
|
|
|
this._tabIndexMaxCreators = Math.max(this._tabIndexMaxCreators, tabindex);
|
|
|
|
td.appendChild(firstlast);
|
|
|
|
// Single/double field toggle
|
|
var toggleButton = document.createElement('button');
|
|
toggleButton.setAttribute('fieldname',
|
|
'creator-' + rowIndex + '-fieldMode');
|
|
toggleButton.className = 'zotero-field-toggle zotero-clicky zotero-focusable';
|
|
toggleButton.setAttribute('ztabindex', tabindex + 3);
|
|
td.appendChild(toggleButton);
|
|
|
|
// Minus (-) button
|
|
var removeButton = document.createElement('button');
|
|
removeButton.textContent = "-";
|
|
removeButton.setAttribute("class", "zotero-clicky zotero-clicky-minus zotero-focusable");
|
|
removeButton.setAttribute('ztabindex', tabindex + 4);
|
|
removeButton.setAttribute('aria-label', Zotero.getString('general.delete'));
|
|
// If default first row, don't let user remove it
|
|
if (defaultRow) {
|
|
this.disableButton(removeButton);
|
|
}
|
|
else {
|
|
removeButton.addEventListener("click", () => {
|
|
this.removeCreator(rowIndex, td.parentNode);
|
|
});
|
|
}
|
|
td.appendChild(removeButton);
|
|
|
|
// Plus (+) button
|
|
var addButton = document.createElement('button');
|
|
addButton.textContent = "+";
|
|
addButton.setAttribute("class", "zotero-clicky zotero-clicky-plus zotero-focusable");
|
|
addButton.setAttribute('ztabindex', tabindex + 5);
|
|
// If row isn't saved, don't let user add more
|
|
if (unsaved) {
|
|
this.disableButton(addButton);
|
|
}
|
|
else {
|
|
this._enablePlusButton(addButton, typeID, fieldMode);
|
|
}
|
|
td.appendChild(addButton);
|
|
|
|
for (const domEl of [th, toggleButton, removeButton, addButton]) {
|
|
domEl.setAttribute('tabindex', '0');
|
|
domEl.addEventListener('keypress', this.handleKeyPress.bind(this));
|
|
domEl.addEventListener('focusin', this.updateLastFocused.bind(this));
|
|
}
|
|
|
|
this._creatorCount++;
|
|
|
|
if (!this.editable) {
|
|
toggleButton.hidden = true;
|
|
removeButton.hidden = true;
|
|
addButton.hidden = true;
|
|
}
|
|
|
|
this.addDynamicRow(th, td, true);
|
|
|
|
// Set single/double field toggle mode
|
|
if (fieldMode) {
|
|
this.switchCreatorMode(td.parentNode, 1, true);
|
|
}
|
|
else {
|
|
this.switchCreatorMode(td.parentNode, 0, true);
|
|
}
|
|
|
|
// Focus new rows
|
|
if (unsaved && !defaultRow){
|
|
lastNameElem.click();
|
|
}
|
|
}
|
|
|
|
addMoreCreatorsRow(num) {
|
|
var th = document.createElement('th');
|
|
|
|
var td = document.createElement('td');
|
|
td.id = 'more-creators-label';
|
|
td.setAttribute('onclick',
|
|
"var binding = this.closest('item-box'); "
|
|
+ "binding._displayAllCreators = true; "
|
|
+ "binding.refresh()"
|
|
);
|
|
td.textContent = Zotero.getString('general.numMore', num);
|
|
|
|
this.addDynamicRow(th, td, true);
|
|
}
|
|
|
|
addDateRow(field, value, tabindex) {
|
|
var th = document.createElement("th");
|
|
th.setAttribute("fieldname", field);
|
|
th.setAttribute("onclick", "this.nextSibling.firstChild.blur()");
|
|
var label = document.createElement('label');
|
|
label.className = 'key';
|
|
label.textContent = Zotero.ItemFields.getLocalizedString(field);
|
|
th.appendChild(label);
|
|
|
|
var td = document.createElement('td');
|
|
td.className = "date-box";
|
|
|
|
var elem = this.createValueElement(
|
|
Zotero.Date.multipartToStr(value),
|
|
field,
|
|
tabindex
|
|
);
|
|
|
|
// y-m-d status indicator
|
|
var ymd = document.createElement('span');
|
|
ymd.id = 'zotero-date-field-status';
|
|
ymd.textContent = Zotero.Date.strToDate(Zotero.Date.multipartToStr(value))
|
|
.order.split('').join(' ');
|
|
|
|
td.appendChild(elem);
|
|
td.appendChild(ymd);
|
|
|
|
this.addDynamicRow(th, td);
|
|
}
|
|
|
|
switchCreatorMode(row, fieldMode, initial, updatePref) {
|
|
// Change if button position changes
|
|
var button = row.lastChild.lastChild.previousSibling.previousSibling;
|
|
var creatorNameBox = button.previousSibling;
|
|
var lastName = creatorNameBox.firstChild;
|
|
var comma = creatorNameBox.firstChild.nextSibling;
|
|
var firstName = creatorNameBox.lastChild;
|
|
|
|
// Switch to single-field mode
|
|
if (fieldMode == 1) {
|
|
button.style.background = `url("chrome://zotero/skin/textfield-dual${Zotero.hiDPISuffix}.png") center/21px auto no-repeat`;
|
|
button.setAttribute('title', Zotero.getString('pane.item.switchFieldMode.two'));
|
|
lastName.setAttribute('fieldMode', '1');
|
|
button.setAttribute('onclick', "this.closest('item-box').switchCreatorMode(this.closest('tr'), 0, false, true)");
|
|
delete lastName.style.width;
|
|
delete lastName.style.maxWidth;
|
|
|
|
// Remove firstname field from tabindex
|
|
var tab = parseInt(firstName.getAttribute('ztabindex'));
|
|
firstName.setAttribute('ztabindex', -1);
|
|
if (this._tabIndexMaxCreators == tab) {
|
|
this._tabIndexMaxCreators--;
|
|
}
|
|
|
|
// Hide first name field and prepend to last name field
|
|
firstName.hidden = true;
|
|
comma.hidden = true;
|
|
|
|
if (!initial) {
|
|
var first = this._getFieldValue(firstName);
|
|
if (first && first != this._defaultFirstName) {
|
|
var last = this._getFieldValue(lastName);
|
|
this._setFieldValue(lastName, first + ' ' + last);
|
|
}
|
|
}
|
|
|
|
if (this._getFieldValue(lastName) == this._defaultLastName) {
|
|
this._setFieldValue(lastName, this._defaultFullName);
|
|
}
|
|
|
|
// If one of the creator fields is open, leave it open after swap
|
|
let activeField = this._infoTable.querySelector('input');
|
|
if (activeField == firstName || activeField == lastName) {
|
|
this._lastTabIndex = parseInt(lastName.getAttribute('ztabindex'));
|
|
this._tabDirection = false;
|
|
}
|
|
}
|
|
// Switch to two-field mode
|
|
else {
|
|
button.style.background = `url("chrome://zotero/skin/textfield-single${Zotero.hiDPISuffix}.png") center/21px auto no-repeat`;
|
|
button.setAttribute('title', Zotero.getString('pane.item.switchFieldMode.one'));
|
|
lastName.setAttribute('fieldMode', '0');
|
|
button.setAttribute('onclick', "this.closest('item-box').switchCreatorMode(this.closest('tr'), 1, false, true)");
|
|
|
|
// appropriately truncate lastName
|
|
|
|
// get item box width
|
|
var computedStyle = window.getComputedStyle(this, null);
|
|
var boxWidth = computedStyle.getPropertyValue('width');
|
|
// get field label width
|
|
var computedStyle = window.getComputedStyle(row.firstChild, null);
|
|
var leftHboxWidth = computedStyle.getPropertyValue('width');
|
|
// get last name width
|
|
computedStyle = window.getComputedStyle(lastName, null);
|
|
var lastNameWidth = computedStyle.getPropertyValue('width');
|
|
if(boxWidth.substr(-2) === 'px'
|
|
&& leftHboxWidth.substr(-2) === 'px'
|
|
&& lastNameWidth.substr(-2) === "px") {
|
|
// compute a maximum width
|
|
boxWidth = parseInt(boxWidth);
|
|
leftHboxWidth = parseInt(leftHboxWidth);
|
|
lastNameWidth = parseInt(lastNameWidth);
|
|
var maxWidth = boxWidth-leftHboxWidth-140;
|
|
if(lastNameWidth > maxWidth) {
|
|
//lastName.style.width = maxWidth+"px";
|
|
//lastName.style.maxWidth = maxWidth+"px";
|
|
} else {
|
|
delete lastName.style.width;
|
|
delete lastName.style.maxWidth;
|
|
}
|
|
}
|
|
|
|
// Add firstname field to tabindex
|
|
var tab = parseInt(lastName.getAttribute('ztabindex'));
|
|
firstName.setAttribute('ztabindex', tab + 1);
|
|
if (this._tabIndexMaxCreators == tab)
|
|
{
|
|
this._tabIndexMaxCreators++;
|
|
}
|
|
|
|
if (!initial) {
|
|
// Move all but last word to first name field and show it
|
|
var last = this._getFieldValue(lastName);
|
|
if (last && last != this._defaultFullName) {
|
|
var lastNameRE = /(.*?)[ ]*([^ ]+[ ]*)$/;
|
|
var parts = lastNameRE.exec(last);
|
|
if (parts[2] && parts[2] != last)
|
|
{
|
|
this._setFieldValue(lastName, parts[2]);
|
|
this._setFieldValue(firstName, parts[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!this._getFieldValue(firstName)) {
|
|
this._setFieldValue(firstName, this._defaultFirstName);
|
|
}
|
|
|
|
if (this._getFieldValue(lastName) == this._defaultFullName) {
|
|
this._setFieldValue(lastName, this._defaultLastName);
|
|
}
|
|
|
|
firstName.hidden = false;
|
|
comma.hidden = false;
|
|
}
|
|
|
|
// Save the last-used field mode
|
|
if (updatePref) {
|
|
Zotero.debug("Switching lastCreatorFieldMode to " + fieldMode);
|
|
Zotero.Prefs.set('lastCreatorFieldMode', fieldMode);
|
|
}
|
|
|
|
if (!initial) {
|
|
var index = button.getAttribute('fieldname').split('-')[1];
|
|
var fields = this.getCreatorFields(row);
|
|
fields.fieldMode = fieldMode;
|
|
this.modifyCreator(index, fields);
|
|
if (this.saveOnEdit) {
|
|
let activeField = this._infoTable.querySelector('input, textarea');
|
|
if (activeField !== null && activeField !== firstName && activeField !== lastName) {
|
|
this.blurOpenField();
|
|
}
|
|
else {
|
|
this.item.saveTx();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
scrollToTop() {
|
|
this.scrollTop = 0;
|
|
}
|
|
|
|
ensureElementIsVisible(elem) {
|
|
elem.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
async changeTypeTo(itemTypeID, menu) {
|
|
var functionsToRun = [];
|
|
if (this.eventHandlers.itemtypechange && this.eventHandlers.itemtypechange.length) {
|
|
functionsToRun = [...this.eventHandlers.itemtypechange];
|
|
}
|
|
|
|
if (itemTypeID == this.item.itemTypeID) {
|
|
return true;
|
|
}
|
|
|
|
if (this.saveOnEdit) {
|
|
await this.blurOpenField();
|
|
await this.item.saveTx();
|
|
}
|
|
|
|
var fieldsToDelete = this.item.getFieldsNotInType(itemTypeID, true);
|
|
|
|
// Special cases handled below
|
|
var bookTypeID = Zotero.ItemTypes.getID('book');
|
|
var bookSectionTypeID = Zotero.ItemTypes.getID('bookSection');
|
|
|
|
// Add warning for shortTitle when moving from book to bookSection
|
|
// when title will be transferred
|
|
if (this.item.itemTypeID == bookTypeID && itemTypeID == bookSectionTypeID) {
|
|
var titleFieldID = Zotero.ItemFields.getID('title');
|
|
var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
|
|
if (this.item.getField(titleFieldID) && this.item.getField(shortTitleFieldID)) {
|
|
if (!fieldsToDelete) {
|
|
fieldsToDelete = [];
|
|
}
|
|
fieldsToDelete.push(shortTitleFieldID);
|
|
}
|
|
}
|
|
|
|
// Generate list of localized field names for display in pop-up
|
|
if (fieldsToDelete) {
|
|
// Ignore warning for bookTitle when going from bookSection to book
|
|
// if there's not also a title, since the book title is transferred
|
|
// to title automatically in Zotero.Item.setType()
|
|
if (this.item.itemTypeID == bookSectionTypeID && itemTypeID == bookTypeID) {
|
|
var titleFieldID = Zotero.ItemFields.getID('title');
|
|
var bookTitleFieldID = Zotero.ItemFields.getID('bookTitle');
|
|
var shortTitleFieldID = Zotero.ItemFields.getID('shortTitle');
|
|
if (this.item.getField(bookTitleFieldID) && !this.item.getField(titleFieldID)) {
|
|
var index = fieldsToDelete.indexOf(bookTitleFieldID);
|
|
fieldsToDelete.splice(index, 1);
|
|
// But warn for short title, which will be removed
|
|
if (this.item.getField(shortTitleFieldID)) {
|
|
fieldsToDelete.push(shortTitleFieldID);
|
|
}
|
|
}
|
|
}
|
|
|
|
var fieldNames = "";
|
|
for (var i=0; i<fieldsToDelete.length; i++) {
|
|
fieldNames += "\n - " +
|
|
Zotero.ItemFields.getLocalizedString(fieldsToDelete[i]);
|
|
}
|
|
|
|
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
|
.getService(Components.interfaces.nsIPromptService);
|
|
}
|
|
|
|
if (!fieldsToDelete || fieldsToDelete.length == 0 ||
|
|
promptService.confirm(null,
|
|
Zotero.getString('pane.item.changeType.title'),
|
|
Zotero.getString('pane.item.changeType.text') + "\n" + fieldNames)) {
|
|
this.item.setType(itemTypeID);
|
|
|
|
if (this.saveOnEdit) {
|
|
// See note in transformText()
|
|
await this.blurOpenField();
|
|
await this.item.saveTx();
|
|
}
|
|
else {
|
|
this.refresh();
|
|
}
|
|
|
|
functionsToRun.forEach(f => f.bind(this)());
|
|
|
|
return true;
|
|
}
|
|
|
|
// Revert the menu (which changes before the pop-up)
|
|
if (menu) {
|
|
menu.value = this.item.itemTypeID;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
toggleAbstractExpand(label, valueElement) {
|
|
var cur = Zotero.Prefs.get('lastAbstractExpand');
|
|
Zotero.Prefs.set('lastAbstractExpand', !cur);
|
|
|
|
var valueText = this.item.getField('abstractNote');
|
|
var tabindex = valueElement.getAttribute('ztabindex');
|
|
var newValueElement = this.createValueElement(
|
|
valueText,
|
|
'abstractNote',
|
|
tabindex
|
|
);
|
|
valueElement.replaceWith(newValueElement);
|
|
|
|
var text = Zotero.ItemFields.getLocalizedString('abstractNote');
|
|
// Add '(...)' before "Abstract" for collapsed abstracts
|
|
if (valueText && cur) {
|
|
text = '(\u2026) ' + text;
|
|
}
|
|
label.textContent = text;
|
|
}
|
|
|
|
disableButton(button) {
|
|
button.setAttribute('disabled', true);
|
|
button.setAttribute('onclick', false);
|
|
}
|
|
|
|
_enablePlusButton(button, creatorTypeID, fieldMode) {
|
|
button.removeAttribute('disabled');
|
|
button.onclick = () => {
|
|
this.disableButton(button);
|
|
this.addCreatorRow(null, creatorTypeID, true);
|
|
};
|
|
}
|
|
|
|
disableCreatorAddButtons() {
|
|
// Disable the "+" button on all creator rows
|
|
var elems = this._infoTable.getElementsByClassName('zotero-clicky-plus');
|
|
for (let elem of elems) {
|
|
this.disableButton(elem);
|
|
}
|
|
}
|
|
|
|
createValueElement(valueText, fieldName, tabindex) {
|
|
valueText = valueText + '';
|
|
|
|
if (fieldName) {
|
|
var fieldID = Zotero.ItemFields.getID(fieldName);
|
|
}
|
|
|
|
// Allow multiline/long fields to wrap
|
|
var isMultiline = Zotero.ItemFields.isMultiline(fieldName) || Zotero.ItemFields.isLong(fieldName);
|
|
// But treat Abstract as a multiline field only when expanded
|
|
if (fieldName == 'abstractNote') {
|
|
isMultiline &&= Zotero.Prefs.get('lastAbstractExpand');
|
|
}
|
|
|
|
var valueElement = document.createElement("div");
|
|
|
|
valueElement.setAttribute('id', `itembox-field-value-${fieldName}`);
|
|
valueElement.className = 'value';
|
|
valueElement.setAttribute('fieldname', fieldName);
|
|
|
|
if (this._fieldIsClickable(fieldName)) {
|
|
valueElement.setAttribute('ztabindex', tabindex);
|
|
valueElement.addEventListener('click', (event) => {
|
|
/* Skip right-click on Windows */
|
|
if (event.button) {
|
|
return;
|
|
}
|
|
this.clickHandler(event.target);
|
|
}, false);
|
|
valueElement.classList.add('zotero-clicky');
|
|
}
|
|
|
|
switch (fieldName) {
|
|
case 'itemType':
|
|
valueElement.setAttribute('itemTypeID', valueText);
|
|
valueText = Zotero.ItemTypes.getLocalizedString(valueText);
|
|
break;
|
|
|
|
// Convert dates from UTC
|
|
case 'dateAdded':
|
|
case 'dateModified':
|
|
case 'accessDate':
|
|
case 'date':
|
|
|
|
// TEMP - NSF
|
|
case 'dateSent':
|
|
case 'dateDue':
|
|
case 'accepted':
|
|
if (fieldName == 'date' && this.item._objectType != 'feedItem') {
|
|
break;
|
|
}
|
|
if (valueText) {
|
|
var date = Zotero.Date.sqlToDate(valueText, true);
|
|
if (date) {
|
|
// If no time, interpret as local, not UTC
|
|
if (Zotero.Date.isSQLDate(valueText)) {
|
|
// Add time to avoid showing previous day if date is in
|
|
// DST (including the current date at 00:00:00) and we're
|
|
// in standard time
|
|
date = Zotero.Date.sqlToDate(valueText + ' 12:00:00');
|
|
valueText = date.toLocaleDateString();
|
|
}
|
|
else {
|
|
valueText = date.toLocaleString();
|
|
}
|
|
}
|
|
else {
|
|
valueText = '';
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (fieldID) {
|
|
// Display the SQL date as a tooltip for date fields
|
|
// TEMP - filingDate
|
|
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') || fieldName == 'filingDate') {
|
|
valueElement.setAttribute('title',
|
|
Zotero.Date.multipartToSQL(this.item.getField(fieldName, true)));
|
|
}
|
|
|
|
// Display a context menu for certain fields
|
|
if (this.editable && (fieldName == 'seriesTitle' || fieldName == 'shortTitle' ||
|
|
Zotero.ItemFields.isFieldOfBase(fieldID, 'title') ||
|
|
Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) {
|
|
valueElement.setAttribute('context', 'zotero-field-transform-menu');
|
|
valueElement.oncontextmenu = (event) => {
|
|
document.popupNode = valueElement;
|
|
this._id('zotero-field-transform-menu').openPopupAtScreen(
|
|
event.screenX + 1,
|
|
event.screenY + 1,
|
|
true
|
|
);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Add popup menu on DOI field with value
|
|
if (fieldName == 'DOI' && valueText) {
|
|
valueElement.oncontextmenu = (event) => {
|
|
this._id('zotero-doi-menu').openPopupAtScreen(
|
|
event.screenX + 1,
|
|
event.screenY + 1,
|
|
true
|
|
);
|
|
};
|
|
}
|
|
|
|
valueElement.textContent = valueText;
|
|
|
|
// Attempt to make bidi things work automatically:
|
|
// If we have text to work off of, let the layout engine try to guess the text direction
|
|
if (valueText) {
|
|
valueElement.dir = 'auto';
|
|
}
|
|
// If not, assume it follows the locale's direction
|
|
else {
|
|
valueElement.dir = Zotero.dir;
|
|
}
|
|
|
|
// Regardless, align the text in the label consistently, following the locale's direction
|
|
if (Zotero.rtl) {
|
|
valueElement.style.textAlign = 'right';
|
|
}
|
|
else {
|
|
valueElement.style.textAlign = 'left';
|
|
}
|
|
|
|
if (isMultiline) {
|
|
valueElement.classList.add('multiline');
|
|
}
|
|
|
|
// Allow toggling non-editable Abstract open and closed with click
|
|
if (fieldName == 'abstractNote' && !this.editable) {
|
|
valueElement.classList.add("pointer");
|
|
valueElement.addEventListener('click', () => {
|
|
let label = valueElement.parentElement.previousElementSibling.firstElementChild;
|
|
this.toggleAbstractExpand(label, valueElement);
|
|
});
|
|
}
|
|
|
|
return valueElement;
|
|
}
|
|
|
|
async removeCreator(index, labelToDelete) {
|
|
// If unsaved row, just remove element
|
|
if (!this.item.hasCreatorAt(index)) {
|
|
labelToDelete.parentNode.removeChild(labelToDelete);
|
|
|
|
// Enable the "+" button on the previous row
|
|
var elems = this._infoTable.getElementsByClassName('zotero-clicky-plus');
|
|
var button = elems[elems.length-1];
|
|
var creatorFields = this.getCreatorFields(button.closest('tr'));
|
|
this._enablePlusButton(button, creatorFields.creatorTypeID, creatorFields.fieldMode);
|
|
|
|
this._creatorCount--;
|
|
return;
|
|
}
|
|
await this.blurOpenField();
|
|
this.item.removeCreator(index);
|
|
await this.item.saveTx();
|
|
}
|
|
|
|
async showEditor(elem) {
|
|
Zotero.debug(`Showing editor for ${elem.getAttribute('fieldname')}`);
|
|
|
|
var label = elem.closest('tr').querySelector('th > label');
|
|
var lastTabIndex = this._lastTabIndex = parseInt(elem.getAttribute('ztabindex'));
|
|
|
|
// If a field is open, hide it before selecting the new field, which might
|
|
// trigger a refresh
|
|
var activeField = this._infoTable.querySelector('input, textarea');
|
|
if (activeField) {
|
|
this._refreshed = false;
|
|
await this.blurOpenField();
|
|
this._lastTabIndex = lastTabIndex;
|
|
// If the box was refreshed, the clicked element is no longer valid,
|
|
// so just focus by tab index
|
|
if (this._refreshed) {
|
|
this._focusNextField(this._lastTabIndex);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var fieldName = elem.getAttribute('fieldname');
|
|
var tabindex = elem.getAttribute('ztabindex');
|
|
|
|
var [field, creatorIndex, creatorField] = fieldName.split('-');
|
|
if (field == 'creator') {
|
|
var value = this.item.getCreator(creatorIndex)[creatorField];
|
|
if (value === undefined) {
|
|
value = "";
|
|
}
|
|
var itemID = this.item.id;
|
|
}
|
|
else {
|
|
var value = this.item.getField(fieldName);
|
|
var itemID = this.item.id;
|
|
|
|
// Access date needs to be converted from UTC
|
|
if (value != '') {
|
|
switch (fieldName) {
|
|
case 'accessDate':
|
|
|
|
// TEMP - NSF
|
|
case 'dateSent':
|
|
case 'dateDue':
|
|
case 'accepted':
|
|
// If no time, interpret as local, not UTC
|
|
if (Zotero.Date.isSQLDate(value)) {
|
|
var localDate = Zotero.Date.sqlToDate(value);
|
|
}
|
|
else {
|
|
var localDate = Zotero.Date.sqlToDate(value, true);
|
|
}
|
|
var value = Zotero.Date.dateToSQL(localDate);
|
|
|
|
// Don't show time in editor
|
|
value = value.replace(' 00:00:00', '');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var t;
|
|
if (Zotero.ItemFields.isMultiline(fieldName) || Zotero.ItemFields.isLong(fieldName)) {
|
|
t = document.createElement("textarea");
|
|
}
|
|
// Add auto-complete for certain fields
|
|
else if (field == 'creator' || Zotero.ItemFields.isAutocompleteField(fieldName)) {
|
|
t = document.createElement("input", { is: 'shadow-autocomplete-input' });
|
|
t.setAttribute('autocompletesearch', 'zotero');
|
|
|
|
let params = {
|
|
fieldName: fieldName,
|
|
libraryID: this.item.libraryID
|
|
};
|
|
if (field == 'creator') {
|
|
params.fieldMode = parseInt(elem.getAttribute('fieldMode'));
|
|
|
|
// Include itemID and creatorTypeID so the autocomplete can
|
|
// avoid showing results for creators already set on the item
|
|
let row = elem.closest('tr');
|
|
let creatorTypeID = parseInt(
|
|
row.getElementsByClassName('creator-type-label')[0]
|
|
.getAttribute('typeid')
|
|
);
|
|
if (itemID) {
|
|
params.itemID = itemID;
|
|
params.creatorTypeID = creatorTypeID;
|
|
}
|
|
|
|
// Return/click
|
|
// Monkey-patching onTextEntered is apparently the current official way to detect completion --
|
|
// there's also a custom event called textEntered, but it won't be fired unless the input has its
|
|
// 'notifylegacyevents' attribute set to true
|
|
// https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/toolkit/content/widgets/autocomplete-input.js#372
|
|
// https://searchfox.org/mozilla-central/rev/2d678a843ceab81e43f7ffb83212197dc10e944a/browser/components/search/content/searchbar.js#791
|
|
t.onTextEntered = () => {
|
|
this.handleCreatorAutoCompleteSelect(t, true);
|
|
};
|
|
// Tab/Shift-Tab
|
|
t.addEventListener('change', () => {
|
|
this.handleCreatorAutoCompleteSelect(t, true);
|
|
});
|
|
|
|
if (creatorField == 'lastName') {
|
|
t.setAttribute('fieldMode', elem.getAttribute('fieldMode'));
|
|
t.addEventListener('paste', (event) => {
|
|
let lastName = event.clipboardData.getData('text').trim();
|
|
// Handle \n\r and \n delimited entries and a single line containing a tab
|
|
var rawNameArray = lastName.split(/\r\n?|\n/);
|
|
if (rawNameArray.length > 1 || rawNameArray[0].includes('\t')) {
|
|
// Pasting multiple authors; first make sure we prevent normal paste behavior
|
|
event.preventDefault();
|
|
|
|
// Save tab direction and add creator flags since they are reset in the
|
|
// process of adding multiple authors
|
|
var tabDirectionBuffer = this._tabDirection;
|
|
var addCreatorRowBuffer = this._addCreatorRow;
|
|
var tabIndexBuffer = this._lastTabIndex;
|
|
this._tabDirection = false;
|
|
this._addCreatorRow = false;
|
|
|
|
// Filter out bad names
|
|
var nameArray = rawNameArray.filter(name => name);
|
|
|
|
// If not adding names at the end of the creator list, make new creator
|
|
// entries and then shift down existing creators.
|
|
var initNumCreators = this.item.numCreators();
|
|
var creatorsToShift = initNumCreators - creatorIndex;
|
|
if (creatorsToShift > 0) {
|
|
// Add extra creators with dummy values
|
|
for (let i = 0; i < nameArray.length; i++) {
|
|
this.modifyCreator(i + initNumCreators, {
|
|
firstName: '',
|
|
lastName: '',
|
|
fieldMode: 0,
|
|
creatorTypeID
|
|
});
|
|
}
|
|
|
|
// Shift existing creators
|
|
for (let i = initNumCreators - 1; i >= creatorIndex; i--) {
|
|
let shiftedCreatorData = this.item.getCreator(i);
|
|
this.item.setCreator(nameArray.length + i, shiftedCreatorData);
|
|
}
|
|
}
|
|
|
|
let currentIndex = creatorIndex;
|
|
let newCreator = { creatorTypeID };
|
|
// Add the creators in lastNameArray one at a time
|
|
for (let tempName of nameArray) {
|
|
// Check for tab to determine creator name format
|
|
newCreator.fieldMode = (tempName.indexOf('\t') == -1) ? 1 : 0;
|
|
if (newCreator.fieldMode == 0) {
|
|
newCreator.lastName = tempName.split('\t')[0];
|
|
newCreator.firstName = tempName.split('\t')[1];
|
|
}
|
|
else {
|
|
newCreator.lastName = tempName;
|
|
newCreator.firstName = '';
|
|
}
|
|
this.modifyCreator(currentIndex, newCreator);
|
|
currentIndex++;
|
|
}
|
|
this._tabDirection = tabDirectionBuffer;
|
|
this._addCreatorRow = (creatorsToShift == 0) ? addCreatorRowBuffer : false;
|
|
if (this._tabDirection == 1) {
|
|
this._lastTabIndex = tabIndexBuffer + 2 * (nameArray.length - 1);
|
|
if (newCreator.fieldMode == 0) {
|
|
this._lastTabIndex++;
|
|
}
|
|
}
|
|
|
|
if (this.saveOnEdit) {
|
|
this.item.saveTx();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
t.setAttribute(
|
|
'autocompletesearchparam', JSON.stringify(params)
|
|
);
|
|
t.setAttribute('completeselectedindex', true);
|
|
}
|
|
|
|
if (!t) {
|
|
t = document.createElement("input");
|
|
}
|
|
|
|
t.id = `itembox-field-textbox-${fieldName}`;
|
|
t.value = value;
|
|
t.dataset.originalValue = value;
|
|
t.style.mozBoxFlex = 1;
|
|
t.setAttribute('fieldname', fieldName);
|
|
t.setAttribute('ztabindex', tabindex);
|
|
// We set dir in createValueElement(), so figure out what it was computed as
|
|
// and then propagate to the new text field
|
|
t.dir = getComputedStyle(elem).direction;
|
|
|
|
var box = elem.parentNode;
|
|
box.replaceChild(t, elem);
|
|
|
|
// Associate textbox with label
|
|
label.setAttribute('control', t.getAttribute('id'));
|
|
|
|
// Prevent error when clicking between a changed field
|
|
// and another -- there's probably a better way
|
|
if (!t.select) {
|
|
return;
|
|
}
|
|
|
|
t.select();
|
|
|
|
// Leave text field open when window loses focus
|
|
var ignoreBlur = () => {
|
|
this.ignoreBlur = true;
|
|
};
|
|
var unignoreBlur = () => {
|
|
this.ignoreBlur = false;
|
|
};
|
|
addEventListener("deactivate", ignoreBlur);
|
|
addEventListener("activate", unignoreBlur);
|
|
|
|
t.addEventListener('blur', () => {
|
|
if (this.ignoreBlur) return;
|
|
|
|
removeEventListener("deactivate", ignoreBlur);
|
|
removeEventListener("activate", unignoreBlur);
|
|
this.blurHandler(t);
|
|
});
|
|
t.addEventListener('keypress', event => this.handleKeyPress(event));
|
|
|
|
if (t instanceof HTMLTextAreaElement) {
|
|
let updateHeight = () => {
|
|
// Reset height before getting scrollHeight
|
|
// Prevents field from growing slightly each time
|
|
// https://stackoverflow.com/a/58073583
|
|
t.style.height = 'auto';
|
|
t.style.height = `calc(max(6em, ${t.scrollHeight}px))`;
|
|
};
|
|
t.addEventListener('input', updateHeight);
|
|
updateHeight();
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
|
|
/**
|
|
* Save a multiple-field selection for the creator autocomplete
|
|
* (e.g. "Shakespeare, William")
|
|
*/
|
|
handleCreatorAutoCompleteSelect(textbox, stayFocused) {
|
|
var controller = textbox.controller;
|
|
if (!controller.matchCount) return;
|
|
|
|
var id = false;
|
|
for (let i = 0; i < controller.matchCount; i++) {
|
|
if (controller.getCommentAt(i) == textbox.value) {
|
|
id = controller.getLabelAt(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No result selected
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
var [creatorID, numFields] = id.split('-');
|
|
|
|
// If result uses two fields, save both
|
|
if (numFields==2)
|
|
{
|
|
// Manually clear autocomplete controller's reference to
|
|
// textbox to prevent error next time around
|
|
textbox.mController.input = null;
|
|
|
|
var [field, creatorIndex, creatorField] =
|
|
textbox.getAttribute('fieldname').split('-');
|
|
|
|
if (stayFocused) {
|
|
this._lastTabIndex = parseInt(textbox.getAttribute('ztabindex'));
|
|
this._tabDirection = false;
|
|
}
|
|
|
|
var creator = Zotero.Creators.get(creatorID);
|
|
|
|
var otherField = creatorField == 'lastName' ? 'firstName' : 'lastName';
|
|
|
|
// Update this textbox
|
|
textbox.setAttribute('value', creator[creatorField]);
|
|
textbox.value = creator[creatorField];
|
|
|
|
// Update the other label
|
|
if (otherField=='firstName'){
|
|
var label = textbox.nextSibling.nextSibling;
|
|
}
|
|
else if (otherField=='lastName'){
|
|
var label = textbox.previousSibling.previousSibling;
|
|
}
|
|
|
|
//this._setFieldValue(label, creator[otherField]);
|
|
if (label.firstChild){
|
|
label.firstChild.nodeValue = creator[otherField];
|
|
}
|
|
else {
|
|
label.value = creator[otherField];
|
|
}
|
|
|
|
var row = textbox.closest('tr');
|
|
|
|
var fields = this.getCreatorFields(row);
|
|
fields[creatorField] = creator[creatorField];
|
|
fields[otherField] = creator[otherField];
|
|
|
|
this.modifyCreator(creatorIndex, fields);
|
|
if (this.saveOnEdit) {
|
|
this.ignoreBlur = true;
|
|
this.item.saveTx().then(() => {
|
|
this.ignoreBlur = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Otherwise let the autocomplete popup handle matters
|
|
}
|
|
|
|
handleKeyPress(event) {
|
|
var target = event.target;
|
|
var focused = document.commandDispatcher.focusedElement;
|
|
|
|
if ((event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === ' ')
|
|
&& target.classList.contains('creator-type-label')) {
|
|
event.preventDefault();
|
|
target.click();
|
|
|
|
setTimeout(() => {
|
|
this._creatorTypeMenu.dispatchEvent(
|
|
new KeyboardEvent("keydown", { key: 'ArrowDown', keyCode: 40, charCode: 0 })
|
|
);
|
|
}, 0);
|
|
return;
|
|
}
|
|
|
|
switch (event.keyCode)
|
|
{
|
|
case event.DOM_VK_RETURN:
|
|
var fieldname = target.getAttribute('fieldname');
|
|
// Use shift-enter as the save action for the larger fields
|
|
if (Zotero.ItemFields.isMultiline(fieldname) && !event.shiftKey) {
|
|
return;
|
|
}
|
|
|
|
if (target.classList.contains('zotero-focusable')) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
// Prevent blur on containing textbox
|
|
// DEBUG: what happens if this isn't present?
|
|
event.preventDefault();
|
|
|
|
// Shift-enter adds new creator row
|
|
if (fieldname.indexOf('creator-') == 0 && event.shiftKey) {
|
|
// Value hasn't changed
|
|
if (target.dataset.originalValue == target.value) {
|
|
Zotero.debug("Value hasn't changed");
|
|
// If + button is disabled, just focus next creator row
|
|
if (target.closest('tr').lastChild.lastChild.disabled) {
|
|
this._focusNextField(this._lastTabIndex);
|
|
}
|
|
else {
|
|
var creatorFields = this.getCreatorFields(target.closest('tr'));
|
|
this.addCreatorRow(false, creatorFields.creatorTypeID, true);
|
|
}
|
|
}
|
|
// Value has changed
|
|
else {
|
|
this._tabDirection = 1;
|
|
this._addCreatorRow = true;
|
|
focused.blur();
|
|
}
|
|
return;
|
|
}
|
|
focused.blur();
|
|
|
|
// Return focus to items pane
|
|
var tree = document.getElementById('zotero-items-tree');
|
|
if (tree) {
|
|
tree.focus();
|
|
}
|
|
|
|
return;
|
|
|
|
case event.DOM_VK_ESCAPE:
|
|
// Reset field to original value
|
|
target.value = target.dataset.originalValue;
|
|
|
|
focused.blur();
|
|
|
|
// Return focus to items pane
|
|
var tree = document.getElementById('zotero-items-tree');
|
|
if (tree) {
|
|
tree.focus();
|
|
}
|
|
|
|
return;
|
|
|
|
case event.DOM_VK_TAB:
|
|
event.preventDefault();
|
|
if (event.shiftKey) {
|
|
this._focusNextField(this._lastTabIndex, true);
|
|
}
|
|
else {
|
|
// If on the last field, allow default tab action
|
|
if (this._lastTabIndex == this._tabIndexMaxFields) {
|
|
return;
|
|
}
|
|
this._focusNextField(++this._lastTabIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
itemTypeMenuTab(event) {
|
|
if (!event.shiftKey) {
|
|
this.focusFirstField();
|
|
event.preventDefault();
|
|
}
|
|
// Shift-tab
|
|
else {
|
|
this._tabDirection = false;
|
|
}
|
|
}
|
|
|
|
async hideEditor(textbox) {
|
|
// Handle cases where creator autocomplete doesn't trigger
|
|
// the textentered and change events handled in showEditor
|
|
if (textbox.getAttribute('fieldname').startsWith('creator-')) {
|
|
this.handleCreatorAutoCompleteSelect(textbox);
|
|
}
|
|
|
|
Zotero.debug(`Hiding editor for ${textbox.getAttribute('fieldname')}`);
|
|
|
|
var label = textbox.closest('tr').querySelector('th > label');
|
|
this._lastTabIndex = -1;
|
|
|
|
// Prevent autocomplete breakage in Firefox 3
|
|
if (textbox.mController) {
|
|
textbox.mController.input = null;
|
|
}
|
|
|
|
var fieldName = textbox.getAttribute('fieldname');
|
|
var tabindex = textbox.getAttribute('ztabindex');
|
|
|
|
//var value = t.value;
|
|
var value = textbox.value.trim();
|
|
|
|
var elem;
|
|
var [field, creatorIndex, creatorField] = fieldName.split('-');
|
|
var newVal;
|
|
|
|
// Creator fields
|
|
if (field == 'creator') {
|
|
var row = textbox.closest('tr');
|
|
|
|
var otherFields = this.getCreatorFields(row);
|
|
otherFields[creatorField] = value;
|
|
this.modifyCreator(creatorIndex, otherFields);
|
|
|
|
var val = this.item.getCreator(creatorIndex);
|
|
val = val ? val[creatorField] : null;
|
|
|
|
if (!val) {
|
|
// Reset to '(first)'/'(last)'/'(name)'
|
|
if (creatorField == 'lastName') {
|
|
val = otherFields.fieldMode
|
|
? this._defaultFullName : this._defaultLastName;
|
|
}
|
|
else if (creatorField == 'firstName') {
|
|
val = this._defaultFirstName;
|
|
}
|
|
}
|
|
|
|
newVal = val;
|
|
|
|
if (Zotero.ItemTypes.getName(this.item.itemTypeID) === "bookSection") {
|
|
this._showCreatorTypeGuidance = true;
|
|
}
|
|
}
|
|
|
|
// Fields
|
|
else {
|
|
// Access date needs to be parsed and converted to UTC SQL date
|
|
if (value != '') {
|
|
switch (fieldName) {
|
|
case 'accessDate':
|
|
// Parse 'yesterday'/'today'/'tomorrow'
|
|
value = Zotero.Date.parseDescriptiveString(value);
|
|
|
|
// Allow "now" to use current time
|
|
if (value == 'now') {
|
|
value = Zotero.Date.dateToSQL(new Date(), true);
|
|
}
|
|
// If just date, don't convert to UTC
|
|
else if (Zotero.Date.isSQLDate(value)) {
|
|
var localDate = Zotero.Date.sqlToDate(value);
|
|
value = Zotero.Date.dateToSQL(localDate).replace(' 00:00:00', '');
|
|
}
|
|
else if (Zotero.Date.isSQLDateTime(value)) {
|
|
var localDate = Zotero.Date.sqlToDate(value);
|
|
value = Zotero.Date.dateToSQL(localDate, true);
|
|
}
|
|
else {
|
|
var d = Zotero.Date.strToDate(value);
|
|
value = null;
|
|
if (d.year && d.month != undefined && d.day) {
|
|
d = new Date(d.year, d.month, d.day);
|
|
value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', '');
|
|
}
|
|
}
|
|
break;
|
|
|
|
// TEMP - NSF
|
|
case 'dateSent':
|
|
case 'dateDue':
|
|
case 'accepted':
|
|
if (Zotero.Date.isSQLDate(value)) {
|
|
var localDate = Zotero.Date.sqlToDate(value);
|
|
value = Zotero.Date.dateToSQL(localDate).replace(' 00:00:00', '');
|
|
}
|
|
else {
|
|
var d = Zotero.Date.strToDate(value);
|
|
value = null;
|
|
if (d.year && d.month != undefined && d.day) {
|
|
d = new Date(d.year, d.month, d.day);
|
|
value = Zotero.Date.dateToSQL(d).replace(' 00:00:00', '');
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// TODO: generalize to all date rows/fields
|
|
if (Zotero.ItemFields.isFieldOfBase(fieldName, 'date')) {
|
|
// Parse 'yesterday'/'today'/'tomorrow'
|
|
value = Zotero.Date.parseDescriptiveString(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._modifyField(fieldName, value);
|
|
newVal = this.item.getField(fieldName);
|
|
}
|
|
|
|
// Close box
|
|
elem = this.createValueElement(
|
|
newVal,
|
|
fieldName,
|
|
tabindex
|
|
);
|
|
textbox.replaceWith(elem);
|
|
|
|
// Disassociate textbox from label
|
|
label.setAttribute('control', elem.getAttribute('id'));
|
|
|
|
if (field == 'creator') {
|
|
// Set correct flex settings and fieldMode attribute
|
|
this.switchCreatorMode(row, (otherFields.fieldMode ? 1 : 0), true);
|
|
}
|
|
|
|
if (this.saveOnEdit) {
|
|
await this.item.saveTx();
|
|
}
|
|
}
|
|
|
|
_rowIsClickable(fieldName) {
|
|
return this.clickByRow &&
|
|
(this.clickable ||
|
|
this._clickableFields.indexOf(fieldName) != -1);
|
|
}
|
|
|
|
_fieldIsClickable(fieldName) {
|
|
return !this.clickByRow &&
|
|
((this.clickable && !Zotero.Items.isPrimaryField(fieldName))
|
|
|| this._clickableFields.indexOf(fieldName) != -1);
|
|
}
|
|
|
|
_modifyField(field, value) {
|
|
this.item.setField(field, value);
|
|
}
|
|
|
|
_getFieldValue(label) {
|
|
return label.firstChild?.nodeValue
|
|
|| label.value
|
|
|| label.textContent;
|
|
}
|
|
|
|
_setFieldValue(label, value) {
|
|
if (label.firstChild) {
|
|
label.firstChild.nodeValue = value;
|
|
}
|
|
else if (label instanceof HTMLInputElement || label instanceof HTMLTextAreaElement) {
|
|
label.value = value;
|
|
}
|
|
else {
|
|
label.textContent = value;
|
|
}
|
|
}
|
|
|
|
textTransformString(val, mode) {
|
|
switch (mode) {
|
|
case 'title':
|
|
return Zotero.Utilities.capitalizeTitle(val.toLowerCase(), true);
|
|
case 'sentence':
|
|
// capitalize the first letter, including after beginning punctuation
|
|
// capitalize after ?, ! and remove space(s) before those as well as colon analogous to capitalizeTitle function
|
|
// also deal with initial punctuation here - open quotes and Spanish beginning punctuation marks
|
|
val = val.toLowerCase().replace(/\s*:/, ":");
|
|
val = val.replace(/(([\?!]\s*|^)([\'\"¡¿“‘„«\s]+)?[^\s])/g, function (x) {
|
|
return x.replace(/\s+/m, " ").toUpperCase();});
|
|
return val;
|
|
default:
|
|
throw new Error("Invalid transform mode '" + mode + "' in ItemBox.textTransformString()");
|
|
}
|
|
}
|
|
|
|
canTextTransformField(label, mode) {
|
|
let val = this._getFieldValue(label);
|
|
return this.textTransformString(val, mode) != val;
|
|
}
|
|
|
|
/**
|
|
* TODO: work with textboxes too
|
|
*/
|
|
async textTransformField(label, mode) {
|
|
var val = this._getFieldValue(label);
|
|
var newVal = this.textTransformString(val, mode);
|
|
this._setFieldValue(label, newVal);
|
|
var fieldName = label.getAttribute('fieldname');
|
|
this._modifyField(fieldName, newVal);
|
|
|
|
// If this is a title field, convert the Short Title too
|
|
var isTitle = Zotero.ItemFields.getBaseIDFromTypeAndField(
|
|
this.item.itemTypeID, fieldName) == Zotero.ItemFields.getID('title');
|
|
var shortTitleVal = this.item.getField('shortTitle');
|
|
if (isTitle && newVal.toLowerCase().startsWith(shortTitleVal.toLowerCase())) {
|
|
this._modifyField('shortTitle', newVal.substr(0, shortTitleVal.length));
|
|
}
|
|
|
|
if (this.saveOnEdit) {
|
|
// If a field is open, blur it, which will trigger a save and cause
|
|
// the saveTx() to be a no-op
|
|
await this.blurOpenField();
|
|
await this.item.saveTx();
|
|
}
|
|
}
|
|
|
|
getCreatorFields(row) {
|
|
var typeID = row.getElementsByClassName('creator-type-label')[0].getAttribute('typeid');
|
|
var label1 = row.getElementsByClassName('creator-name-box')[0].firstChild;
|
|
var label2 = label1.parentNode.lastChild;
|
|
|
|
var fields = {
|
|
lastName: label1.firstChild ? label1.firstChild.nodeValue : label1.value,
|
|
firstName: label2.firstChild ? label2.firstChild.nodeValue : label2.value,
|
|
fieldMode: label1.getAttribute('fieldMode')
|
|
? parseInt(label1.getAttribute('fieldMode')) : 0,
|
|
creatorTypeID: parseInt(typeID),
|
|
};
|
|
|
|
// Ignore '(first)'
|
|
if (fields.fieldMode == 1 || fields.firstName == this._defaultFirstName) {
|
|
fields.firstName = '';
|
|
}
|
|
// Ignore '(last)' or '(name)'
|
|
if (fields.lastName == this._defaultFullName
|
|
|| fields.lastName == this._defaultLastName) {
|
|
fields.lastName = '';
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
modifyCreator(index, fields) {
|
|
var libraryID = this.item.libraryID;
|
|
var firstName = fields.firstName;
|
|
var lastName = fields.lastName;
|
|
var fieldMode = fields.fieldMode;
|
|
var creatorTypeID = fields.creatorTypeID;
|
|
|
|
var oldCreator = this.item.getCreator(index);
|
|
|
|
// Don't save empty creators
|
|
if (!firstName && !lastName){
|
|
if (!oldCreator) {
|
|
return false;
|
|
}
|
|
return this.item.removeCreator(index);
|
|
}
|
|
|
|
return this.item.setCreator(index, fields);
|
|
}
|
|
|
|
/**
|
|
* @return {Promise}
|
|
*/
|
|
async swapNames(event) {
|
|
var row = document.popupNode.closest('tr');
|
|
var typeBox = row.querySelector('.creator-type-label');
|
|
var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
|
var fields = this.getCreatorFields(row);
|
|
var lastName = fields.lastName;
|
|
var firstName = fields.firstName;
|
|
fields.lastName = firstName;
|
|
fields.firstName = lastName;
|
|
this.modifyCreator(creatorIndex, fields);
|
|
if (this.saveOnEdit) {
|
|
// See note in transformText()
|
|
await this.blurOpenField();
|
|
await this.item.saveTx();
|
|
}
|
|
}
|
|
|
|
canCapitalizeCreatorName(row) {
|
|
var fields = this.getCreatorFields(row);
|
|
return fields.firstName && Zotero.Utilities.capitalizeName(fields.firstName) != fields.firstName
|
|
|| fields.lastName && Zotero.Utilities.capitalizeName(fields.lastName) != fields.lastName;
|
|
}
|
|
|
|
/**
|
|
* @return {Promise}
|
|
*/
|
|
async capitalizeCreatorName(event) {
|
|
var row = document.popupNode.closest('tr');
|
|
var typeBox = row.querySelector('.creator-type-label');
|
|
var creatorIndex = parseInt(typeBox.getAttribute('fieldname').split('-')[1]);
|
|
var fields = this.getCreatorFields(row);
|
|
fields.firstName = fields.firstName && Zotero.Utilities.capitalizeName(fields.firstName);
|
|
fields.lastName = fields.lastName && Zotero.Utilities.capitalizeName(fields.lastName);
|
|
this.modifyCreator(creatorIndex, fields);
|
|
if (this.saveOnEdit) {
|
|
// See note in transformText()
|
|
await this.blurOpenField();
|
|
await this.item.saveTx();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {Promise}
|
|
*/
|
|
moveCreator(index, dir) {
|
|
return Zotero.spawn(function* () {
|
|
yield this.blurOpenField();
|
|
if (index == 0 && dir == 'up') {
|
|
Zotero.debug("Can't move up creator 0");
|
|
return;
|
|
}
|
|
else if (index + 1 == this.item.numCreators() && dir == 'down') {
|
|
Zotero.debug("Can't move down last creator");
|
|
return;
|
|
}
|
|
|
|
var newIndex;
|
|
switch (dir) {
|
|
case 'top':
|
|
newIndex = 0;
|
|
break;
|
|
|
|
case 'up':
|
|
newIndex = index - 1;
|
|
break;
|
|
|
|
case 'down':
|
|
newIndex = index + 1;
|
|
break;
|
|
}
|
|
let creator = this.item.getCreator(index);
|
|
// When moving to top, increment index of all other creators
|
|
if (dir == 'top') {
|
|
let otherCreators = this.item.getCreators();
|
|
this.item.setCreator(newIndex, creator);
|
|
for (let i = 0; i < index; i++) {
|
|
this.item.setCreator(i + 1, otherCreators[i]);
|
|
}
|
|
}
|
|
// When moving up or down, swap places with next creator
|
|
else {
|
|
let otherCreator = this.item.getCreator(newIndex);
|
|
this.item.setCreator(newIndex, creator);
|
|
this.item.setCreator(index, otherCreator);
|
|
}
|
|
if (this.saveOnEdit) {
|
|
return this.item.saveTx();
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
_updateAutoCompleteParams(row, changedParams) {
|
|
var textboxes = row.querySelectorAll('input');
|
|
if (textboxes.length) {
|
|
var t = textboxes[0];
|
|
var params = JSON.parse(t.getAttribute('autocompletesearchparam'));
|
|
for (var param in changedParams) {
|
|
params[param] = changedParams[param];
|
|
}
|
|
t.setAttribute('autocompletesearchparam', JSON.stringify(params));
|
|
}
|
|
}
|
|
|
|
focusFirstField() {
|
|
this._focusNextField(1);
|
|
}
|
|
|
|
focusLastField() {
|
|
const tabbableFields = this.querySelectorAll('*[ztabindex]:not([disabled=true])');
|
|
const last = tabbableFields[tabbableFields.length - 1];
|
|
|
|
if (last.classList.contains('zotero-focusable')) {
|
|
last.focus();
|
|
}
|
|
// Fields need to be clicked
|
|
else {
|
|
last.click();
|
|
}
|
|
}
|
|
|
|
focusField(fieldName) {
|
|
let field = this.querySelector(`[fieldname="${fieldName}"][ztabindex]`);
|
|
if (!field) return false;
|
|
return this._focusNextField(field.getAttribute('ztabindex'));
|
|
}
|
|
|
|
/**
|
|
* Advance the field focus forward or backward
|
|
*
|
|
* Note: We're basically replicating the built-in tabindex functionality,
|
|
* which doesn't work well with the weird label/textbox stuff we're doing.
|
|
* (The textbox being tabbed away from is deleted before the blur()
|
|
* completes, so it doesn't know where it's supposed to go next.)
|
|
*/
|
|
_focusNextField(tabindex, back) {
|
|
var box = this._infoTable;
|
|
tabindex = parseInt(tabindex);
|
|
|
|
// Get all fields with ztabindex attributes
|
|
var tabbableFields = box.querySelectorAll('*[ztabindex]:not([disabled=true])');
|
|
|
|
if (!tabbableFields.length) {
|
|
Zotero.debug("No tabbable fields found");
|
|
return false;
|
|
}
|
|
|
|
var next;
|
|
if (back) {
|
|
Zotero.debug('Looking for previous tabindex before ' + tabindex, 4);
|
|
for (let i = tabbableFields.length - 1; i >= 0; i--) {
|
|
let field = tabbableFields[i];
|
|
let tabIndexHere = parseInt(field.getAttribute('ztabindex'));
|
|
if (tabIndexHere !== -1 && tabIndexHere < tabindex) {
|
|
next = tabbableFields[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
Zotero.debug('Looking for tabindex ' + tabindex, 4);
|
|
for (var pos = 0; pos < tabbableFields.length; pos++) {
|
|
let field = tabbableFields[pos];
|
|
let tabIndexHere = parseInt(field.getAttribute('ztabindex'));
|
|
if (tabIndexHere !== -1 && tabIndexHere >= tabindex) {
|
|
next = tabbableFields[pos];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!next) {
|
|
Zotero.debug("Next field not found");
|
|
return false;
|
|
}
|
|
|
|
// Drop-down and creator buttons need to be focused
|
|
if (next.id == 'item-type-menu' || next.classList.contains('zotero-focusable')) {
|
|
next.focus();
|
|
}
|
|
// Fields need to be clicked
|
|
else {
|
|
next.click();
|
|
}
|
|
|
|
// 1) next.parentNode is always null for some reason
|
|
// 2) For some reason it's necessary to scroll to the next element when
|
|
// moving forward for the target element to be fully in view
|
|
if (!back && tabbableFields[pos + 1]) {
|
|
Zotero.debug("Scrolling to next field");
|
|
var visElem = tabbableFields[pos + 1];
|
|
}
|
|
else {
|
|
var visElem = next;
|
|
}
|
|
this.ensureElementIsVisible(visElem);
|
|
|
|
return true;
|
|
}
|
|
|
|
updateLastFocused(ev) {
|
|
if (ev.target.classList.contains('zotero-focusable')) {
|
|
this._lastTabIndex = parseInt(ev.target.getAttribute('ztabindex'));
|
|
}
|
|
}
|
|
|
|
async blurOpenField() {
|
|
var activeField = this._infoTable.querySelector('input, textarea');
|
|
if (!activeField) {
|
|
return false;
|
|
}
|
|
return this.blurHandler(activeField);
|
|
}
|
|
|
|
/**
|
|
* Available handlers:
|
|
*
|
|
* - 'itemtypechange'
|
|
*
|
|
* Note: 'this' in the function will be bound to the item box.
|
|
*/
|
|
addHandler(eventName, func) {
|
|
if (!this.eventHandlers[eventName]) {
|
|
this.eventHandlers[eventName] = [];
|
|
}
|
|
this.eventHandlers[eventName].push(func);
|
|
}
|
|
|
|
removeHandler(eventName, func) {
|
|
if (!this.eventHandlers[eventName]) {
|
|
return;
|
|
}
|
|
var pos = this.eventHandlers[eventName].indexOf(func);
|
|
if (pos != -1) {
|
|
this.eventHandlers[eventName].splice(pos, 1);
|
|
}
|
|
}
|
|
|
|
updateRetracted() {
|
|
// Create the real function here so we can use Zotero.serial(). updateRetracted()
|
|
// isn't awaited in refresh(), so we want to make sure successive invocations
|
|
// don't overlap.
|
|
if (!this._updateRetracted) {
|
|
this._updateRetracted = Zotero.serial(async function (item) {
|
|
var show = Zotero.Retractions.isRetracted(item);
|
|
if (!show) {
|
|
this._id('retraction-box').hidden = true;
|
|
return;
|
|
}
|
|
var data = await Zotero.Retractions.getData(item);
|
|
|
|
this._id('retraction-box').hidden = false;
|
|
this._id('retraction-header-text').textContent
|
|
= Zotero.getString('retraction.banner');
|
|
|
|
// Date
|
|
if (data.date) {
|
|
this._id('retraction-date').hidden = false;
|
|
this._id('retraction-date').textContent = Zotero.getString(
|
|
'retraction.date',
|
|
data.date.toLocaleDateString()
|
|
);
|
|
}
|
|
else {
|
|
this._id('retraction-date').hidden = true;
|
|
}
|
|
|
|
// Reasons
|
|
var allowHiding = false;
|
|
if (data.reasons.length) {
|
|
let elem = this._id('retraction-reasons');
|
|
elem.hidden = false;
|
|
elem.textContent = '';
|
|
for (let reason of data.reasons) {
|
|
let dt = document.createElement('dt');
|
|
let dd = document.createElement('dd');
|
|
|
|
dt.textContent = reason;
|
|
dd.textContent = Zotero.Retractions.getReasonDescription(reason);
|
|
|
|
elem.appendChild(dt);
|
|
elem.appendChild(dd);
|
|
|
|
if (reason == 'Retract and Replace') {
|
|
allowHiding = true;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
this._id('retraction-reasons').hidden = true;
|
|
}
|
|
|
|
// Retraction DOI or PubMed ID
|
|
if (data.doi || data.pmid) {
|
|
let div = this._id('retraction-notice');
|
|
div.textContent = '';
|
|
let a = document.createElement('a');
|
|
a.textContent = Zotero.getString('retraction.notice');
|
|
if (data.doi) {
|
|
a.href = 'https://doi.org/' + data.doi;
|
|
}
|
|
else {
|
|
a.href = `https://www.ncbi.nlm.nih.gov/pubmed/${data.pmid}/`;
|
|
}
|
|
div.appendChild(a);
|
|
}
|
|
else {
|
|
this._id('retraction-notice').hidden = true;
|
|
}
|
|
|
|
// Links
|
|
if (data.urls.length) {
|
|
let div = this._id('retraction-links');
|
|
div.hidden = false;
|
|
div.textContent = '';
|
|
|
|
let p = document.createElement('p');
|
|
p.textContent = Zotero.getString('retraction.details');
|
|
|
|
let ul = document.createElement('ul');
|
|
for (let url of data.urls) {
|
|
let li = document.createElement('li');
|
|
let a = document.createElement('a');
|
|
url = url.replace(/^http:/, 'https:');
|
|
a.href = url;
|
|
a.textContent = url;
|
|
li.appendChild(a);
|
|
ul.appendChild(li);
|
|
}
|
|
|
|
div.appendChild(p);
|
|
div.appendChild(ul);
|
|
}
|
|
else {
|
|
this._id('retraction-links').hidden = true;
|
|
}
|
|
|
|
let creditElem = this._id('retraction-credit');
|
|
if (!creditElem.childNodes.length) {
|
|
let text = Zotero.getString(
|
|
'retraction.credit',
|
|
'<a href="https://retractionwatch.com">Retraction Watch</a>'
|
|
);
|
|
let parts = Zotero.Utilities.parseMarkup(text);
|
|
for (let part of parts) {
|
|
if (part.type == 'text') {
|
|
creditElem.appendChild(document.createTextNode(part.text));
|
|
}
|
|
else if (part.type == 'link') {
|
|
let a = document.createElement('a');
|
|
a.href = part.attributes.href;
|
|
a.textContent = part.text;
|
|
creditElem.appendChild(a);
|
|
}
|
|
}
|
|
}
|
|
|
|
let hideElem = this._id('retraction-hide');
|
|
hideElem.firstChild.textContent = Zotero.getString('retraction.replacedItem.hide');
|
|
hideElem.hidden = !allowHiding;
|
|
hideElem.firstChild.onclick = (event) => {
|
|
ZoteroPane.promptToHideRetractionForReplacedItem(item);
|
|
};
|
|
|
|
Zotero.Utilities.Internal.updateHTMLInXUL(this._id('retraction-box'));
|
|
}.bind(this));
|
|
}
|
|
|
|
return this._updateRetracted(this.item);
|
|
}
|
|
|
|
_id(id) {
|
|
return this.querySelector(`#${id}`);
|
|
}
|
|
}
|
|
customElements.define("item-box", ItemBox);
|
|
}
|