Notes/Tags/Related redesign

This commit is contained in:
Abe Jellinek 2023-11-28 05:58:59 -05:00 committed by Dan Stillman
parent 374b5337d8
commit bf72c9d828
56 changed files with 1367 additions and 934 deletions

View file

@ -938,45 +938,30 @@ var ZoteroContextPane = new function () {
div.className = 'zotero-view-item';
main.append(div);
let createSection = (pane, showAdd = false) => {
let section = document.createXULElement('collapsible-section');
section.dataset.pane = pane;
section.setAttribute('data-l10n-id', 'section-' + pane);
section.toggleAttribute('show-add', showAdd);
return section;
};
// Info
var itemBoxContainer = createSection('info');
var itemBox = new (customElements.get('item-box'));
itemBoxContainer.append(itemBox);
itemBox.setAttribute('data-pane', 'info');
div.append(itemBox);
// Abstract
var abstractBoxContainer = createSection('abstract');
var abstractBox = new (customElements.get('abstract-box'));
abstractBox.className = 'zotero-editpane-abstract';
abstractBoxContainer.append(abstractBox);
abstractBox.setAttribute('data-pane', 'abstract');
div.append(abstractBox);
// TODO: Attachments
// Tags
var tagsBoxContainer = createSection('tags', true);
var tagsBox = new (customElements.get('tags-box'));
tagsBox.className = 'zotero-editpane-tags';
tagsBoxContainer.append(tagsBox);
tagsBox.setAttribute('data-pane', 'tags');
div.append(tagsBox);
// Related
var relatedBoxContainer = createSection('related', true);
var relatedBox = new (customElements.get('related-box'));
relatedBox.className = 'zotero-editpane-related';
relatedBox.addEventListener('click', (event) => {
if (event.originalTarget.closest('.zotero-clicky')) {
Zotero_Tabs.select('zotero-pane');
}
});
relatedBoxContainer.append(relatedBox);
div.append(itemBoxContainer, abstractBoxContainer, tagsBoxContainer, relatedBoxContainer);
relatedBox.setAttribute('data-pane', 'related');
div.append(relatedBox);
// item-pane-sidenav
var sidenav = document.createXULElement('item-pane-sidenav');

View file

@ -50,6 +50,9 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/editableTe
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemPaneSidenav.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/abstractBox.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/collapsibleSection.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentsBox.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/attachmentRow.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/annotationRow.js', this);
// Fix missing property bug that breaks arrow key navigation between <tab>s
{

View file

@ -28,7 +28,11 @@
{
class AbstractBox extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<editable-text multiline="true" data-l10n-id="abstract-field" />
<collapsible-section data-l10n-id="section-abstract" data-pane="abstract">
<html:div class="body">
<editable-text multiline="true" data-l10n-id="abstract-field" />
</html:div>
</collapsible-section>
`);
_item = null;
@ -83,8 +87,10 @@
}
async blurOpenField() {
this.abstractField.blur();
await this.save();
if (this.abstractField?.matches(':focus-within')) {
this.abstractField.blur();
await this.save();
}
}
render() {
@ -92,9 +98,10 @@
return;
}
let title = this.item.getField('abstractNote');
if (this.abstractField.initialValue !== title) {
this.abstractField.value = title;
let abstract = this.item.getField('abstractNote');
if (!this.abstractField.initialValue || this.abstractField.initialValue !== abstract) {
this.abstractField.value = abstract;
this.abstractField.initialValue = '';
}
this.abstractField.readOnly = this._mode == 'view';
this.abstractField.setAttribute('aria-label', Zotero.ItemFields.getLocalizedString('abstractNote'));

View file

@ -0,0 +1,121 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2023 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 AnnotationRow extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="head">
<image class="icon"/>
<html:div class="title"/>
</html:div>
<html:div class="body"/>
<html:div class="tags"/>
`);
_annotation = null;
_mode = null;
_listenerAdded = false;
static get observedAttributes() {
return ['annotation-id'];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'annotation-id':
this._annotation = Zotero.Items.get(newValue);
break;
}
this.render();
}
get annotation() {
return this._annotation;
}
set annotation(annotation) {
this._annotation = annotation;
this.setAttribute('annotation-id', annotation.id);
}
init() {
this._head = this.querySelector('.head');
this._title = this.querySelector('.title');
this._body = this.querySelector('.body');
this._tags = this.querySelector('.tags');
this.render();
}
render() {
if (!this.initialized) return;
this._title.textContent = Zotero.getString('pdfReader.page') + ' '
+ (this._annotation.annotationPageLabel || '-');
let type = this._annotation.annotationType;
if (type == 'image') {
type = 'area';
}
this.querySelector('.icon').src = 'chrome://zotero/skin/16/universal/annotate-' + type + '.svg';
this._body.replaceChildren();
if (['image', 'ink'].includes(this._annotation.annotationType)) {
let imagePath = Zotero.Annotations.getCacheImagePath(this._annotation);
if (imagePath) {
let img = document.createElement('img');
img.src = Zotero.File.pathToFileURI(imagePath);
img.draggable = false;
this._body.append(img);
}
}
if (this._annotation.annotationText) {
let text = document.createElement('div');
text.classList.add('quote');
text.textContent = this._annotation.annotationText;
this._body.append(text);
}
if (this._annotation.annotationComment) {
let comment = document.createElement('div');
comment.classList.add('comment');
comment.textContent = this._annotation.annotationComment;
this._body.append(comment);
}
let tags = this._annotation.getTags();
this._tags.hidden = !tags.length;
this._tags.textContent = tags.map(tag => tag.tag).sort(Zotero.localeCompare).join(Zotero.getString('punctuation.comma') + ' ');
this.style.setProperty('--annotation-color', this._annotation.annotationColor);
}
}
customElements.define('annotation-row', AnnotationRow);
}

View file

@ -0,0 +1,177 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2023 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";
import { getCSSItemTypeIcon } from 'components/icons';
{
class AttachmentRow extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="head">
<html:span class="twisty"/>
<html:div class="clicky-item">
<html:span class="icon"/>
<html:div class="label"/>
</html:div>
</html:div>
<html:div class="body"/>
`);
_attachment = null;
_mode = null;
_listenerAdded = false;
static get observedAttributes() {
return ['attachment-id'];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'attachment-id':
this._attachment = Zotero.Items.get(newValue);
break;
}
this.render();
}
get open() {
if (this.empty) {
return false;
}
return this.hasAttribute('open');
}
set open(val) {
val = !!val;
let open = this.open;
if (open === val || this.empty) return;
this.render();
let openHeight = this._body.scrollHeight;
if (openHeight) {
this.style.setProperty('--open-height', `${openHeight}px`);
}
else {
this.style.setProperty('--open-height', 'auto');
}
// eslint-disable-next-line no-void
void getComputedStyle(this).maxHeight; // Force style calculation! Without this the animation doesn't work
this.toggleAttribute('open', val);
if (!this.dispatchEvent(new CustomEvent('toggle', { bubbles: false, cancelable: true }))) {
// Revert
this.toggleAttribute('open', open);
return;
}
if (!val && this.ownerDocument?.activeElement && this.contains(this.ownerDocument?.activeElement)) {
this.ownerDocument.activeElement.blur();
}
}
get attachment() {
return this._attachment;
}
set attachment(attachment) {
this._attachment = attachment;
this.setAttribute('attachment-id', attachment.id);
}
get attachmentTitle() {
return this._attachment.getField('title');
}
get empty() {
return !this._attachment || !this._attachment.numAnnotations();
}
get contextRow() {
return this.classList.contains('context');
}
set contextRow(val) {
this.classList.toggle('context', !!val);
}
init() {
this._head = this.querySelector('.head');
this._head.addEventListener('click', this._handleClick);
this._head.addEventListener('keydown', this._handleKeyDown);
this._label = this.querySelector('.label');
this._body = this.querySelector('.body');
this.open = false;
this.render();
}
_handleClick = (event) => {
if (event.target.closest('.clicky-item')) {
let win = Zotero.getMainWindow();
if (win) {
win.ZoteroPane.selectItem(this._attachment.id);
win.Zotero_Tabs.select('zotero-pane');
win.focus();
}
return;
}
this.open = !this.open;
};
_handleKeyDown = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
this.open = !this.open;
event.preventDefault();
}
};
render() {
if (!this.initialized) return;
this.querySelector('.icon').replaceWith(getCSSItemTypeIcon(this._attachment.getItemTypeIconName()));
this._label.textContent = this._attachment.getField('title');
this._body.replaceChildren();
for (let annotation of this._attachment.getAnnotations()) {
let row = document.createXULElement('annotation-row');
row.annotation = annotation;
this._body.append(row);
}
if (!this._listenerAdded) {
this._body.addEventListener('transitionend', () => {
this.style.setProperty('--open-height', 'auto');
});
this._listenerAdded = true;
}
this._head.setAttribute('aria-expanded', this.open);
this.toggleAttribute('empty', this.empty);
}
}
customElements.define('attachment-row', AttachmentRow);
}

View file

@ -0,0 +1,164 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2023 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 AttachmentsBox extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-attachments" data-pane="attachments" show-add="true">
<html:div class="body">
</html:div>
</collapsible-section>
`);
_item = null;
_mode = null;
_inTrash = false;
get item() {
return this._item;
}
set item(item) {
if (this._item === item) {
return;
}
this._item = item;
this._body.replaceChildren();
if (item) {
for (let attachment of Zotero.Items.get(item.getAttachments())) {
this.addRow(attachment);
}
this.updateCount();
}
}
get mode() {
return this._mode;
}
set mode(mode) {
this._mode = mode;
}
get inTrash() {
return this._inTrash;
}
set inTrash(inTrash) {
this._inTrash = inTrash;
for (let row of this._body.children) {
row.contextRow = this._isContext(row.attachment);
}
this.updateCount();
}
init() {
this._section = this.querySelector('collapsible-section');
this._section.addEventListener('add', this._handleAdd);
this._body = this.querySelector('.body');
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'attachmentsBox');
}
destroy() {
Zotero.Notifier.unregisterObserver(this._notifierID);
}
notify(action, type, ids) {
if (!this._item) return;
let itemAttachmentIDs = this._item.getAttachments(true);
let attachments = Zotero.Items.get(ids.filter(id => itemAttachmentIDs.includes(id)));
if (action == 'add') {
for (let attachment of attachments) {
this.addRow(attachment);
}
}
else if (action == 'modify') {
for (let attachment of attachments) {
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
let open = false;
if (row) {
open = row.open;
row.remove();
}
this.addRow(attachment).open = open;
}
}
else if (action == 'delete') {
for (let attachment of attachments) {
let row = this.querySelector(`attachment-row[attachment-id="${attachment.id}"]`);
if (row) {
row.remove();
}
}
}
this.updateCount();
}
addRow(attachment) {
if (attachment.deleted && !this._inTrash) return;
let row = document.createXULElement('attachment-row');
row.attachment = attachment;
row.contextRow = this._isContext(attachment);
let inserted = false;
for (let existingRow of this._body.children) {
if (Zotero.localeCompare(row.attachmentTitle, existingRow.attachmentTitle) < 0) {
continue;
}
existingRow.before(row);
inserted = true;
break;
}
if (!inserted) {
this._body.append(row);
}
return row;
}
updateCount() {
let count = this._item.numAttachments(this._inTrash);
this._section.setCount(count);
}
_handleAdd = () => {
ZoteroPane.addAttachmentFromDialog(false, this._item.id);
this._section.empty = false;
this._section.open = true;
};
_isContext(attachment) {
return this._inTrash && !this._item.deleted && !attachment.deleted;
}
}
customElements.define("attachments-box", AttachmentsBox);
}

