From 62162d956c462ea1b52410d2dff1440741e70ecf Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Tue, 7 Apr 2026 14:44:13 -0700 Subject: [PATCH 1/2] preview of progressWindow for claude --- .claude/launch.json | 11 ++ gulpfile.js | 2 + .../progressWindow_preview.html | 164 ++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 .claude/launch.json create mode 100644 src/common/progressWindow/progressWindow_preview.html diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 000000000..faf39392e --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "preview-server", + "runtimeExecutable": "/usr/bin/python3", + "runtimeArgs": ["-m", "http.server", "8765", "--directory", "build/firefox"], + "port": 8765 + } + ] +} diff --git a/gulpfile.js b/gulpfile.js index 41ec57ef9..9b11523a4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -269,6 +269,7 @@ function processFile() { } case 'preferences.html': case 'progressWindow.html': + case 'progressWindow_preview.html': case 'modalPrompt.html': case 'offscreenSandbox.html': file.contents = Buffer.from(file.contents.toString() @@ -398,6 +399,7 @@ gulp.task('process-custom-scripts', function() { './src/browserExt/confirm/confirm.html', './src/common/preferences/preferences.html', './src/common/progressWindow/progressWindow.html', + './src/common/progressWindow/progressWindow_preview.html', './src/common/modalPrompt/modalPrompt.html', './src/browserExt/offscreen/offscreenSandbox.html', './src/common/schema.js', diff --git a/src/common/progressWindow/progressWindow_preview.html b/src/common/progressWindow/progressWindow_preview.html new file mode 100644 index 000000000..0528568fd --- /dev/null +++ b/src/common/progressWindow/progressWindow_preview.html @@ -0,0 +1,164 @@ + + + + + + + ProgressWindow Preview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 40571b5d675bb3e8a164bbaee0f87ca599ce6ea6 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Thu, 9 Apr 2026 15:30:17 -0700 Subject: [PATCH 2/2] mini metadata table in connector popup After translation, display a mini version of itemBox when user clicks on the chevron next to the name of a regular item. On update, metadata is sent to the local Zotero server to update the data in the newly added item. That way, one can verify what the metadata looks like and make adjustments without going back to the desktop app: https://forums.zotero.org/discussion/comment/510105/#Comment_510105 --- src/common/cachedTypes.js | 10 + src/common/connector.js | 1 + src/common/inject/pageSaving.js | 13 +- src/common/inject/progressWindow_inject.js | 85 ++- src/common/progressWindow/chevron-8.svg | 3 + src/common/progressWindow/input-dual.svg | 3 + src/common/progressWindow/input-single.svg | 3 + src/common/progressWindow/minus-circle.svg | 3 + src/common/progressWindow/plus-circle.svg | 3 + src/common/progressWindow/progressWindow.css | 297 ++++++++ src/common/progressWindow/progressWindow.html | 1 + .../progressWindow_preview.html | 46 ++ src/common/schema.js | 2 + src/common/ui/ItemBox.jsx | 719 ++++++++++++++++++ src/common/ui/ProgressWindow.jsx | 89 ++- src/messages.json | 30 + 16 files changed, 1294 insertions(+), 14 deletions(-) create mode 100644 src/common/progressWindow/chevron-8.svg create mode 100644 src/common/progressWindow/input-dual.svg create mode 100644 src/common/progressWindow/input-single.svg create mode 100644 src/common/progressWindow/minus-circle.svg create mode 100644 src/common/progressWindow/plus-circle.svg create mode 100644 src/common/ui/ItemBox.jsx diff --git a/src/common/cachedTypes.js b/src/common/cachedTypes.js index 213bc0add..f476715b9 100644 --- a/src/common/cachedTypes.js +++ b/src/common/cachedTypes.js @@ -172,6 +172,16 @@ Zotero.Connector_Types = new function() { this.getItemTypeFields = function(typeIdOrName) { return itemTypes[typeIdOrName][4]/* fields */.slice(); }; + + this.isMultiline = function(field) { + field = this.getName(field); + var multilineFields = [ + 'abstractNote', + 'extra', + 'address' + ]; + return multilineFields.indexOf(field) != -1; + }; } }; diff --git a/src/common/connector.js b/src/common/connector.js index a8eb66342..4763b2412 100644 --- a/src/common/connector.js +++ b/src/common/connector.js @@ -81,6 +81,7 @@ Zotero.Connector = new function() { 'googleDocsCitationExplorerEnabled', 'supportsAttachmentUpload', 'supportsTagsAutocomplete', + 'supportsMetadataUpdates', 'canUserAddNote' ]; for (const key of PREF_KEYS) { diff --git a/src/common/inject/pageSaving.js b/src/common/inject/pageSaving.js index e539be3b8..0a3c30d41 100644 --- a/src/common/inject/pageSaving.js +++ b/src/common/inject/pageSaving.js @@ -241,6 +241,16 @@ let PageSaving = { itemType: item.itemType } ); + + // Send full item data to have progressWindow render it in the itemBox + Zotero.Messaging.sendMessage( + "progressWindow.setItemMetadata", + { + sessionID, + id: item.id, + item + } + ); }; const onTranslatorFallback = (oldTranslator, newTranslator) => { Zotero.debug(`Saving with ${oldTranslator.label} failed. Trying ${newTranslator.label}`); @@ -669,7 +679,8 @@ let PageSaving = { sessionID: this.sessionDetails.id, target: data.target, tags: data.tags, - note: data.note + note: data.note, + updatedMetadata: data.updatedMetadata } ); diff --git a/src/common/inject/progressWindow_inject.js b/src/common/inject/progressWindow_inject.js index 0e1245867..97b6e68cf 100644 --- a/src/common/inject/progressWindow_inject.js +++ b/src/common/inject/progressWindow_inject.js @@ -44,6 +44,7 @@ if (isTopWindow) { // var frameID = 'zotero-progress-window-frame'; var closeOnLeave = false; + var itemBoxOpen = false; var lastSuccessfulTarget; var frameReadyDeferred = Zotero.Promise.defer(); var frameInitialized; @@ -83,6 +84,57 @@ if (isTopWindow) { sendMessage(`progressWindowIframe.${name}`, data); }); } + + /** + * Transform a raw translator item into the format expected by the ItemBox component: + * { id, fields: [{name, label, value}], creators: [...], creatorTypes: [{value, label}] } + * + * Field and type labels come from Zotero.Schema.locale (en-US locale from schema.json), + * stored during Zotero.Schema.init() in schema.js. + */ + function formatItemMetadata(id, item) { + let locale = Zotero.Schema.locale || {}; + let fieldLocale = locale.fields || {}; + let itemTypeLocale = locale.itemTypes || {}; + let creatorTypeLocale = locale.creatorTypes || {}; + + let itemTypeID = Zotero.ItemTypes.getID(item.itemType); + let fieldIDs = Zotero.ItemFields.getItemTypeFields(itemTypeID); + + let fields = [ + { + name: 'itemType', + label: Zotero.getString("progressWindow_itemBox_itemType"), + value: itemTypeLocale[item.itemType] || item.itemType + } + ]; + for (let fieldID of fieldIDs) { + let fieldName = Zotero.ItemFields.getName(fieldID); + let value = item[fieldName]; + let field = { + name: fieldName, + label: fieldLocale[fieldName] || fieldName, + value: (value != null && value !== '') ? String(value) : '' + }; + if (Zotero.ItemFields.isMultiline(fieldID)) { + field.multiline = true; + } + fields.push(field); + } + + let creatorTypeObjs = Zotero.CreatorTypes.getTypesForItemType(itemTypeID); + let creatorTypes = creatorTypeObjs.map(ct => ({ + value: ct.name, + label: creatorTypeLocale[ct.name] || ct.name + })); + + return { + id, + fields, + creators: item.creators || [], + creatorTypes + }; + } function changeHeadline() { isReadOnly = arguments.length <= 2; @@ -225,8 +277,10 @@ if (isTopWindow) { // Don't start the timer if the mouse is over the popup or the tags box has focus if (insideIframe) return; if (closeTimerDisabled) return; - + if (!delay) delay = 5000; + // Give the user more time if they have the item box open + if (itemBoxOpen) delay = Math.max(delay, 20000); stopCloseTimer(); closeTimeoutID = setTimeout(hideFrame, delay); } @@ -298,7 +352,12 @@ if (isTopWindow) { // If we're making changes, don't close the popup and keep delaying syncs stopCloseTimer(); blurred = false; - + + // If target selector hasn't been used yet, data.target may be undefined. + // Use lastSuccessfulTarget as fallback. + if (!data.target && !lastSuccessfulTarget) return; + var target = data.target || lastSuccessfulTarget; + // If the session isn't yet registered or a session update is in progress, // store the data to run after, overwriting any already-queued data if (!createdSessions.has(currentSessionID) || updatingSession) { @@ -311,11 +370,12 @@ if (isTopWindow) { await sendMessage( "updateSession", { - target: data.target.id, + target: target.id, tags: data.tags, + updatedMetadata: data.updatedMetadata, note: data.note.replace(/\n/g, "
"), // replace newlines with
for note-editor - resaveAttachments: !lastSuccessfulTarget.filesEditable && data.target.filesEditable, - removeAttachments: lastSuccessfulTarget.filesEditable && !data.target.filesEditable + resaveAttachments: !lastSuccessfulTarget.filesEditable && target.filesEditable, + removeAttachments: lastSuccessfulTarget.filesEditable && !target.filesEditable } ); } @@ -335,7 +395,7 @@ if (isTopWindow) { } // Keep track of last successful target to show on reopen and failure - lastSuccessfulTarget = data.target; + lastSuccessfulTarget = target; }; // Once a session is created in the client, send any queued session data @@ -358,6 +418,7 @@ if (isTopWindow) { addMessageListener('progressWindowIframe.disableCloseTimer', () => closeTimerDisabled = true); addMessageListener('progressWindowIframe.enableCloseTimer', () => closeTimerDisabled = false); + addMessageListener('progressWindowIframe.itemBoxToggled', (data) => { itemBoxOpen = data.open; }); addMessageListener('progressWindowIframe.blurred', async function() { blurred = true; @@ -366,7 +427,12 @@ if (isTopWindow) { // (i.e., they didn't just switch to another window) await Zotero.Promise.delay(150); if (lastClick > new Date() - 500) { - hideFrame(); + if (itemBoxOpen) { + startCloseTimer(20000); + } + else { + hideFrame(); + } } }); @@ -467,6 +533,11 @@ if (isTopWindow) { addEvent("updateProgress", [data.id, data]); }); + Zotero.Messaging.addMessageListener("progressWindow.setItemMetadata", (data) => { + if (data.sessionID && data.sessionID != currentSessionID) return; + addEvent("setItemMetadata", [formatItemMetadata(data.id, data.item)]); + }); + Zotero.Messaging.addMessageListener("progressWindow.close", function () { // Mark frame as hidden so that if this is called after a progressWindow.show but before // the popup has been initialized (e.g., when displaying the Select Items dialog) it's diff --git a/src/common/progressWindow/chevron-8.svg b/src/common/progressWindow/chevron-8.svg new file mode 100644 index 000000000..5b3cd41c9 --- /dev/null +++ b/src/common/progressWindow/chevron-8.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/progressWindow/input-dual.svg b/src/common/progressWindow/input-dual.svg new file mode 100644 index 000000000..fb8bf798b --- /dev/null +++ b/src/common/progressWindow/input-dual.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/progressWindow/input-single.svg b/src/common/progressWindow/input-single.svg new file mode 100644 index 000000000..a67bd5a1a --- /dev/null +++ b/src/common/progressWindow/input-single.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/progressWindow/minus-circle.svg b/src/common/progressWindow/minus-circle.svg new file mode 100644 index 000000000..be7b5e340 --- /dev/null +++ b/src/common/progressWindow/minus-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/progressWindow/plus-circle.svg b/src/common/progressWindow/plus-circle.svg new file mode 100644 index 000000000..6d3bbde02 --- /dev/null +++ b/src/common/progressWindow/plus-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/progressWindow/progressWindow.css b/src/common/progressWindow/progressWindow.css index c02035635..727f7123b 100644 --- a/src/common/progressWindow/progressWindow.css +++ b/src/common/progressWindow/progressWindow.css @@ -372,3 +372,300 @@ html[dir="rtl"] .arrow svg, margin-left: 4px; } + +/* ---------------------- itemBox.css -------------------------- */ + +.ItemBox { + margin: 4px 0 4px 4px; + padding: 6px 0; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +.ItemBox-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 8px; + row-gap: 2px; +} + +.ItemBox-row { + display: grid; + grid-template-columns: subgrid; + grid-column: span 2; + align-items: baseline; +} + +.ItemBox-label { + display: flex; + justify-content: flex-end; + align-items: center; + color: rgba(0, 0, 0, 0.55); + font-size: 13px; + font-weight: normal; + white-space: nowrap; +} + +.ItemBox-label label { + cursor: default; + user-select: none; +} + +.ItemBox-value { + width: 0; + min-width: 100%; + display: flex; + align-items: center; + font-size: 13px; +} + +.ItemBox-input { + flex: 1; + min-width: 0; + max-width: 100%; + font-family: inherit; + font-size: inherit; + padding: 1px 4px; + border: 1px solid transparent; + border-radius: 5px; + background: transparent; + outline: none; + box-sizing: border-box; +} + +.ItemBox-input:not(:read-only):hover { + background: rgba(0, 0, 0, 0.05); + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05); +} + +.ItemBox-input:not(:read-only):focus { + border-color: var(--theme-selection-background, #0a84ff); + background: white; + box-shadow: none; +} + +.ItemBox-textarea { + resize: none; + font: inherit; + line-height: 1.4; + max-height: calc(1.4em * 5 + 2px + 2px); /* at most 5 lines + padding + border */ + overflow-y: auto; +} + +.ItemBox-row--multiline .ItemBox-label { + padding-top: 3px; +} + +.ItemBox-creatorTypeLabel.disabled { + cursor: default; +} + +.ItemBox-creatorTypeLabel.disabled:hover { + background: transparent; +} + +/* Creator type selector — custom dropdown matching Zotero desktop */ +.ItemBox-creatorTypeSelector { + position: relative; +} + +.ItemBox-creatorTypeLabel { + display: flex; + align-items: center; + gap: 2px; + padding: 1px 4px; + border-radius: 5px; + cursor: pointer; + color: rgba(0, 0, 0, 0.55); + font-size: 13px; + white-space: nowrap; + user-select: none; + outline: none; +} + +.ItemBox-creatorTypeLabel:hover { + background: rgba(0, 0, 0, 0.05); +} + +.ItemBox-creatorTypeLabel:hover .ItemBox-creatorTypeChevron, +.ItemBox-creatorTypeLabel:focus .ItemBox-creatorTypeChevron { + visibility: visible; +} + +.ItemBox-creatorTypeLabel:focus-visible { + box-shadow: 0 0 0 1px var(--theme-selection-background, #0a84ff); +} + +.ItemBox-creatorTypeChevron { + display: inline-block; + width: 8px; + height: 8px; + background-color: #0000008c; + -webkit-mask-image: url('chevron-8.svg'); + mask-image: url('chevron-8.svg'); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + visibility: hidden; + flex-shrink: 0; +} + +.ItemBox-creatorTypeMenu { + position: absolute; + left: 0; + top: 100%; + z-index: 100; + list-style: none; + margin: 2px 0 0; + padding: 4px 0; + background: white; + border: 1px solid #ccc; + border-radius: 5px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + min-width: 100%; + white-space: nowrap; +} + +.ItemBox-creatorTypeOption { + padding: 2px 12px 2px 8px; + font-size: 13px; + cursor: default; + color: #333; +} + +.ItemBox-creatorTypeOption:hover, +.ItemBox-creatorTypeOption.focused { + background: var(--theme-selection-background, #0a84ff); + color: white; +} + +.ItemBox-creatorTypeMenuSeparator { + height: 0; + margin: 4px 0; + border-top: 1px solid #ddd; +} + +.ItemBox-creatorTypeOption.selected { + font-weight: 600; +} + +/* Creator name inputs — maxWidth is set by JS to each input's content width. + flex-grow distributes remaining space 3:1 in favor of last name, matching + Zotero desktop's sizeToContent approach. */ +.ItemBox-creatorInput:first-of-type { + flex: 3 1 0; + min-width: 0; +} + +.ItemBox-creatorInput:nth-of-type(2) { + flex: 1 1 0; + min-width: 0; +} + +/* Creator +/- buttons — SVG icon buttons matching Zotero desktop. + Icon rendered via ::after pseudo-element with mask-image so CSS controls fill color. + Button background handles hover effect separately. */ +.ItemBox-creatorBtn { + flex: 0 0 auto; + width: 20px; + height: 20px; + padding: 0; + margin: 0; + margin-left: 1px; + cursor: pointer; + border: none; + border-radius: 6px; + background-color: transparent; + position: relative; + box-sizing: border-box; +} + +.ItemBox-creatorBtn::after { + content: ''; + position: absolute; + inset: 1px; + background-color: #0000008c; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: 16px 16px; + mask-size: 16px 16px; +} + +.ItemBox-creatorBtn:not(:disabled):hover { + background-color: rgba(0, 0, 0, 0.05); +} + +.ItemBox-creatorBtn:not(:disabled):active { + background-color: rgba(0, 0, 0, 0.1); +} + +.ItemBox-creatorBtn:disabled { + cursor: default; +} + +.ItemBox-creatorBtn:disabled::after { + opacity: 0.3; +} + +.ItemBox-creatorBtn--plus::after { + -webkit-mask-image: url('plus-circle.svg'); + mask-image: url('plus-circle.svg'); +} + +.ItemBox-creatorBtn--minus::after { + -webkit-mask-image: url('minus-circle.svg'); + mask-image: url('minus-circle.svg'); +} + +.ItemBox-creatorBtn--switch { + margin-left: auto; +} + +/* When in dual mode, show icon to switch to single */ +.ItemBox-creatorBtn--switch.is-dual::after { + -webkit-mask-image: url('input-single.svg'); + mask-image: url('input-single.svg'); + -webkit-mask-size: 20px 16px; + mask-size: 20px 16px; +} + +/* When in single mode, show icon to switch to dual */ +.ItemBox-creatorBtn--switch.is-single::after { + -webkit-mask-image: url('input-dual.svg'); + mask-image: url('input-dual.svg'); + -webkit-mask-size: 20px 16px; + mask-size: 20px 16px; +} + +.ProgressWindow-itemChevron { + flex: 0 0 auto; + width: 16px; + height: 16px; + padding: 0; + margin-left: 4px; + border: none; + border-radius: 3px; + background: url('chevron-8.svg') no-repeat center / 8px 8px transparent; + cursor: pointer; + transform: rotate(0deg); + transition: transform 0.125s ease; + opacity: 0.5; + align-self: center; +} + +.ProgressWindow-itemChevron.is-expanded { + transform: rotate(-180deg); +} + +.ProgressWindow-itemChevron:hover { + background-color: rgba(0, 0, 0, 0.05); + opacity: 0.8; +} diff --git a/src/common/progressWindow/progressWindow.html b/src/common/progressWindow/progressWindow.html index aff953141..06c7d9229 100644 --- a/src/common/progressWindow/progressWindow.html +++ b/src/common/progressWindow/progressWindow.html @@ -14,6 +14,7 @@ + diff --git a/src/common/progressWindow/progressWindow_preview.html b/src/common/progressWindow/progressWindow_preview.html index 0528568fd..fb816a1e5 100644 --- a/src/common/progressWindow/progressWindow_preview.html +++ b/src/common/progressWindow/progressWindow_preview.html @@ -49,6 +49,10 @@ getPlatformInfo: function() { return Promise.resolve({ os: 'mac' }); }, getBrowserInfo: function() { return Promise.resolve({ name: 'Firefox', version: '0' }); }, sendMessage: async function(msg) { + // Return true for pref requests so features are enabled in preview + if (Array.isArray(msg) && msg[0] === 'Connector.getPref') { + return true; + } console.log('[mock] browser.runtime.sendMessage:', msg); return undefined; }, @@ -93,6 +97,7 @@ + @@ -155,6 +160,47 @@ itemType: 'PDF' } ]); + + // Item metadata for ItemBox — all fields for journalArticle, including empty ones + dispatchMessage('progressWindowIframe.setItemMetadata', [{ + id: itemId, + fields: [ + { name: 'itemType', label: 'Item Type', value: 'Journal Article' }, + { name: 'title', label: 'Title', value: 'The Structure of Scientific Revolutions' }, + { name: 'abstractNote', label: 'Abstract', value: 'A groundbreaking work in the philosophy of science that introduced the concept of paradigm shifts. Kuhn argued that scientific progress is not a steady accumulation of knowledge but rather a series of revolutions in which one paradigm replaces another. The book has been influential across many disciplines beyond the history of science.', multiline: true }, + { name: 'publicationTitle', label: 'Publication', value: 'International Encyclopedia of Unified Science' }, + { name: 'volume', label: 'Volume', value: '2' }, + { name: 'issue', label: 'Issue', value: '2' }, + { name: 'pages', label: 'Pages', value: '1-210' }, + { name: 'date', label: 'Date', value: '1962' }, + { name: 'series', label: 'Series', value: '' }, + { name: 'seriesTitle', label: 'Series Title', value: '' }, + { name: 'seriesText', label: 'Series Text', value: '' }, + { name: 'journalAbbreviation', label: 'Journal Abbr', value: '' }, + { name: 'language', label: 'Language', value: '' }, + { name: 'DOI', label: 'DOI', value: '10.1234/example' }, + { name: 'ISSN', label: 'ISSN', value: '' }, + { name: 'shortTitle', label: 'Short Title', value: '' }, + { name: 'url', label: 'URL', value: 'https://example.com/article' }, + { name: 'accessDate', label: 'Accessed', value: '' }, + { name: 'archive', label: 'Archive', value: '' }, + { name: 'archiveLocation', label: 'Loc. in Archive', value: '' }, + { name: 'libraryCatalog', label: 'Library Catalog', value: '' }, + { name: 'callNumber', label: 'Call Number', value: '' }, + { name: 'rights', label: 'Rights', value: '' }, + { name: 'extra', label: 'Extra', value: '', multiline: true } + ], + creators: [ + { firstName: 'Thomas S.', lastName: 'Kuhn', creatorType: 'author' }, + { firstName: 'Margaret', lastName: 'Masterman', creatorType: 'editor' } + ], + creatorTypes: [ + { value: 'author', label: 'Author' }, + { value: 'editor', label: 'Editor' }, + { value: 'translator', label: 'Translator' }, + { value: 'reviewedAuthor', label: 'Reviewed Author' } + ] + }]); }); diff --git a/src/common/schema.js b/src/common/schema.js index 2e34bc58a..4ef0bf5d4 100644 --- a/src/common/schema.js +++ b/src/common/schema.js @@ -3,4 +3,6 @@ var initSchema = Zotero.Schema.init; Zotero.Schema.init = function() { initSchema(data); + // Store en-US locale for item field/type label lookups + Zotero.Schema.locale = data.locales && data.locales['en-US'] || {}; }; diff --git a/src/common/ui/ItemBox.jsx b/src/common/ui/ItemBox.jsx new file mode 100644 index 000000000..e7cd34958 --- /dev/null +++ b/src/common/ui/ItemBox.jsx @@ -0,0 +1,719 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2018 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://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 . + + ***** END LICENSE BLOCK ***** +*/ + +window.Zotero = window.Zotero || {}; +Zotero.UI = Zotero.UI || {}; + +class ItemBoxField extends React.PureComponent { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.textareaRef = React.createRef(); + } + + componentDidMount() { + this.autoSizeTextarea(); + } + + componentDidUpdate(prevProps) { + if (prevProps.value !== this.props.value) { + this.autoSizeTextarea(); + } + } + + autoSizeTextarea() { + var textarea = this.textareaRef.current; + if (!textarea) return; + // Reset to single row to get accurate scrollHeight + textarea.style.height = 'auto'; + textarea.style.height = textarea.scrollHeight + 'px'; + } + + handleChange(event) { + this.props.onChange(this.props.fieldName, event.target.value); + } + + render() { + var { label, value, readOnly, fieldName, multiline } = this.props; + var inputElement; + if (multiline) { + inputElement =