View file

@ -50,8 +50,8 @@ class XULElementBase extends XULElement {
document.l10n.connectRoot(this.shadowRoot);
}
this.init();
this.initialized = true;
this.init();
}
disconnectedCallback() {

View file

@ -36,13 +36,16 @@
_listenerAdded = false;
get open() {
if (this.empty) {
return false;
}
return this.hasAttribute('open');
}
set open(val) {
val = !!val;
let open = this.open;
if (open === val) return;
if (open === val || this.empty) return;
this.render();
let openHeight = this._head?.nextSibling?.scrollHeight;
if (openHeight) {
@ -58,11 +61,28 @@
if (!this.dispatchEvent(new CustomEvent('toggle', { bubbles: false, cancelable: true }))) {
// Revert
this.toggleAttribute('open', open);
return;
}
if (!val && this.ownerDocument?.activeElement && this.contains(this.ownerDocument?.activeElement)) {
this.ownerDocument.activeElement.blur();
}
this._saveOpenState();
}
get empty() {
return this.hasAttribute('empty');
}
set empty(val) {
this.toggleAttribute('empty', !!val);
}
setCount(count) {
this.setAttribute('data-l10n-args', JSON.stringify({ count }));
this.empty = !count;
}
get label() {
return this.getAttribute('label');
}
@ -80,7 +100,7 @@
}
static get observedAttributes() {
return ['open', 'label', 'show-add'];
return ['open', 'empty', 'label', 'show-add'];
}
attributeChangedCallback() {
@ -92,7 +112,6 @@
throw new Error('data-pane is required');
}
this._restoreOpenState();
this.tabIndex = 0;
this._head = document.createElement('div');
@ -108,8 +127,7 @@
this._addButton = document.createXULElement('toolbarbutton');
this._addButton.className = 'add';
this._addButton.addEventListener('command', (event) => {
// TODO: Is this the best approach?
this._head.nextSibling?.dispatchEvent(new CustomEvent('add', { ...event, bubbles: true }));
this.dispatchEvent(new CustomEvent('add', { ...event, bubbles: false }));
});
this._head.append(this._addButton);
@ -118,9 +136,14 @@
this._head.append(twisty);
this.prepend(this._head);
this._restoreOpenState();
this.render();
this._notifierID = Zotero.Prefs.registerObserver(`panes.${this.dataset.pane}.open`, this._restoreOpenState.bind(this));
if (this.hasAttribute('data-l10n-id') && !this.hasAttribute('data-l10n-args')) {
this.setAttribute('data-l10n-args', JSON.stringify({ count: 0 }));
}
}
destroy() {

View file

@ -27,13 +27,9 @@
{
class EditableText extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:textarea rows="1" />
`);
_input;
_textarea;
static observedAttributes = ['multiline', 'readonly', 'label'];
static observedAttributes = ['multiline', 'readonly', 'placeholder', 'label', 'aria-label', 'value'];
get multiline() {
return this.hasAttribute('multiline');
@ -50,45 +46,74 @@
set readOnly(readOnly) {
this.toggleAttribute('readonly', readOnly);
}
// Fluent won't set placeholder on an editable-text for some reason, so we use the label property to store
// the placeholder that will be set on the child <textarea> or <input>
get placeholder() {
return this._textarea.placeholder;
return this.label;
}
set placeholder(placeholder) {
this._textarea.placeholder = placeholder;
this.label = placeholder;
}
// Fluent won't set placeholder on an editable-text for some reason, so we use the label property to store
// the placeholder that will be set on the child <textarea>
get label() {
return this.getAttribute('label');
return this.getAttribute('label') || '';
}
set label(label) {
this.setAttribute('label', label);
this.setAttribute('label', label || '');
}
get ariaLabel() {
return this._textarea.getAttribute('aria-label');
return this.getAttribute('aria-label') || '';
}
set ariaLabel(ariaLabel) {
this._textarea.setAttribute('aria-label', ariaLabel);
this.setAttribute('aria-label', ariaLabel);
}
get value() {
return this._textarea.value;
return this.getAttribute('value') || '';
}
set value(value) {
this._textarea.value = value;
this.dataset.value = value;
this.render();
this.setAttribute('value', value || '');
}
get initialValue() {
return this._textarea.dataset.initialValue;
return this._input?.dataset.initialValue || '';
}
set initialValue(initialValue) {
this._input.dataset.initialValue = initialValue || '';
}
get autocomplete() {
let val = this.getAttribute('autocomplete');
try {
let props = JSON.parse(val);
if (typeof props === 'object') {
return props;
}
}
catch (e) {
// Ignore
}
return null;
}
set autocomplete(val) {
if (val) {
this.setAttribute('autocomplete', JSON.stringify(val));
}
else {
this.removeAttribute('autocomplete');
}
}
get ref() {
return this._input;
}
attributeChangedCallback() {
@ -96,46 +121,99 @@
}
init() {
this._textarea = this.querySelector('textarea');
this._textarea.addEventListener('input', () => {
if (!this.multiline) {
this._textarea.value = this._textarea.value.replace(/\n/g, ' ');
}
this.dataset.value = this._textarea.value;
});
this._textarea.addEventListener('focus', () => {
this._textarea.dataset.initialValue = this._textarea.value;
});
this._textarea.addEventListener('blur', () => {
delete this._textarea.dataset.initialValue;
});
this._textarea.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
if (!this.multiline || event.shiftKey) {
event.preventDefault();
this._textarea.blur();
}
}
else if (event.key === 'Escape') {
this._textarea.value = this._textarea.dataset.initialValue;
this._textarea.blur();
}
});
this.render();
}
render() {
if (!this._textarea) return;
this._textarea.readOnly = this.readOnly;
this._textarea.placeholder = this.label;
let autocompleteParams = this.autocomplete;
let autocompleteEnabled = !this.multiline && !!autocompleteParams;
if (!this._input || autocompleteEnabled !== (this._input.constructor.name === 'AutocompleteInput')) {
let input;
if (autocompleteEnabled) {
input = document.createElement('input', { is: 'autocomplete-input' });
input.type = 'autocomplete';
}
else {
input = document.createElement('textarea');
input.rows = 1;
}
input.classList.add('input');
let handleInput = () => {
if (!this.multiline) {
this._input.value = this._input.value.replace(/\n/g, ' ');
}
this.value = this._input.value;
};
let handleChange = () => {
this.value = this._input.value;
};
input.addEventListener('input', handleInput);
input.addEventListener('change', handleChange);
input.addEventListener('focus', () => {
this.dispatchEvent(new CustomEvent('focus'));
this._input.dataset.initialValue = this._input.value;
});
input.addEventListener('blur', () => {
this.dispatchEvent(new CustomEvent('blur'));
delete this._input.dataset.initialValue;
});
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
if (this.multiline === event.shiftKey) {
event.preventDefault();
this._input.blur();
}
}
else if (event.key === 'Escape') {
this._input.value = this._input.dataset.initialValue;
this._input.blur();
}
});
let focused = false;
let selectionStart = this._input?.selectionStart;
let selectionEnd = this._input?.selectionEnd;
let selectionDirection = this._input?.selectionDirection;
if (this._input && document.activeElement === this._input) {
focused = true;
input.dataset.initialValue = this._input?.dataset.initialValue;
}
if (this._input) {
this._input.replaceWith(input);
}
else {
this.append(input);
}
this._input = input;
if (focused) {
this._input.focus();
}
if (selectionStart !== undefined && selectionEnd !== undefined) {
this._input.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
}
}
this._input.readOnly = this.readOnly;
this._input.placeholder = this.label;
this._input.setAttribute('aria-label', this.ariaLabel);
this._input.value = this.value;
if (autocompleteEnabled) {
this._input.setAttribute('autocomplete', 'on');
this._input.setAttribute('autocompletepopup', autocompleteParams.popup || '');
this._input.setAttribute('autocompletesearch', autocompleteParams.search || '');
delete autocompleteParams.popup;
delete autocompleteParams.search;
Object.assign(this._input, autocompleteParams);
}
}
focus() {
this._textarea.focus();
focus(options) {
this._input?.focus(options);
}
blur() {
this._textarea.blur();
this._input?.blur();
}
}
customElements.define("editable-text", EditableText);

View file

@ -57,42 +57,46 @@
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-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"/>
<collapsible-section data-l10n-id="section-info" data-pane="info">
<html:div class="body">
<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-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>
<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>
</html:div>
</collapsible-section>
`, ['chrome://zotero/locale/zotero.dtd']);
}
@ -960,8 +964,7 @@
td.appendChild(toggleButton);
// Minus (-) button
var removeButton = document.createElement('button');
removeButton.textContent = "-";
var removeButton = document.createXULElement('toolbarbutton');
removeButton.setAttribute("class", "zotero-clicky zotero-clicky-minus zotero-focusable");
removeButton.setAttribute('ztabindex', tabindex + 4);
removeButton.setAttribute('aria-label', Zotero.getString('general.delete'));
@ -977,8 +980,7 @@
td.appendChild(removeButton);
// Plus (+) button
var addButton = document.createElement('button');
addButton.textContent = "+";
var addButton = document.createXULElement('toolbarbutton');
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

View file

@ -104,6 +104,11 @@
_updateSelectedPane() {
let topPane = null;
let containerBoundingRect = this.container.getBoundingClientRect();
// If not initialized with content, just select the first pane
if (containerBoundingRect.height == 0) {
this.selectedPane = 'info';
return;
}
for (let box of this._getPanes()) {
// Allow a little padding to deal with floating-point imprecision
if (box.getBoundingClientRect().top > containerBoundingRect.top + 5) {

View file

@ -28,56 +28,32 @@
import { getCSSItemTypeIcon } from 'components/icons';
{
class NotesBox extends XULElement {
class NotesBox extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-notes" data-pane="notes">
<html:div class="body"/>
</collapsible-section>
`);
constructor() {
super();
this._mode = 'view';
this._item = null;
this._destroyed = false;
this._noteIDs = [];
this.content = MozXULElement.parseXULToFragment(`
<box flex="1" style="display: flex" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<div style="flex-grow: 1" xmlns="http://www.w3.org/1999/xhtml">
<div class="header">
<label id="notes-num"/>
<button id="notes-add">&zotero.item.add;</button>
</div>
<div id="notes-grid" class="grid"/>
</div>
</box>
`, ['chrome://zotero/locale/zotero.dtd']);
}
connectedCallback() {
this._destroyed = false;
window.addEventListener("unload", this.destroy);
let content = document.importNode(this.content, true);
this.append(content);
this._id('notes-add').addEventListener('click', this._handleAdd);
this.addEventListener('add', this._handleAdd);
init() {
this._section = this.querySelector('collapsible-section');
this._section.addEventListener('add', this._handleAdd);
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'notesBox');
}
destroy() {
if (this._destroyed) {
return;
}
window.removeEventListener("unload", this.destroy);
this._destroyed = true;
this._section = null;
Zotero.Notifier.unregisterObserver(this._notifierID);
}
disconnectedCallback() {
this.replaceChildren();
this.destroy();
}
get mode() {
return this._mode;
}
@ -118,38 +94,45 @@ import { getCSSItemTypeIcon } from 'components/icons';
}
this._noteIDs = this._item.getNotes();
this._id('notes-add').hidden = this._mode != 'edit';
let grid = this._id('notes-grid');
grid.replaceChildren();
let body = this.querySelector('.body');
body.replaceChildren();
let notes = Zotero.Items.get(this._item.getNotes());
for (let item of notes) {
let id = item.id;
let icon = getCSSItemTypeIcon(item.getItemTypeIconName());
let label = document.createElement("label");
label.append(item.getDisplayTitle());
let row = document.createElement('div');
row.className = 'row';
let icon = getCSSItemTypeIcon('note');
let label = document.createElement("span");
label.className = 'label';
label.append(this._TODO_EXTRACT_noteToTitle(item.getNote(), {
maxLength: 0
}));
let box = document.createElement('div');
box.addEventListener('click', () => this._handleShowItem(id));
box.className = 'box zotero-clicky';
box.appendChild(icon);
box.appendChild(label);
box.className = 'box';
box.append(icon, label);
grid.append(box);
row.append(box);
if (this._mode == 'edit') {
let remove = document.createElement("label");
remove.addEventListener('click', () => this._handleRemove(id));
let remove = document.createXULElement("toolbarbutton");
remove.addEventListener('command', () => this._handleRemove(id));
remove.className = 'zotero-clicky zotero-clicky-minus';
remove.append('-');
grid.append(remove);
row.append(remove);
}
body.append(row);
}
let num = this._noteIDs.length;
this._id('notes-num').replaceChildren(Zotero.getString('pane.item.notes.count', num, num));
let count = this._noteIDs.length;
this._section.showAdd = this._mode == 'edit';
this._section.setCount(count);
}
_handleAdd = (event) => {
@ -170,6 +153,56 @@ import { getCSSItemTypeIcon } from 'components/icons';
_id(id) {
return this.querySelector(`#${id}`);
}
/**
* TODO: Extract this back to utilities_item.js when merging
* Return first line (or first maxLength characters) of note content
*
* @param {String} text
* @param {Object} [options]
* @param {Boolean} [options.stopAtLineBreak] - Stop at <br/> instead of converting to space
* @param {Number} [options.maxLength] - Defaults to 120. If set to 0, no limit is applied.
* @return {String}
*/
_TODO_EXTRACT_noteToTitle(text, options = {}) {
var maxLength = options.maxLength;
if (maxLength === undefined) {
maxLength = 120;
}
else if (maxLength === 0) {
maxLength = Infinity;
}
var origText = text;
text = text.trim();
// Add line breaks after block elements
text = text.replace(/(<\/(h\d|p|div)+>)/g, '$1\n');
if (options.stopAtLineBreak) {
text = text.replace(/<br\s*\/?>/g, '\n');
}
else {
text = text.replace(/<br\s*\/?>/g, ' ');
}
text = Zotero.Utilities.unescapeHTML(text);
// If first line is just an opening HTML tag, remove it
//
// Example:
//
// <blockquote>
// <p>Foo</p>
// </blockquote>
if (/^<[^>\n]+[^\/]>\n/.test(origText)) {
text = text.trim();
}
var t = text.substring(0, maxLength);
var ln = t.indexOf("\n");
if (ln > -1 && ln < maxLength) {
t = t.substring(0, ln);
}
return t;
}
}
customElements.define("notes-box", NotesBox);
}

View file

@ -128,8 +128,10 @@
}
async blurOpenField() {
this.titleField.blur();
await this.save();
if (this.titleField?.matches(':focus-within')) {
this.titleField.blur();
await this.save();
}
}
render() {
@ -140,8 +142,9 @@
this._titleFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(this.item.itemTypeID, 'title');
let title = this.item.getField(this._titleFieldID);
if (this.titleField.initialValue !== title) {
if (!this.titleField.initialValue || this.titleField.initialValue !== title) {
this.titleField.value = title;
this.titleField.initialValue = '';
}
this.titleField.readOnly = this._mode == 'view';
this.titleField.placeholder = Zotero.ItemFields.getLocalizedString(this._titleFieldID);

View file

@ -25,56 +25,34 @@
"use strict";
import { getCSSItemTypeIcon } from 'components/icons';
{
class RelatedBox extends XULElement {
class RelatedBox extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<collapsible-section data-l10n-id="section-related" data-pane="related">
<html:div class="body"/>
</collapsible-section>
`);
constructor() {
super();
this._mode = 'view';
this._item = null;
this._destroyed = false;
this.content = MozXULElement.parseXULToFragment(`
<box flex="1" style="display: flex" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<div style="flex-grow: 1" xmlns="http://www.w3.org/1999/xhtml">
<div class="header">
<label id="related-num"/>
<button id="related-add">&zotero.item.add;</button>
</div>
<div id="related-grid" class="grid"/>
</div>
</box>
`, ['chrome://zotero/locale/zotero.dtd']);
}
connectedCallback() {
this._destroyed = false;
window.addEventListener("unload", this.destroy);
let content = document.importNode(this.content, true);
this.append(content);
this._id('related-add').addEventListener('click', this.add);
this.addEventListener('add', this.add);
init() {
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'relatedbox');
this._section = this.querySelector('collapsible-section');
this._section.addEventListener('add', this.add);
}
destroy() {
if (this._destroyed) {
return;
}
window.removeEventListener("unload", this.destroy);
this._destroyed = true;
Zotero.Notifier.unregisterObserver(this._notifierID);
this._section = null;
}
disconnectedCallback() {
this.replaceChildren();
this.destroy();
}
get mode() {
return this._mode;
}
@ -126,10 +104,8 @@
}
refresh() {
this._id('related-add').hidden = this._mode != 'edit';
let grid = this._id('related-grid');
grid.replaceChildren();
let body = this.querySelector('.body');
body.replaceChildren();
if (this._item) {
let relatedKeys = this._item.relatedItems;
@ -144,33 +120,42 @@
continue;
}
let id = relatedItem.id;
let icon = document.createElement("img");
icon.src = relatedItem.getImageSrc();
let label = document.createElement("label");
let row = document.createElement('div');
row.className = 'row';
let icon = getCSSItemTypeIcon(relatedItem.getItemTypeIconName());
let label = document.createElement("span");
label.className = 'label';
label.append(relatedItem.getDisplayTitle());
let box = document.createElement('div');
box.addEventListener('click', () => this._handleShowItem(id));
box.className = 'box zotero-clicky';
box.className = 'box';
box.appendChild(icon);
box.appendChild(label);
grid.append(box);
row.append(box);
if (this._mode == 'edit') {
let remove = document.createElement("label");
remove.addEventListener('click', () => this._handleRemove(id));
let remove = document.createXULElement("toolbarbutton");
remove.addEventListener('command', () => this._handleRemove(id));
remove.className = 'zotero-clicky zotero-clicky-minus';
remove.append('-');
grid.append(remove);
row.append(remove);
}
body.append(row);
}
this._updateCount();
}
this._section.showAdd = this._mode == 'edit';
}
add = async () => {
this._section.empty = false;
this._section.open = true;
let io = { dataIn: null, dataOut: null, deferred: Zotero.Promise.defer(), itemTreeID: 'related-box-select-item-dialog' };
window.openDialog('chrome://zotero/content/selectItemsDialog.xhtml', '',
'chrome,dialog=no,centerscreen,resizable=yes', io);
@ -233,19 +218,7 @@
_updateCount() {
let count = this._item.relatedItems.length;
let str = 'pane.item.related.count.';
switch (count) {
case 0:
str += 'zero';
break;
case 1:
str += 'singular';
break;
default:
str += 'plural';
break;
}
this._id('related-num').replaceChildren(Zotero.getString(str, [count]));
this._section.setCount(count);
}
_id(id) {

View file

@ -33,7 +33,6 @@
this.count = 0;
this.clickHandler = null;
this._lastTabIndex = false;
this._tabDirection = null;
this._tagColors = [];
this._notifierID = null;
@ -41,34 +40,30 @@
this._item = null;
this.content = MozXULElement.parseXULToFragment(`
<box flex="1" tooltip="html-tooltip" style="display: flex" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<div id="tags-box" style="flex-grow: 1" xmlns="http://www.w3.org/1999/xhtml">
<div class="tags-box-header">
<label id="count"/>
<button id="tags-box-add-button">&zotero.item.add;</button>
</div>
<ul id="rows" class="tags-box-list"/>
</div>
</box>
<popupset xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<tooltip id="html-tooltip" page="true"/>
<menupopup id="tags-context-menu">
<menuitem id="remove-all-item-tags" label="&zotero.item.tags.removeAll;"/>
</menupopup>
<!-- Note: autocomplete-input can create this panel by itself, but it appears
in the top DOM and is behind the tags box popup -->
<panel
is="autocomplete-richlistbox-popup"
type="autocomplete-richlistbox"
id="PopupAutoComplete"
role="group"
noautofocus="true"
hidden="true"
overflowpadding="4"
norolluponanchor="true"
nomaxresults="true"
/>
</popupset>
<collapsible-section data-l10n-id="section-tags" data-pane="tags">
<html:div class="body">
<html:div id="rows" class="tags-box-list"/>
<popupset>
<tooltip id="html-tooltip" page="true"/>
<menupopup id="tags-context-menu">
<menuitem id="remove-all-item-tags" label="&zotero.item.tags.removeAll;"/>
</menupopup>
<!-- Note: autocomplete-input can create this panel by itself, but it appears
in the top DOM and is behind the tags box popup -->
<panel
is="autocomplete-richlistbox-popup"
type="autocomplete-richlistbox"
id="PopupAutoComplete"
role="group"
noautofocus="true"
hidden="true"
overflowpadding="4"
norolluponanchor="true"
nomaxresults="true"
/>
</popupset>
</html:div>
</collapsible-section>
`, ['chrome://zotero/locale/zotero.dtd']);
}
@ -79,23 +74,23 @@
let content = document.importNode(this.content, true);
this.append(content);
this._id("tags-box-add-button").addEventListener('click', this._handleAddButtonClick);
this.addEventListener('add', this._handleAddButtonClick);
this._id("tags-box-add-button").addEventListener('keydown', this._handleAddButtonKeyDown);
this._id('tags-box').addEventListener('click', (event) => {
if (event.target.id == 'tags-box') {
this._section = this.querySelector('collapsible-section');
this._section.addEventListener('add', this._handleAddButtonClick);
this.addEventListener('click', (event) => {
if (event.target === this) {
this.blurOpenField();
}
});
let removeAllItemTags = this._id('remove-all-item-tags');
this._id('remove-all-item-tags').addEventListener('command', this.removeAll);
this._id('tags-box').addEventListener('contextmenu', (event) => {
this.addEventListener('contextmenu', (event) => {
removeAllItemTags.disabled = !this.count;
this._id('tags-context-menu').openPopupAtScreen(event.screenX, event.screenY, true);
});
this._notifierID = Zotero.Notifier.registerObserver(this, ['item-tag', 'setting'], 'tagsBox');
// Register our observer with priority 101 (after Zotero.Tags) so we get updated tag colors
this._notifierID = Zotero.Notifier.registerObserver(this, ['item-tag', 'setting'], 'tagsBox', 101);
}
destroy() {
@ -105,6 +100,7 @@
window.removeEventListener("unload", this.destroy);
this._destroyed = true;
this._section = null;
Zotero.Notifier.unregisterObserver(this._notifierID);
}
@ -130,8 +126,6 @@
case 'edit':
this.clickable = true;
this.editable = true;
this.clickHandler = this.showEditor;
this.blurHandler = this.hideEditor;
break;
default:
@ -151,7 +145,6 @@
return;
}
this._item = val;
this._lastTabIndex = false;
this.reload();
}
@ -175,20 +168,7 @@
let tagType = data.type;
if (event == 'add') {
var newTabIndex = this.add(tagName, tagType);
if (newTabIndex == -1) {
return;
}
if (this._tabDirection == -1) {
if (this._lastTabIndex > newTabIndex) {
this._lastTabIndex++;
}
}
else if (this._tabDirection == 1) {
if (this._lastTabIndex > newTabIndex) {
this._lastTabIndex++;
}
}
this.add(tagName, tagType);
}
else if (event == 'modify') {
let oldTagName = data.old.tag;
@ -196,20 +176,7 @@
this.add(tagName, tagType);
}
else if (event == 'remove') {
var oldTabIndex = this.remove(tagName);
if (oldTabIndex == -1) {
return;
}
if (this._tabDirection == -1) {
if (this._lastTabIndex > oldTabIndex) {
this._lastTabIndex--;
}
}
else if (this._tabDirection == 1) {
if (this._lastTabIndex >= oldTabIndex) {
this._lastTabIndex--;
}
}
this.remove(tagName);
}
}
@ -229,9 +196,9 @@
// Cancel field focusing while we're updating
this._reloading = true;
this._id("tags-box-add-button").hidden = !this.editable;
this._tagColors = Zotero.Tags.getColors(this.item.libraryID);
let focusedTag = this._id('rows').querySelector('editable-text:focus')?.value;
let tagRows = this._id('rows');
tagRows.replaceChildren();
@ -240,15 +207,32 @@
// Sort tags alphabetically
var collation = Zotero.getLocaleCollation();
tags.sort((a, b) => collation.compareString(1, a.tag, b.tag));
tags.sort((a, b) => {
let aTag = a.tag;
let bTag = b.tag;
let aHasColor = this._tagColors.has(aTag);
let bHasColor = this._tagColors.has(bTag);
// Sort colored tags to the top
if (aHasColor && !bHasColor) {
return -1;
}
if (!aHasColor && bHasColor) {
return 1;
}
return collation.compareString(1, aTag, bTag);
});
for (let i = 0; i < tags.length; i++) {
this.addDynamicRow(tags[i], i + 1);
}
this.updateCount(tags.length);
this._section.showAdd = this.editable;
this._reloading = false;
this._focusField();
if (focusedTag) {
this._id('rows').querySelector(`[value="${CSS.escape(focusedTag)}"]`)?.focus();
}
}
addDynamicRow(tagData, tabindex, skipAppend) {
@ -259,7 +243,7 @@
tabindex = this._id('rows').childNodes.length + 1;
}
var icon = document.createElement("img");
var icon = document.createElement("div");
icon.className = "zotero-box-icon";
// DEBUG: Why won't just this.nextSibling.blur() work?
@ -270,13 +254,18 @@
var label = this.createValueElement(name, tabindex);
if (this.editable) {
var remove = document.createXULElement("label");
var remove = document.createXULElement("toolbarbutton");
remove.setAttribute('value', '-');
remove.setAttribute('class', 'zotero-clicky zotero-clicky-minus');
remove.setAttribute('tabindex', -1);
}
var row = document.createElement("li");
var row = document.createElement("div");
row.classList.add('row');
if (name && this._tagColors.has(name)) {
row.classList.add('has-color');
row.style.setProperty('--tag-color', this._tagColors.get(name).color);
}
if (isNew) {
row.setAttribute('isNew', true);
}
@ -302,7 +291,6 @@
var tagName = tagData ? tagData.tag : "";
var tagType = (tagData && tagData.type) ? tagData.type : 0;
var icon = row.firstChild;
if (this.editable) {
var remove = row.lastChild;
}
@ -312,21 +300,15 @@
row.setAttribute('tagType', tagType);
// Icon
var iconFile = 'tag';
if (!tagData || tagType == 0) {
icon.setAttribute('title', Zotero.getString('pane.item.tags.icon.user'));
}
else if (tagType == 1) {
iconFile += '-automatic';
icon.setAttribute('title', Zotero.getString('pane.item.tags.icon.automatic'));
}
icon.setAttribute('src', `chrome://zotero/skin/${iconFile}${Zotero.hiDPISuffix}.png`);
let icon = row.firstChild;
icon.title = tagType == 0
? Zotero.getString('pane.item.tags.icon.user')
: Zotero.getString('pane.item.tags.icon.automatic');
// "-" button
if (this.editable) {
remove.setAttribute('disabled', false);
remove.addEventListener('click', async (event) => {
this._lastTabIndex = false;
if (tagData) {
let item = this.item;
this.remove(tagName);
@ -354,218 +336,96 @@
}
createValueElement(valueText, tabindex) {
var valueElement = document.createXULElement("label");
createValueElement(valueText) {
var valueElement = document.createXULElement("editable-text");
valueElement.setAttribute('fieldname', 'tag');
valueElement.setAttribute('flex', 1);
valueElement.className = 'zotero-box-label';
if (this.clickable) {
if (tabindex) {
valueElement.setAttribute('ztabindex', tabindex);
}
valueElement.addEventListener('click', (event) => {
/* Skip right-click on Windows */
if (event.button) {
return;
}
this.clickHandler(event.target, 1, valueText);
}, false);
valueElement.className += ' zotero-clicky';
}
var firstSpace;
if (typeof valueText == 'string') {
firstSpace = valueText.indexOf(" ");
}
// 29 == arbitrary length at which to chop uninterrupted text
if ((firstSpace == -1 && valueText.length > 29) || firstSpace > 29) {
valueElement.setAttribute('crop', 'end');
valueElement.setAttribute('value', valueText);
}
else {
// Wrap to multiple lines
valueElement.appendChild(document.createTextNode(valueText));
}
// Tag color
var colorData = this._tagColors.get(valueText);
if (colorData) {
valueElement.style.color = colorData.color;
valueElement.style.fontWeight = 'bold';
}
valueElement.readOnly = !this.editable;
valueElement.value = valueText;
let params = {
fieldName: 'tag',
libraryID: this._item.libraryID,
itemID: this._item.id || ''
};
valueElement.autocomplete = {
ignoreBlurWhileSearching: true,
popup: 'PopupAutoComplete',
search: 'zotero',
searchParam: JSON.stringify(params),
completeSelectedIndex: true
};
valueElement.addEventListener('blur', this.saveTag);
valueElement.addEventListener('keydown', this.handleKeyDown);
valueElement.addEventListener('paste', this.handlePaste);
return valueElement;
}
showEditor(elem, rows, value) {
// Blur any active fields
/*
if (this._dynamicFields) {
this._dynamicFields.focus();
}
*/
Zotero.debug('Showing editor');
var fieldName = 'tag';
var tabindex = elem.getAttribute('ztabindex');
var itemID = this._item.id;
var t = document.createElement(rows > 1 ? 'textarea' : 'input', { is: 'shadow-autocomplete-input' });
t.setAttribute('class', 'editable');
t.setAttribute('value', value);
t.setAttribute('fieldname', fieldName);
t.setAttribute('ztabindex', tabindex);
t.setAttribute('ignoreblurwhilesearching', 'true');
t.setAttribute('autocompletepopup', 'PopupAutoComplete');
// Multi-line
if (rows > 1) {
t.setAttribute('rows', rows);
}
// Add auto-complete
else {
t.setAttribute('type', 'autocomplete');
t.setAttribute('autocompletesearch', 'zotero');
let params = {
fieldName: fieldName,
libraryID: this.item.libraryID
};
params.itemID = itemID ? itemID : '';
t.setAttribute(
'autocompletesearchparam', JSON.stringify(params)
);
t.setAttribute('completeselectedindex', true);
}
var box = elem.parentNode;
box.replaceChild(t, elem);
t.addEventListener('blur', this.blurHandler);
t.addEventListener('keydown', this.handleKeyDown);
t.addEventListener('paste', this.handlePaste);
this._tabDirection = false;
this._lastTabIndex = tabindex;
// Prevent error when clicking between a changed field
// and another -- there's probably a better way
if (!t.select) {
return;
}
t.select();
return t;
}
handleKeyDown = async (event) => {
var target = event.target;
var focused = document.activeElement;
var target = event.currentTarget;
switch (event.keyCode) {
case event.DOM_VK_RETURN:
var multiline = target.parentNode.classList.contains('multiline');
var empty = target.value == "";
if (event.shiftKey) {
if (!multiline) {
var self = this;
setTimeout(function () {
var val = target.value;
if (val !== "") {
val += "\n";
}
self.makeMultiline(target, val, 6);
}, 0);
return false;
}
// Submit
}
else if (multiline) {
return true;
if (event.key === 'Enter') {
var multiline = target.multiline;
var empty = target.value == "";
if (event.shiftKey) {
if (!multiline) {
setTimeout(() => {
var val = target.value;
if (val !== "") {
val += "\n";
}
this.makeMultiline(target, val);
});
return;
}
// Submit
}
else if (multiline) {
return;
}
var row = target.closest('li');
let blurOnly = false;
event.preventDefault();
// If non-empty last row, only blur, because the open textbox will
// be cleared in hideEditor() and remain in place
if (row == row.parentNode.lastChild && !empty) {
blurOnly = true;
}
// If empty non-last row, refocus current row
else if (row != row.parentNode.lastChild && empty) {
var focusField = true;
}
// If non-empty non-last row, return focus to items pane
else {
var focusField = false;
this._lastTabIndex = false;
}
var row = target.parentElement;
// Not sure why this can happen, but if the event fires on an unmounted node, just ignore it
if (!row.parentElement) {
return;
}
let blurOnly = false;
let focusField = false;
await this.blurHandler(event);
// If non-empty last row, only blur, because the open textbox will
// be cleared in saveTag() and remain in place
if (row == row.parentNode.lastChild && !empty) {
blurOnly = true;
}
// If empty non-last row, refocus current row
else if (row != row.parentNode.lastChild && empty) {
focusField = row.nextElementSibling;
}
if (blurOnly) {
return false;
}
if (focusField) {
this._focusField();
}
// Return focus to items pane
else {
var tree = document.getElementById('zotero-items-tree');
if (tree) {
tree.focus();
}
}
await this.blurOpenField();
return false;
case event.DOM_VK_ESCAPE:
// Reset field to original value
target.value = target.getAttribute('value');
var tagsbox = focused.closest('.editable');
this._lastTabIndex = false;
await this.blurHandler(event);
if (tagsbox) {
tagsbox.closePopup();
}
// TODO: Return focus to items pane
if (blurOnly) {
return;
}
if (focusField) {
focusField.focus();
}
// Return focus to items pane
else {
var tree = document.getElementById('zotero-items-tree');
if (tree) {
tree.focus();
}
return false;
case event.DOM_VK_TAB:
// If already an empty last row, ignore forward tab
if (target.value == "" && !event.shiftKey) {
var row = Zotero.getAncestorByTagName(target, 'li');
if (row == row.parentNode.lastChild) {
return false;
}
}
this._tabDirection = event.shiftKey ? -1 : 1;
await this.blurHandler(event);
this._focusField();
return false;
}
}
return true;
};
// Intercept paste, check for newlines, and convert textbox
// to multiline if necessary
handlePaste = (event) => {
var textbox = event.target;
var textbox = event.currentTarget;
var str = event.clipboardData.getData('text');
var multiline = !!str.trim().match(/\n/);
@ -577,32 +437,21 @@
}
};
makeMultiline(textbox, value, rows) {
textbox.parentNode.classList.add('multiline');
// If rows not specified, use one more than lines in input
if (!rows) {
rows = value.match(/\n/g).length + 1;
}
textbox = this.showEditor(textbox, rows, textbox.getAttribute('value'));
textbox.value = value;
makeMultiline(editable, value) {
editable.multiline = true;
editable.value = value;
// Move cursor to end
textbox.selectionStart = value.length;
editable.ref.selectionStart = value.length;
}
hideEditor = async (event) => {
var textbox = event.target;
saveTag = async (event) => {
var textbox = event.currentTarget;
Zotero.debug('Hiding editor');
Zotero.debug('Saving tag');
var oldValue = textbox.getAttribute('value');
var oldValue = textbox.initialValue;
var value = textbox.value = textbox.value.trim();
var tagsbox = textbox.closest('.editable');
if (!tagsbox) {
Zotero.debug('Tagsbox not found', 1);
return;
}
var row = textbox.parentNode;
var isNew = row.getAttribute('isNew');
@ -613,9 +462,8 @@
return;
}
// If row hasn't changed, change back to label
// If row hasn't changed, we're done
if (oldValue == value) {
this.textboxToLabel(textbox);
return;
}
@ -628,9 +476,6 @@
// The existing textbox will be removed in notify()
this.removeRow(row);
this.add(value);
if (event.type != 'blur') {
this._focusField();
}
try {
this.item.replaceTag(oldValue, value);
await this.item.saveTx();
@ -644,9 +489,10 @@
// Existing tag cleared
else {
try {
let nextRowElem = row.nextElementSibling?.querySelector('editable-text');
this.removeRow(row);
if (event.type != 'blur') {
this._focusField();
if (event.type != 'change') {
nextRowElem?.focus();
}
this.item.removeTag(oldValue);
await this.item.saveTx();
@ -659,8 +505,6 @@
}
// Multiple tags
else if (tags.length > 1) {
var lastTag = row == row.parentNode.lastChild;
if (!isNew) {
// If old tag isn't in array, remove it
if (tags.indexOf(oldValue) == -1) {
@ -670,27 +514,24 @@
// immediately. This isn't strictly necessary, but it
// makes the transition nicer.
else {
textbox.value = textbox.getAttribute('value');
this.textboxToLabel(textbox);
textbox.value = textbox.initialValue;
textbox.blur();
}
}
tags.forEach(tag => this.item.addTag(tag));
await this.item.saveTx();
if (lastTag) {
this._lastTabIndex = this.item.getTags().length;
}
this.reload();
}
// Single tag at end
else {
if (event.type == 'blur') {
if (event.type == 'change') {
this.removeRow(row);
}
else {
textbox.value = '';
// We need a setTimeout here for some reason - why?
setTimeout(() => textbox.focus());
}
this.add(value);
this.item.addTag(value);
@ -705,31 +546,13 @@
};
newTag() {
var rowsElement = this._id('rows');
var rows = rowsElement.childNodes;
// Don't add new row if there already is one
if (rows.length && rows[rows.length - 1].querySelector('.editable')) {
return;
}
this._section.empty = false;
this._section.open = true;
var row = this.addDynamicRow();
// It needs relatively high delay to make focus-on-click work
setTimeout(() => {
row.firstChild.nextSibling.click();
}, 50);
row.querySelector('editable-text').focus();
return row;
}
textboxToLabel(textbox) {
var elem = this.createValueElement(
textbox.value, textbox.getAttribute('ztabindex')
);
var row = textbox.parentNode;
row.replaceChild(elem, textbox);
}
add(tagName, tagType) {
var rowsElement = this._id('rows');
var rows = rowsElement.childNodes;
@ -738,7 +561,7 @@
var row = false;
for (let i = 0; i < rows.length; i++) {
if (rows[i].getAttribute('tagName') === tagName) {
return rows[i].getAttribute('ztabindex');
return rows[i];
}
}
@ -746,90 +569,55 @@
tag: tagName,
type: tagType
};
var color = this._tagColors.has(tagName);
if (row) {
// Update row and label
this.updateRow(row, tagData);
var elem = this.createValueElement(tagName);
// Remove the old row, which we'll reinsert at the correct place
rowsElement.removeChild(row);
// Find the current label or textbox within the row
// and replace it with the new element -- this is used
// both when creating new rows and when hiding the
// entry textbox
var oldElem = row.getElementsByAttribute('fieldname', 'tag')[0];
row.replaceChild(elem, oldElem);
}
else {
// Create new row, but don't insert it
row = this.addDynamicRow(tagData, false, true);
var elem = row.getElementsByAttribute('fieldname', 'tag')[0];
}
// Create new row, but don't insert it
row = this.addDynamicRow(tagData, false, true);
var elem = row.getElementsByAttribute('fieldname', 'tag')[0];
// Move row to appropriate place, alphabetically
var collation = Zotero.getLocaleCollation();
var labels = rowsElement.getElementsByAttribute('fieldname', 'tag');
var tagEditables = rowsElement.getElementsByAttribute('fieldname', 'tag');
var inserted = false;
var newTabIndex = false;
for (var i = 0; i < labels.length; i++) {
let index = i + 1;
if (inserted) {
labels[i].setAttribute('ztabindex', index);
for (let editable of tagEditables) {
// Sort tags without colors below tags with colors
if (!color && this._tagColors.has(editable.value)
|| collation.compareString(1, tagName, editable.value) > 0) {
continue;
}
if (collation.compareString(1, tagName, labels[i].textContent) > 0
// Ignore textbox at end
&& labels[i].tagName != 'input') {
labels[i].setAttribute('ztabindex', index);
continue;
}
elem.setAttribute('ztabindex', index);
rowsElement.insertBefore(row, labels[i].parentNode);
newTabIndex = index;
rowsElement.insertBefore(row, editable.parentNode);
inserted = true;
break;
}
if (!inserted) {
newTabIndex = i + 1;
elem.setAttribute('ztabindex', newTabIndex);
rowsElement.appendChild(row);
}
this.updateCount(this.count + 1);
return newTabIndex;
return elem;
}
remove(tagName) {
var rowsElement = this._id('rows');
var rows = rowsElement.childNodes;
var oldTabIndex = -1;
for (var i = 0; i < rows.length; i++) {
let value = rows[i].getAttribute('tagName');
if (value === tagName) {
oldTabIndex = this.removeRow(rows[i]);
this.removeRow(rows[i]);
break;
}
}
return oldTabIndex;
}
// Remove the row and update tab indexes
removeRow(row) {
var origTabIndex = row.getElementsByAttribute('fieldname', 'tag')[0].getAttribute('ztabindex');
var origRow = row;
var i = origTabIndex;
while (row = row.nextSibling) {
let elem = row.getElementsByAttribute('fieldname', 'tag')[0];
elem.setAttribute('ztabindex', i++);
}
origRow.parentNode.removeChild(origRow);
this.updateCount(this.count - 1);
return origTabIndex;
}
removeAll = () => {
@ -854,7 +642,7 @@
}
}
this._id('count').replaceChildren(Zotero.getString('pane.item.tags.count', count, count));
this._section.setCount(count);
this.count = count;
}
@ -864,73 +652,6 @@
}
}
// Open the textbox for a particular label
//
// 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.)
_focusField() {
if (this._reloading) {
return;
}
if (this._lastTabIndex === false) {
return;
}
var maxIndex = this._id('rows').childNodes.length + 1;
var tabindex = parseInt(this._lastTabIndex);
var dir = this._tabDirection;
if (dir == 1) {
var nextIndex = tabindex + 1;
}
else if (dir == -1) {
if (tabindex == 1) {
// Focus Add button
this._id("tags-box-add-button").focus();
return false;
}
var nextIndex = tabindex - 1;
}
else {
var nextIndex = tabindex;
}
nextIndex = Math.min(nextIndex, maxIndex);
Zotero.debug('Looking for tabindex ' + nextIndex, 4);
var next = this.querySelector(`[ztabindex="${nextIndex}"]`);
if (next.length) {
next = next[0];
next.click();
}
else {
next = this.newTag();
next = next.firstChild.nextSibling;
}
if (!next) {
Components.utils.reportError('Next row not found');
return;
}
next.scrollIntoView();
}
_handleAddButtonKeyDown = (event) => {
if (event.keyCode != event.DOM_VK_TAB || event.shiftKey) {
return;
}
this._lastTabIndex = 0;
this._tabDirection = 1;
this._focusField();
event.preventDefault();
};
_handleAddButtonClick = async (event) => {
await this.blurOpenField();
this.newTag();
@ -941,15 +662,13 @@
}
async blurOpenField(stayOpen) {
this._lastTabIndex = false;
var textboxe = this.querySelector('.editable');
if (textboxe) {
await this.blurHandler({
target: textboxe,
// If coming from the Add button, pretend user pressed return
type: stayOpen ? 'keypress' : 'blur',
// DOM_VK_RETURN
keyCode: stayOpen ? 13 : undefined
var editable = this.querySelector('editable-text:focus-within');
if (editable) {
await this.saveTag({
currentTarget: editable,
// If coming from the Add button, pretend user pressed Enter
type: stayOpen ? 'keypress' : 'change',
key: stayOpen ? 'Enter' : undefined
});
}
}

View file

@ -239,8 +239,8 @@
<menupopup/>
</menulist>
<zoterosearchagefield id="value-date-age" class="value-date-age" hidden="true"/>
<label id="remove" class="zotero-clicky zotero-clicky-minus" value="-" onclick="this.closest('zoterosearchcondition').onRemoveClicked(event)"/>
<label id="add" class="zotero-clicky zotero-clicky-plus" value="+" onclick="this.closest('zoterosearchcondition').onAddClicked(event)"/>
<toolbarbutton id="remove" class="zotero-clicky zotero-clicky-minus" value="-" onclick="this.closest('zoterosearchcondition').onRemoveClicked(event)"/>
<toolbarbutton id="add" class="zotero-clicky zotero-clicky-plus" value="+" onclick="this.closest('zoterosearchcondition').onAddClicked(event)"/>
</html:div>
`, ['chrome://zotero/locale/zotero.dtd', 'chrome://zotero/locale/searchbox.dtd']);

View file

@ -25,7 +25,7 @@
var ZoteroItemPane = new function() {
var _container;
var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _tagsBox, _notesBox, _relatedBox, _boxes;
var _header, _sidenav, _scrollParent, _itemBox, _abstractBox, _attachmentsBox, _tagsBox, _notesBox, _relatedBox, _boxes;
var _deck;
var _lastItem;
var _selectedNoteID;
@ -43,9 +43,10 @@ var ZoteroItemPane = new function() {
_itemBox = document.getElementById('zotero-editpane-item-box');
_abstractBox = document.getElementById('zotero-editpane-abstract');
_notesBox = document.getElementById('zotero-editpane-notes');
_attachmentsBox = document.getElementById('zotero-editpane-attachments');
_tagsBox = document.getElementById('zotero-editpane-tags');
_relatedBox = document.getElementById('zotero-editpane-related');
_boxes = [_itemBox, _abstractBox, _notesBox, _tagsBox, _relatedBox];
_boxes = [_itemBox, _abstractBox, _notesBox, _attachmentsBox, _tagsBox, _relatedBox];
_deck = document.getElementById('zotero-item-pane-content');
@ -90,6 +91,7 @@ var ZoteroItemPane = new function() {
this.setTranslateButton();
}
let inTrash = ZoteroPane.collectionsView.selectedTreeRow && ZoteroPane.collectionsView.selectedTreeRow.isTrash();
for (let box of [_header, ..._boxes]) {
if (mode) {
box.mode = mode;
@ -103,6 +105,7 @@ var ZoteroItemPane = new function() {
}
box.item = item;
box.inTrash = inTrash;
}
_sidenav.selectedPane = pane;
@ -112,7 +115,7 @@ var ZoteroItemPane = new function() {
this.notify = Zotero.Promise.coroutine(function* (action, type, ids, extraData) {
if (action == 'refresh' && _lastItem) {
yield this.viewItem(_lastItem, null, 0);
yield this.viewItem(_lastItem, null, _sidenav.selectedPane);
}
});

View file

@ -724,38 +724,11 @@ var Zotero_Tabs = new function () {
document.getElementById("attachment-note-editor").focus();
return;
}
// Focusing on the last field in whichever tab is opened for
// regular items
const tabBox = document.getElementById("zotero-view-tabbox");
if (tabBox.selectedIndex === 0) {
const itembox = document.getElementById("zotero-editpane-item-box");
itembox.focusLastField();
}
else if (tabBox.selectedIndex === 1) {
const notes = document.getElementById("zotero-editpane-notes");
const nodes = notes.querySelectorAll("button");
const node = nodes[nodes.length - 1];
node.focus();
// TODO: the notes are currently inaccessible to the keyboard
}
else if (tabBox.selectedIndex === 2) {
const tagContainer = document.getElementById("tags-box-container");
const tags = tagContainer.querySelectorAll("#tags-box-add-button,.zotero-clicky");
const last = tags[tags.length - 1];
if (last.id === "tags-box-add-button") {
last.focus();
}
else {
last.click();
}
}
else if (tabBox.selectedIndex === 3) {
const related = tabBox.querySelector("relatedbox");
related.receiveKeyboardFocus("end");
}
else {
throw new Error("The selectedIndex should always be between 1 and 4");
}
// For regular items, focus the last field
// We do that by moving focus backwards from the element following the pane, because Services.focus doesn't
// support MOVEFOCUS_LAST on subtrees
Services.focus.moveFocus(window, document.getElementById('zotero-context-splitter'),
Services.focus.MOVEFOCUS_BACKWARD, 0);
}
};
};

View file

@ -1115,29 +1115,17 @@
<pane-header id="zotero-item-pane-header" />
<html:div id="zotero-view-item" class="zotero-view-item">
<collapsible-section data-l10n-id="section-info" data-pane="info">
<item-box id="zotero-editpane-item-box"/>
</collapsible-section>
<item-box id="zotero-editpane-item-box" data-pane="info"/>
<collapsible-section data-l10n-id="section-abstract" data-pane="abstract">
<abstract-box id="zotero-editpane-abstract" class="zotero-editpane-abstract"/>
</collapsible-section>
<abstract-box id="zotero-editpane-abstract" class="zotero-editpane-abstract" data-pane="abstract"/>
<collapsible-section data-l10n-id="section-attachments" data-pane="attachments" show-add="true">
<html:span>[placeholder for attachments section]</html:span>
</collapsible-section>
<attachments-box id="zotero-editpane-attachments" data-pane="attachments"/>
<collapsible-section data-l10n-id="section-notes" data-pane="notes" show-add="true">
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes"/>
</collapsible-section>
<notes-box id="zotero-editpane-notes" class="zotero-editpane-notes" data-pane="notes"/>
<collapsible-section data-l10n-id="section-tags" data-pane="tags" show-add="true">
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags"/>
</collapsible-section>
<tags-box id="zotero-editpane-tags" class="zotero-editpane-tags" data-pane="tags"/>
<collapsible-section data-l10n-id="section-related" data-pane="related" show-add="true">
<related-box id="zotero-editpane-related" class="zotero-editpane-related"/>
</collapsible-section>
<related-box id="zotero-editpane-related" class="zotero-editpane-related" data-pane="related"/>
</html:div>
</html:div>

View file

@ -231,13 +231,22 @@ section-info =
section-abstract =
.label = { pane-abstract }
section-attachments =
.label = { pane-attachments }
.label = { $count ->
[one] { $count } Attachment
*[other] { $count } Attachments
}
section-notes =
.label = { pane-notes }
.label = { $count ->
[one] { $count } Note
*[other] { $count } Notes
}
section-tags =
.label = { pane-tags }
.label = { $count ->
[one] { $count } Tag
*[other] { $count } Tags
}
section-related =
.label = { pane-related }
.label = { $count } Related
sidenav-info =
.tooltiptext = { pane-info }

View file

@ -418,7 +418,6 @@ pane.item.creator.moveDown = Move Down
pane.item.notes.allNotes = All Notes
pane.item.notes.untitled = Untitled Note
pane.item.notes.delete.confirm = Are you sure you want to delete this note?
pane.item.notes.count = %1$S note;%1$S notes
pane.item.notes.ignoreMissingImage = Some note images are missing and cannot be copied.
pane.item.attachments.rename.title = New title:
pane.item.attachments.rename.renameAssociatedFile = Rename associated file
@ -440,9 +439,6 @@ pane.item.attachments.autoRelinkOthers.title = Additional Files Located
pane.item.attachments.autoRelinkOthers.text = One other unlinked attachment in this library was found within the same directory. Relink this attachment as well?;%S other unlinked attachments in this library were found within the same directory. Relink all located attachments?
pane.item.attachments.autoRelink.locateManually = Locate Manually…
pane.item.attachments.autoRelink.relinkAll = Relink All
pane.item.attachments.count.zero = %S attachments:
pane.item.attachments.count.singular = %S attachment:
pane.item.attachments.count.plural = %S attachments:
pane.item.attachments.has = Has attachment
pane.item.attachments.hasPDF = Has PDF attachment
pane.item.attachments.hasSnapshot = Has snapshot
@ -453,13 +449,9 @@ pane.item.attachments.PDF.installTools.title = PDF Tools Not Installed
pane.item.attachments.PDF.installTools.text = To use this feature, you must first install the PDF tools in the Search pane of the Zotero preferences.
pane.item.attachments.filename = Filename
pane.item.noteEditor.clickHere = click here
pane.item.tags.count = %1$S tag;%1$S tags
pane.item.tags.icon.user = User-added tag
pane.item.tags.icon.automatic = Automatically added tag
pane.item.tags.icon.user = User-added tag
pane.item.tags.icon.automatic = Automatically added tag
pane.item.tags.removeAll = Remove all tags from this item?
pane.item.related.count.zero = %S related:
pane.item.related.count.singular = %S related:
pane.item.related.count.plural = %S related:
pane.item.parentItem = Parent Item:
pane.item.viewOnline.tooltip = Go to this item online

View file

@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 14.5H14.5V1.5H1.5V8.5M7.5 14.5L1.5 8.5M7.5 14.5V8.5H1.5" stroke="black"/>
<path d="M7.5 14.5H14.5V1.5H1.5V8.5M7.5 14.5L1.5 8.5M7.5 14.5V8.5H1.5" stroke="context-fill"/>
</svg>

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 198 B

View file

Before

Width:  |  Height:  |  Size: 239 B

After

Width:  |  Height:  |  Size: 239 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8" fill="none">
<path d="M0 2.70711L4 6.70711L8 2.70711L7.29289 2L4 5.29289L0.707107 2L0 2.70711Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 205 B

View file

@ -1,5 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon">
<path id="Vector" d="M2 5.70711L8 11.7071L14 5.70711L13.2929 5L8 10.2929L2.70711 5L2 5.70711Z" fill="context-fill"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 239 B

View file

@ -27,9 +27,10 @@
flex: 1;
overflow-y: auto;
padding: 0 8px;
overflow-anchor: none; /* Work around tags box causing scroll to jump - figure this out */
}
.zotero-view-item-container.feed-item .zotero-view-item > :is(notes-box, tags-box, related-box),
.zotero-view-item-container.feed-item .zotero-view-item > :is(attachments-box, notes-box, tags-box, related-box),
.zotero-view-item-container.feed-item .zotero-view-item-sidenav {
display: none;
}
@ -37,7 +38,6 @@
.zotero-view-item > * {
box-sizing: border-box;
width: 100%;
padding-block: 4px;
}
.zotero-view-item > :last-child {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -502,10 +502,6 @@
height: 16px;
}
.zotero-box-label {
margin-inline-start: 3px !important;
}
#retracted-items-banner, #sync-reminder-banner {
display: flex;
justify-content: center;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,5 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="plus">
<path id="Vector" d="M14 8H9V3H8V8H3V9H8V14H9V9H14V8Z" fill="context-fill"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 832 B

View file

@ -0,0 +1,10 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="tag-fill" clip-path="url(#clip0_3570_12428)">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M0 0.75C0 0.335786 0.335787 0 0.75 0H5.20711L11.6768 6.46967C11.9697 6.76256 11.9697 7.23744 11.6768 7.53033L7.53033 11.6768C7.23744 11.9697 6.76256 11.9697 6.46967 11.6768L0 5.20711V0.75ZM3 4C3.55228 4 4 3.55228 4 3C4 2.44772 3.55228 2 3 2C2.44772 2 2 2.44772 2 3C2 3.55228 2.44772 4 3 4Z" fill="context-fill"/>
</g>
<defs>
<clipPath id="clip0_3570_12428">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

View file

@ -0,0 +1,10 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="tag" clip-path="url(#clip0_3570_22211)">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M1 4.79289V1H4.79289L10.7929 7L7 10.7929L1 4.79289ZM0.75 0C0.335787 0 0 0.335786 0 0.75V5V5.20711L0.146447 5.35355L6.46967 11.6768C6.76256 11.9697 7.23744 11.9697 7.53033 11.6768L11.6768 7.53033C11.9697 7.23744 11.9697 6.76256 11.6768 6.46967L5.35355 0.146447L5.20711 0H5H0.75ZM3 4C3.55228 4 4 3.55228 4 3C4 2.44772 3.55228 2 3 2C2.44772 2 2 2.44772 2 3C2 3.55228 2.44772 4 3 4Z" fill="context-fill"/>
</g>
<defs>
<clipPath id="clip0_3570_22211">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

View file

@ -69,3 +69,6 @@
@import "elements/itemPaneSidenav";
@import "elements/abstractBox";
@import "elements/collapsibleSection";
@import "elements/attachmentsBox";
@import "elements/attachmentRow";
@import "elements/annotationRow";

View file

@ -78,4 +78,33 @@
@media (prefers-color-scheme: dark) {
@content("dark");
}
}
}
@mixin clicky-item {
display: flex;
align-items: flex-start;
gap: 4px;
padding-inline-start: 4px;
overflow: hidden;
&:hover {
border-radius: 5px;
background-color: var(--fill-quinary);
}
.label {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10;
width: 0; // Needed to allow the label to shrink for some reason
flex: 1;
overflow: hidden;
line-height: 16px;
}
@include comfortable {
.icon, .label {
padding-block: 2px;
}
}
}

View file

@ -1,57 +1,41 @@
.zotero-clicky {
border-radius: 6px;
border: var(--material-border-transparent);
padding: 2px;
&:not([disabled=true]) {
&:hover {
background-color: var(--fill-quinary) !important;
}
&:active {
background-color: var(--fill-quarternary) !important;
}
}
}
/* Minus and plus buttons with clicky glow effect */
.zotero-clicky-minus, .zotero-clicky-plus {
color: transparent !important;
padding: 0 !important;
margin: 0 !important;
margin-inline-end: 5px !important;
width: 18px;
height: 18px;
width: 20px;
height: 20px;
border-radius: 2px;
color: var(--fill-secondary);
.toolbarbutton-text {
display: none;
}
}
.zotero-clicky-minus {
background: url(chrome://zotero/skin/minus.png) center/auto 18px no-repeat !important;
@include svgicon-menu("minus-circle", "universal", "16");
border: 0px !important;
}
.zotero-clicky-plus {
background: url(chrome://zotero/skin/plus.png) center/auto 18px no-repeat !important;
@include svgicon-menu("plus-circle", "universal", "16");
border: 0px !important;
}
.zotero-clicky-minus[disabled=true], .zotero-clicky-plus[disabled=true] {
opacity: .5;
}
.zotero-clicky-minus:not([disabled=true]):active {
background-image: url('chrome://zotero/skin/minus-active.png') !important;
}
.zotero-clicky-plus:not([disabled=true]):active {
background-image: url('chrome://zotero/skin/plus-active.png') !important;
}
.zotero-clicky:not([disabled=true]):not(.disabled):hover {
background: rgb(187, 206, 241);
border: 1px solid rgb(109, 149, 224);
}
.zotero-clicky:not([disabled=true]):not(.disabled):not(menulist):active,
.zotero-clicky[selected="true"],
.zotero-clicky.selected {
color: white;
background: rgb(89, 139, 236);
}
/* BEGIN 2X BLOCK -- DO NOT EDIT MANUALLY -- USE 2XIZE */
@media (min-resolution: 1.25dppx) {
.zotero-clicky-minus { background: url(chrome://zotero/skin/minus@2x.png) center/auto 18px no-repeat !important; }
.zotero-clicky-plus { background: url(chrome://zotero/skin/plus@2x.png) center/auto 18px no-repeat !important; }
.zotero-clicky-minus:not([disabled=true]):active { background-image: url('chrome://zotero/skin/minus-active@2x.png') !important; }
.zotero-clicky-plus:not([disabled=true]):active { background-image: url('chrome://zotero/skin/plus-active@2x.png') !important; }
}

View file

@ -30,9 +30,7 @@
.toolbarbutton-menu-dropmarker {
display: block;
visibility: visible;
background-image: url("chrome://zotero/skin/chevron-6.svg");
background-repeat: no-repeat;
background-position: center;
@include svgicon("chevron-8", "universal", "8");
fill: var(--fill-secondary);
width: 8px;
height: 8px;

View file

@ -1,5 +1,10 @@
abstract-box {
display: flex;
flex-direction: column;
}
abstract-box .body {
display: flex;
editable-text {
flex: 1;

View file

@ -0,0 +1,64 @@
annotation-row {
display: flex;
flex-direction: column;
border-radius: 5px;
border: 1px solid var(--fill-quinary);
background: var(--material-background);
.head {
display: flex;
padding: 4px 8px;
align-items: center;
gap: 4px;
border-bottom: 1px solid var(--fill-quinary);
.icon {
width: 16px;
height: 16px;
-moz-context-properties: fill;
fill: var(--annotation-color, var(--fill-secondary));
}
.title {
font-weight: 590;
}
}
.body {
img {
max-width: 100%;
}
.quote, .comment {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 9;
overflow: hidden;
}
.quote {
margin: 3px 8px;
padding-inline-start: 6px;
border-inline-start: 2px solid var(--annotation-color, var(--fill-secondary));
color: var(--fill-secondary);
@include comfortable {
margin-block: 5px;
}
}
.comment {
padding: 3px 8px;
@include comfortable {
padding-block: 4px;
}
}
}
.tags {
border-top: 1px solid var(--fill-quinary);
padding: 3px 8px;
}
}

View file

@ -0,0 +1,66 @@
attachment-row {
display: flex;
flex-direction: column;
gap: 2px;
& > .head {
display: flex;
align-items: center;
.twisty {
width: 8px;
height: 8px;
margin: 4px;
align-self: flex-start;
@include comfortable {
padding-block: 2px;
}
@include svgicon("chevron-8", "universal", "8");
fill: var(--fill-secondary);
transform: rotate(0deg);
transform-origin: center;
transition: transform 0.2s ease-in-out;
}
.clicky-item {
@include clicky-item;
flex: 1;
}
}
&[open]:not([empty]) > .head .twisty {
transform: rotate(-180deg);
}
&[empty] > .head .twisty {
fill: var(--fill-tertiary);
}
&.context > .head .title {
// TODO This color is used in virtualized-table - probably want to change to something theme-defined
color: gray;
}
& > .body {
display: flex;
flex-direction: column;
gap: 8px;
max-height: var(--open-height, auto);
opacity: 1;
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out;
}
&:not([open]) {
& > .body {
max-height: 0;
opacity: 0;
visibility: hidden;
overflow-y: hidden;
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out, visibility 0s 0.2s, overflow-y 0s 0.2s;
}
}
}

View file

@ -0,0 +1,10 @@
attachments-box {
display: flex;
flex-direction: column;
& > collapsible-section > .body {
display: flex;
flex-direction: column;
gap: 2px;
}
}

View file

@ -2,7 +2,11 @@ collapsible-section {
display: flex;
flex-direction: column;
gap: 2px;
border-bottom: 1px solid var(--fill-quinary);
padding-block: 4px;
:not(:last-child) > & {
border-bottom: 1px solid var(--fill-quinary);
}
& > .head {
@include comfortable {
@ -33,28 +37,39 @@ collapsible-section {
width: 20px;
height: 20px;
padding: 2px;
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
fill: var(--fill-secondary);
stroke: var(--fill-secondary);
color: var(--fill-secondary);
}
toolbarbutton.add {
list-style-image: icon-url("plus.svg");
@include svgicon-menu("plus", "universal", "16");
border-radius: 2px;
&:hover {
background: var(--fill-quinary);
}
&:active {
background: var(--fill-quarternary);
}
}
toolbarbutton.twisty .toolbarbutton-icon {
list-style-image: icon-url("chevron-12.svg");
@include svgicon-menu("chevron-12", "universal", "16");
transform: rotate(0deg);
transform-origin: center;
transition: transform 0.2s ease-in-out;
}
}
&[open] > .head {
&[open]:not([empty]) > .head {
toolbarbutton.twisty .toolbarbutton-icon {
transform: rotate(-180deg);
}
}
&[empty] > .head > toolbarbutton.twisty {
fill: var(--fill-tertiary);
}
@each $pane, $color in $item-pane-sections {
&[data-pane="#{$pane}"] {
@ -68,7 +83,6 @@ collapsible-section {
}
& > :not(.head) {
overflow: hidden;
max-height: var(--open-height, auto);
opacity: 1;
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out;
@ -79,7 +93,8 @@ collapsible-section {
max-height: 0;
opacity: 0;
visibility: hidden;
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out, visibility 0s 0.2s;
overflow-y: hidden;
transition: max-height 0.2s ease-in-out, opacity 0.2s ease-in-out, visibility 0s 0.2s, overflow-y 0s 0.2s;
}
}
}

View file

@ -15,12 +15,12 @@ editable-text {
display: grid;
&::after {
content: attr(data-value) ' ';
content: attr(value) ' ';
visibility: hidden;
margin: 1px;
}
&::after, textarea {
&::after, .input {
grid-area: 1 / 1 / 2 / 2;
padding: var(--editable-text-padding-block) var(--editable-text-padding-inline);
font: inherit;
@ -30,7 +30,10 @@ editable-text {
white-space: pre-wrap;
}
textarea {
.input {
// Necessary for consistent padding, even if it's actually an <input>
-moz-default-appearance: textarea;
min-height: 0;
margin: 0;
@ -55,11 +58,11 @@ editable-text {
}
&[multiline] {
&::after, textarea {
&::after, .input {
overflow-y: auto;
}
textarea {
.input {
min-height: 5em;
}
}

View file

@ -1,4 +1,9 @@
item-box {
display: flex;
flex-direction: column;
}
item-box .body {
--row-height: 1.5em;
display: flex;

View file

@ -1,49 +1,35 @@
notes-box, related-box {
.header {
font-weight: normal;
display: flex;
flex-direction: column;
gap: 2px;
}
notes-box .body, related-box .body {
display: flex;
flex-direction: column;
padding-inline-start: 16px - 2px;
.row {
display: flex;
padding-left: 10px;
align-items: center;
gap: 4px;
align-items: flex-start;
@include comfortable {
padding-block: 2px;
}
label {
margin-right: 5px;
}
button {
min-width: 79px;
margin: 5px 6px 3px;
padding-top: 1px;
padding-bottom: 1px;
color: ButtonText;
text-shadow: none;
font-size: inherit;
}
}
.grid {
display: grid;
grid-template-columns: 1fr auto;
.box {
overflow: hidden;
display: flex;
margin-left: 5px;
@include clicky-item;
flex: 1;
}
img {
width: 16px;
height: 16px;
}
toolbarbutton {
margin-inline-start: auto;
visibility: hidden;
}
label {
margin-left: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:is(:hover, :focus-within) toolbarbutton {
visibility: visible;
}
}
}

View file

@ -1,74 +1,75 @@
tags-box {
.tags-box-header {
display: flex;
padding-left: 10px;
align-items: center;
display: flex;
flex-direction: column;
}
button {
min-width: 79px;
margin: 5px 6px 3px;
padding-top: 1px;
padding-bottom: 1px;
color: ButtonText;
text-shadow: none;
font-size: inherit;
}
tags-box .body {
display: flex;
flex-direction: column;
margin: 0;
padding-inline-start: 16px;
.tags-box-list {
display: flex;
flex-direction: column;
}
.tags-box-list {
list-style: none;
margin: 0;
padding: 2px 0 0; // Leave space for textbox border on top tag
.row {
display: grid;
grid-template-columns: 12px 1fr 20px;
align-items: center;
column-gap: 4px;
padding-block: 1px;
li {
display: flex;
margin: 3px 0;
margin-inline-start: 6px;
align-items: center;
height: 1.5em;
// Shift-Enter
&.multiline {
align-items: start;
min-height: 9em;
// Shift-Enter
&.multiline {
align-items: start;
height: 9em;
textarea.editable {
resize: none;
}
textarea.editable {
resize: none;
}
}
&:not(.multiline) .editable {
padding: 0 1px;
}
.zotero-box-icon {
grid-column: 1;
width: 12px;
height: 12px;
-moz-context-properties: fill;
background: icon-url('tag.svg') center no-repeat;
}
&[tagType="0"] .zotero-box-icon {
// User tag: use tag color if we have one, blue accent if we don't
fill: var(--tag-color, var(--accent-blue));
}
&[tagType="1"] .zotero-box-icon {
// Automatic tag: use tag color if we have one, gray if we don't
fill: var(--tag-color, var(--fill-secondary));
}
.zotero-box-label {
grid-column: 2;
}
&.has-color {
.zotero-box-icon {
width: 16px;
height: 16px;
background-image: icon-url('tag-fill.svg');
}
.zotero-box-label {
flex-grow: 1;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
width: 0;
font-weight: 590;
}
}
.editable {
font-family: inherit;
font-size: inherit;
flex-grow: 1;
margin: 0 2px;
width: 0;
}
toolbarbutton {
margin-inline-start: auto;
visibility: hidden;
}
button {
border: 0;
background: none;
padding: 0;
width: 20px;
height: 18px;
}
&:is(:hover, :focus-within) toolbarbutton {
visibility: visible;
}
}
}

View file

@ -1,3 +0,0 @@
.tags-box-list li img {
margin-right: 1px;
}