Unify context pane into new item pane/sidenav design

This commit is contained in:
Abe Jellinek 2023-12-22 23:54:08 -08:00 committed by Dan Stillman
parent beaf03197e
commit 583c6328a0
27 changed files with 722 additions and 1345 deletions

View file

@ -37,12 +37,12 @@
#zotero-items-splitter[orient=vertical],
#zotero-context-splitter-stacked
{
-moz-border-start: none !important;
-moz-border-end: none !important;
background-color: #bdbdbd !important;
max-height: 1px !important;
min-height: 1px !important;
height: 1px !important;
-moz-border-start: none;
-moz-border-end: none;
background-color: #bdbdbd;
max-height: 1px;
min-height: 1px;
height: 1px;
}
#zotero-collections-splitter:not([state=collapsed]) > grippy,
@ -53,18 +53,12 @@
display: none;
}
#zotero-collections-splitter[state=collapsed],
#zotero-items-splitter[state=collapsed],
#zotero-context-splitter[state=collapsed],
#zotero-context-splitter-stacked[state=collapsed]
{
#zotero-collections-splitter[state=collapsed] {
border: 0 solid #d6d6d6 !important;
padding: 0;
}
#zotero-collections-splitter[state=collapsed],
#zotero-items-splitter[state=collapsed][orient=horizontal],
#zotero-context-splitter[state=collapsed][orient=horizontal]
#zotero-collections-splitter[state=collapsed]
{
background-image: url("chrome://zotero/skin/mac/vsplitter.png");
background-repeat: repeat-y;
@ -73,14 +67,24 @@
width: 8px !important;
}
#zotero-items-splitter[state=collapsed][orient=horizontal],
#zotero-context-splitter[state=collapsed][orient=horizontal] {
max-width: 1px;
min-width: 1px;
width: 1px;
background: transparent;
margin-inline-start: -1px;
position: relative;
}
#zotero-items-splitter[state=collapsed][orient=vertical],
#zotero-context-splitter-stacked[state=collapsed][orient=vertical]
{
background-image: url("chrome://zotero/skin/mac/hsplitter.png");
background-repeat: repeat-x;
max-height: 8px !important;
min-height: 8px !important;
height: 8px !important;
#zotero-context-splitter-stacked[state=collapsed][orient=vertical] {
max-height: 1px;
min-height: 1px;
height: 1px;
background: transparent;
margin-top: -1px;
position: relative;
}
#zotero-collections-splitter[state=collapsed] {
@ -104,11 +108,6 @@
width: 8px;
}
#zotero-context-toolbar-extension {
/* To cover #zotero-context-splitter 1px border */
margin-inline-start: -1px;
}
/* How to get active twisty?
treechildren::-moz-tree-twisty(active) {
-moz-appearance: none;

View file

@ -1,168 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2020 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
import React, { forwardRef, useImperativeHandle, useState, memo } from 'react';
import cx from 'classnames';
import { CSSItemTypeIcon } from 'components/icons';
const MAX_UNEXPANDED_ALL_NOTES = 7;
const NoteRow = memo(({ id, title, body, date, onClick, onKeyDown, onContextMenu, parentItemType, parentTitle }) => {
return (
<div
tabIndex={-1}
className={cx('note-row', { 'standalone-note-row': !parentItemType })}
onClick={() => onClick(id)}
onContextMenu={(event) => onContextMenu(id, event)}
onKeyDown={onKeyDown}
>
<div className="inner">
{ parentItemType
? <div className="parent-line">
<CSSItemTypeIcon className="parent-item-type" itemType={parentItemType} />
<span className="parent-title">{parentTitle}</span>
</div>
: null
}
<div className="title-line">
<div className="title">{title}</div>
</div>
<div className="body-line">
<div className="date">{date}</div>
<div className="body">{body}</div>
</div>
</div>
</div>
);
});
const NotesList = forwardRef(({ onClick, onContextMenu, onAddChildButtonDown, onAddStandaloneButtonDown }, ref) => {
const [notes, setNotes] = useState([]);
const [expanded, setExpanded] = useState(false);
const [numVisible, setNumVisible] = useState(0);
const [hasParent, setHasParent] = useState(true);
const _setExpanded = (value) => {
setExpanded(value);
if (value) {
setNumVisible(numVisible + 1000);
}
else {
setNumVisible(0);
}
};
useImperativeHandle(ref, () => ({
setNotes,
setHasParent,
setExpanded: _setExpanded
}));
function handleClickMore() {
_setExpanded(true);
}
function handleButtonKeydown(event) {
if (event.key === 'Tab' && !event.shiftKey) {
let node = event.target.parentElement.parentElement.querySelector('[tabindex="-1"]');
if (node) {
node.focus();
event.preventDefault();
}
}
else if (event.key === 'Tab' && event.shiftKey) {
let prevSection = event.target.parentElement.parentElement.previousElementSibling;
if (prevSection) {
let node = prevSection.querySelector('[tabindex="-1"]:last-child');
if (node) {
node.focus();
event.preventDefault();
}
}
}
}
function handleRowKeyDown(event) {
if (['Enter', 'Space'].includes(event.key)) {
// Focus the previous row, because "more-row" will disappear
if (event.target.classList.contains('more-row')) {
let node = event.target.previousElementSibling;
if (node) {
node.focus();
event.preventDefault();
}
}
event.target.click();
}
else if (event.key === 'ArrowUp') {
let node = event.target.previousElementSibling;
if (node) {
node.focus();
event.preventDefault();
}
}
else if (event.key === 'ArrowDown') {
let node = event.target.nextElementSibling;
if (node) {
node.focus();
event.preventDefault();
}
}
}
let childNotes = notes.filter(x => x.isCurrentChild);
let allNotes = notes.filter(x => !x.isCurrentChild);
let visibleNotes = allNotes.slice(0, expanded ? numVisible : MAX_UNEXPANDED_ALL_NOTES);
return (
<div className="notes-list">
{hasParent && <section>
<div className="header-row">
<h2>{Zotero.getString('pane.context.itemNotes')}</h2>
<button onMouseDown={onAddChildButtonDown} onClick={onAddChildButtonDown} onKeyDown={handleButtonKeydown}>+</button>
</div>
{!childNotes.length && <div className="empty-row">{Zotero.getString('pane.context.noNotes')}</div>}
{childNotes.map(note => <NoteRow key={note.id} {...note}
onClick={onClick} onKeyDown={handleRowKeyDown} onContextMenu={onContextMenu}/>)}
</section>}
<section>
<div className="header-row">
<h2>{Zotero.getString('pane.context.allNotes')}</h2>
<button onMouseDown={onAddStandaloneButtonDown} onClick={onAddStandaloneButtonDown} onKeyDown={handleButtonKeydown}>+</button>
</div>
{!allNotes.length && <div className="empty-row">{Zotero.getString('pane.context.noNotes')}</div>}
{visibleNotes.map(note => <NoteRow key={note.id} {...note}
onClick={onClick} onKeyDown={handleRowKeyDown} onContextMenu={onContextMenu}/>)}
{allNotes.length > visibleNotes.length
&& <div className="more-row" tabIndex={-1} onClick={handleClickMore} onKeyDown={handleRowKeyDown}>{
Zotero.getString('general.numMore', Zotero.Utilities.numberFormat(
[allNotes.length - visibleNotes.length], 0))
}</div>
}
</section>
</div>
);
});
export default NotesList;

View file

@ -1,442 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React, { useState, useEffect, useRef, useMemo, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Editable from '../editable';
import Input from '../form/input';
import TextAreaInput from '../form/textArea';
//import Button from '../form/button';
const TagsBox = React.forwardRef((props, ref) => {
const [prevInitialTags, setPrevInitialTags] = useState([]);
const [tags, setTags] = useState([]);
const tagNames = useMemo(() => new Set(tags.map(t => t.tag)), [tags]);
const [selectedTag, setSelectedTag] = useState('');
const [newRow, setNewRow] = useState(false);
const [currentValue, setCurrentValue] = useState('');
const [isMultiline, setIsMultiline] = useState(false);
const [rows, setRows] = useState(1);
const rootRef = useRef(null);
const textboxRef = useRef(null);
const requestID = useRef(1);
const newRowID = useRef(1);
const resetSelectionOnRender = useRef(false);
const skipNextEdit = useRef(false);
const removeStr = Zotero.getString('general.remove');
useEffect(() => {
// Move cursor to end of textarea after paste
if (isMultiline) {
//let textarea = window.getSelection().anchorNode.querySelector('textarea');
let textarea = rootRef.current && rootRef.current.querySelector('textarea');
if (textarea) {
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}
}
if (resetSelectionOnRender.current) {
resetSelectionOnRender.current = false;
if (props.onResetSelection) {
props.onResetSelection();
}
}
});
useImperativeHandle(ref, () => ({
blurOpenField
}));
function handleAddTag() {
setSelectedTag('');
showNewRow(true);
requestID.current++;
}
function showNewRow(show) {
setNewRow(show);
if (show) {
setSelectedTag('');
newRowID.current++;
}
}
function handleEdit(event) {
if (!props.editable) {
return;
}
if (skipNextEdit.current) {
skipNextEdit.current = false;
return;
}
var tag = event.currentTarget.closest('[data-tag]').dataset.tag;
if (tag === '') {
return;
}
// If switching from input to textarea, don't change anything
if (isMultiline) {
return;
}
setCurrentValue(tag);
setSelectedTag(tag);
showNewRow(false);
}
function handleKeyDown(event) {
// With the delete button set to tabindex=-1, Tab doesn't work in the last tag for some
// reason, so blur it manually
if (!isMultiline && event.key == 'Tab' && !event.shiftKey) {
let target = event.currentTarget || event.target;
let oldTag = target.closest('[data-tag]').dataset.tag;
if (oldTag === '' && target.value === '') {
textboxRef.current.blur();
}
}
}
function handleMouseDown(event) {
// Prevent right-click on a tag from switching to edit mode
if (event.button != 0) {
event.stopPropagation();
event.preventDefault();
// The above works on its own, but setting the XUL context popup allows the event to go
// through if the confirmation prompt for "Remove All Tags" is cancelled, so we need
// to skip the next edit event as well
skipNextEdit.current = true;
}
}
function handleCommit(newTag, hasChanged, event) {
var oldTag = (event.currentTarget || event.target).closest('[data-tag]').dataset.tag;
var oldTags = tags;
var sortedTags = getSortedTags(oldTags);
var lastTag = sortedTags.length ? sortedTags[oldTags.length - 1] : null;
if (!isMultiline
&& event.key == 'Enter'
&& event.shiftKey) {
let trimmed = newTag.trim();
if (trimmed !== '') {
trimmed += "\n";
}
setCurrentValue(trimmed);
setIsMultiline(true);
setRows(6);
event.preventDefault();
return;
}
setCurrentValue('');
setSelectedTag('');
setIsMultiline(false);
// Tag hasn't changed
if (oldTag === newTag) {
// If Enter was pressed in an empty text box, hide it
if (newTag === '') {
showNewRow(false);
}
/*else if (oldTag == lastTag.tag) {
showNewRow(true);
}*/
resetSelectionOnRender.current = event.key == 'Enter';
return;
}
var newTags = [];
if (newTag !== '') {
// Split by newlines
let splitTags = newTag.split(/\r\n?|\n/)
.map(val => val.trim())
.filter(x => x);
let newTagsMap = new Map();
// Get all tags
for (let i = 0; i < oldTags.length; i++) {
let tag = oldTags[i];
// If this was the tag being edited, add the new value(s)
if (tag.tag == oldTag) {
for (let t of splitTags) {
newTagsMap.set(t, { tag: t });
}
if (oldTag == lastTag) {
showNewRow(true);
}
}
// Otherwise add the old one
else {
newTagsMap.set(tag.tag, tag);
}
}
// New tag at end
if (oldTag === '') {
for (let t of splitTags) {
newTagsMap.set(t, { tag: t });
}
// Call this again to increment the ref and avoid reusing the entered value in the
// next new row
showNewRow(true);
}
else {
resetSelectionOnRender.current = event.key == 'Enter';
}
newTags = [...newTagsMap.values()];
}
// Tag cleared
else {
newTags = oldTags.filter(tag => tag.tag != oldTag);
showNewRow(false);
resetSelectionOnRender.current = event.key == 'Enter';
}
setTags(getSortedTags(newTags));
props.onTagsUpdate(newTags);
}
function handleCancel() {
setCurrentValue('');
setSelectedTag('');
setIsMultiline(false);
showNewRow(false);
//setSuggestions([]);
resetSelectionOnRender.current = true;
requestID.current++;
}
function handleDelete(event) {
var tag = event.currentTarget.closest('[data-tag]').dataset.tag;
var oldTags = tags;
setSelectedTag('');
var newTags = oldTags.filter(t => t.tag !== tag);
setTags(newTags);
props.onTagsUpdate(newTags);
}
function handlePaste(event) {
var text = event.clipboardData.getData('text');
//paste = paste.toUpperCase();
var multiline = !!text.trim().match(/\n/);
if (multiline) {
//setCurrentValue(str.trim());
let field = event.target;
let newValue;
// TODO: Add newlines before and after if necessary
if (field.selectionStart || field.selectionStart == '0') {
let startPos = field.selectionStart;
let endPos = field.selectionEnd;
newValue = field.value.substring(0, startPos)
+ text
+ field.value.substring(endPos, field.value.length);
}
else {
newValue = field.value + text;
}
setCurrentValue(newValue);
setIsMultiline(true);
setRows(newValue.split(/\n/).length);
event.preventDefault();
}
}
function blurOpenField(event) {
if (textboxRef.current && (!event || event.target != textboxRef.current)) {
textboxRef.current.blur();
}
}
function getSortedTags(tags) {
var sortedTags = [...tags];
sortedTags.sort((a, b) => a.tag.localeCompare(b.tag));
return sortedTags;
}
async function getFilteredSuggestions(value) {
var suggestions = await props.getSuggestions(value);
return suggestions.filter(s => !tagNames.has(s));
}
function tagsEqual(a, b) {
if (a.length != b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].tag !== b[i].tag || a[i].type !== b[i].type) {
return false;
}
}
return true;
}
function renderCount() {
var count = tags.length;
var str = 'pane.item.tags.count.';
// TODO: Switch to plural rules
switch (count) {
case 0:
str += 'zero';
break;
case 1:
str += 'singular';
break;
default:
str += 'plural';
break;
}
return Zotero.getString(str, [count]);
}
function renderTagRow(tag) {
// Icon
var iconFile = 'tag';
var title = '';
if (!tag.type || tag.newRow) {
title = Zotero.getString('pane.item.tags.icon.user');
}
else if (tag.type == 1) {
title = Zotero.getString('pane.item.tags.icon.automatic');
iconFile += '-automatic';
}
var selected = tag.tag === selectedTag;
// Style colored tags
var style = {};
if (!selected) {
let colorData = props.colors.get(tag.tag);
if (colorData) {
style.fontWeight = 'bold';
style.color = colorData.color;
}
}
return (
<li
className={cx({ tag: true, multiline: selected && isMultiline })}
key={tag.newRow ? newRowID.current + '' : tag.tag}
data-tag={tag.tag}
>
<img
src={`chrome://zotero/skin/${iconFile}${Zotero.hiDPISuffix}.png`}
alt={title}
title={title}
/* Fix 'title' not working for HTML-in-XUL */
onMouseOver={() => window.Zotero_Tooltip.start(title)}
onMouseOut={() => window.Zotero_Tooltip.stop()}
style={{ width: "16px", height: "16px" }}
onClick={props.editable ? (() => setSelectedTag(tag.tag)) : undefined}
/>
<div className="editable-container" style={style}>
<Editable
autoComplete={!isMultiline}
autoFocus
className={cx({ 'zotero-clicky': props.editable && !selected })}
getSuggestions={getFilteredSuggestions}
inputComponent={isMultiline ? TextAreaInput : Input}
isActive={selected}
isReadOnly={!props.editable}
onCancel={handleCancel}
onClick={handleEdit}
onCommit={handleCommit}
onFocus={handleEdit}
onKeyDown={handleKeyDown}
onMouseDown={handleMouseDown}
onPaste={handlePaste}
ref={textboxRef}
selectOnFocus={!isMultiline}
value={(selected && isMultiline) ? currentValue : tag.tag}
/>
</div>
{props.editable
&& (<button
onClick={handleDelete}
tabIndex="-1"
>
<img
alt={removeStr}
height="18"
width="18"
title={removeStr}
/* Fix 'title' not working for HTML-in-XUL */
onMouseOver={() => window.Zotero_Tooltip.start(removeStr)}
onMouseOut={() => window.Zotero_Tooltip.stop()}
src={`chrome://zotero/skin/minus${Zotero.hiDPISuffix}.png`}/>
</button>)}
</li>
);
}
// When the initial tags change (because the item was updated), update state with those
var initialTags = getSortedTags(props.initialTags);
if (!tagsEqual(initialTags, prevInitialTags)) {
setTags(initialTags);
setPrevInitialTags(initialTags);
}
var displayTags = [...tags];
if (newRow) {
displayTags.push({
tag: '',
newRow: true
});
}
return (
<div className="tags-box" ref={rootRef} onClick={blurOpenField}>
<div className="tags-box-header">
<div className="tags-box-count">{renderCount()}</div>
{ props.editable && <div><button onClick={handleAddTag}>Add</button></div> }
</div>
<div className="tags-box-list-container">
<ul className="tags-box-list">
{displayTags.map(tag => renderTagRow(tag))}
</ul>
{ props.editable && <span
tabIndex="0"
onFocus={handleAddTag}
/> }
</div>
</div>
);
});
TagsBox.propTypes = {
colors: PropTypes.instanceOf(Map),
editable: PropTypes.bool,
getSuggestions: PropTypes.func,
initialTags: PropTypes.array.isRequired,
onResetSelection: PropTypes.func,
onTagsUpdate: PropTypes.func
};
export default TagsBox;

View file

@ -1,126 +0,0 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2019 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://digitalscholar.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 *****
*/
import React, { useState, useEffect } from 'react';
//import PropTypes from 'prop-types';
import { Cc, Ci } from 'chrome';
import TagsBox from 'components/itemPane/tagsBox.js';
var search = Cc["@mozilla.org/autocomplete/search;1?name=zotero"]
.createInstance(Ci.nsIAutoCompleteSearch);
function TagsBoxContainer(props, ref) {
var map = Zotero.Tags.getColors(props.item.libraryID);
const [tags, setTags] = useState(props.item.getTags());
const [colors, setColors] = useState(Zotero.Tags.getColors(props.item.libraryID));
useEffect(() => {
var observer = {
notify: async function (action, type, ids, extraData) {
if (type == 'setting') {
if (ids.some(val => val.split("/")[1] == 'tagColors')) {
setColors(Zotero.Tags.getColors(props.item.libraryID));
}
}
else if (type == 'item-tag') {
for (let i = 0; i < ids.length; i++) {
let [itemID, _tagID] = ids[i].split('-').map(x => parseInt(x));
if (itemID == props.item.id) {
setTags(props.item.getTags());
break;
}
}
}
}
};
var id = Zotero.Notifier.registerObserver(observer, ['item-tag', 'setting'], 'tagsBox');
return function cleanup() {
Zotero.Notifier.unregisterObserver(id);
};
});
async function getSuggestions(value) {
var i = 0;
return new Zotero.Promise(function (resolve, reject) {
var results = [];
search.startSearch(
value,
JSON.stringify({
libraryID: props.item.libraryID,
fieldName: 'tag',
itemID: props.item.id
}),
[],
{
onSearchResult: function (search, result) {
if (result.searchResult == result.RESULT_IGNORED
|| result.searchResult == result.RESULT_FAILURE) {
reject(result.errorDescription);
return;
}
if (result.searchResult == result.RESULT_SUCCESS
|| result.searchResult == result.RESULT_SUCCESS_ONGOING) {
// Pick up where we left off
for (; i < result.matchCount; i++) {
results.push(result.getValueAt(i));
}
}
if (result.searchResult != result.RESULT_SUCCESS_ONGOING &&
result.searchResult != result.RESULT_NOMATCH_ONGOING) {
resolve(results);
}
}
}
);
});
}
function handleResetSelection() {
if (props.onResetSelection) {
props.onResetSelection();
}
}
async function handleTagsUpdate(newTags) {
var item = props.item;
item.setTags(newTags);
await item.saveTx();
}
return <TagsBox
colors={colors}
editable={props.editable}
getSuggestions={getSuggestions}
initialTags={tags}
onResetSelection={handleResetSelection}
onTagsUpdate={handleTagsUpdate}
ref={ref}
/>;
}
export default React.forwardRef(TagsBoxContainer);

View file

@ -23,11 +23,6 @@
***** END LICENSE BLOCK *****
*/
// TODO: Fix import/require related issues that might be
// related with `require` not reusing the context
var React = require('react');
var ReactDOM = require('react-dom');
var NotesList = require('components/itemPane/notesList').default;
var { getCSSItemTypeIcon } = require('components/icons');
var ZoteroContextPane = new function () {
@ -36,17 +31,11 @@ var ZoteroContextPane = new function () {
var _contextPaneInner;
var _contextPaneSplitter;
var _contextPaneSplitterStacked;
var _itemToggle;
var _notesToggle;
var _sidenav;
var _panesDeck;
var _itemPaneDeck;
var _notesPaneDeck;
var _splitButton;
var _itemPaneToggle;
var _notesPaneToggle;
var _tabToolbar;
var _itemContexts = [];
var _notesContexts = [];
@ -66,42 +55,17 @@ var ZoteroContextPane = new function () {
}
_tabCover = document.getElementById('zotero-tab-cover');
_itemToggle = document.getElementById('zotero-tb-toggle-item-pane');
_notesToggle = document.getElementById('zotero-tb-toggle-notes-pane');
_contextPane = document.getElementById('zotero-context-pane');
_contextPaneInner = document.getElementById('zotero-context-pane-inner');
_contextPaneSplitter = document.getElementById('zotero-context-splitter');
_contextPaneSplitterStacked = document.getElementById('zotero-context-splitter-stacked');
_splitButton = document.getElementById('zotero-tb-split');
_itemPaneToggle = document.getElementById('zotero-tb-toggle-item-pane');
_notesPaneToggle = document.getElementById('zotero-tb-toggle-notes-pane');
_tabToolbar = document.getElementById('zotero-tab-toolbar');
if (Zotero.rtl) {
_tabToolbar.style.left = 0;
_splitButton.style.transform = 'scaleX(-1)';
}
else {
_tabToolbar.style.right = 0;
}
// vbox
var vbox = document.createXULElement('vbox');
vbox.setAttribute('flex', '1');
_contextPaneInner.append(vbox);
// Toolbar extension
var toolbarExtension = document.createXULElement('box');
toolbarExtension.style.height = '32px';
toolbarExtension.id = 'zotero-context-toolbar-extension';
_sidenav = document.getElementById('zotero-context-pane-sidenav');
_panesDeck = document.createXULElement('deck');
_panesDeck.setAttribute('flex', 1);
_panesDeck.setAttribute('selectedIndex', 0);
vbox.append(toolbarExtension, _panesDeck);
_contextPaneInner.append(_panesDeck);
// Item pane deck
_itemPaneDeck = document.createXULElement('deck');
@ -113,17 +77,15 @@ var ZoteroContextPane = new function () {
_panesDeck.append(_itemPaneDeck, _notesPaneDeck);
_sidenav.contextNotesPane = _notesPaneDeck;
this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane');
window.addEventListener('resize', _update);
_itemToggle.addEventListener('click', _toggleItemButton);
_notesToggle.addEventListener('click', _toggleNotesButton);
Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth;
Zotero.Reader.onToggleSidebar = _updatePaneWidth;
};
this.destroy = function () {
_itemToggle.removeEventListener('click', _toggleItemButton);
_notesToggle.removeEventListener('click', _toggleNotesButton);
window.removeEventListener('resize', _update);
Zotero.Notifier.unregisterObserver(this._notifierID);
Zotero.Reader.onChangeSidebarWidth = () => {};
@ -175,7 +137,7 @@ var ZoteroContextPane = new function () {
else if (action == 'close') {
_removeItemContext(ids[0]);
if (Zotero_Tabs.deck.children.length == 1) {
_notesContexts.forEach(x => x.notesListRef.current.setExpanded(false));
_notesContexts.forEach(x => x.notesList.expanded = false);
}
// Close tab specific notes if tab id no longer exists, but
// do that only when unloaded tab is reloaded
@ -201,8 +163,8 @@ var ZoteroContextPane = new function () {
if (Zotero_Tabs.selectedType == 'library') {
_contextPaneSplitter.setAttribute('hidden', true);
_contextPane.setAttribute('collapsed', true);
_tabToolbar.hidden = true;
_tabCover.classList.add('hidden');
_sidenav.hidden = true;
}
else if (Zotero_Tabs.selectedType == 'reader') {
if (_panesDeck.selectedIndex == 1
@ -258,7 +220,8 @@ var ZoteroContextPane = new function () {
setTimeout(() => {
_contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed'));
});
_tabToolbar.hidden = false;
_sidenav.hidden = false;
}
_selectItemContext(ids[0]);
@ -356,25 +319,11 @@ var ZoteroContextPane = new function () {
}
}
function _updateToolbarWidth() {
var stacked = Zotero.Prefs.get('layout') == 'stacked';
var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
if ((stacked || _contextPaneSplitter.getAttribute('state') == 'collapsed')) {
reader.setToolbarPlaceholderWidth(_tabToolbar.offsetWidth);
}
else {
reader.setToolbarPlaceholderWidth(0);
}
}
}
function _update() {
if (Zotero_Tabs.selectedIndex == 0) {
return;
}
var splitter;
var stacked = Zotero.Prefs.get('layout') == 'stacked';
if (stacked) {
_contextPaneSplitterStacked.setAttribute('hidden', false);
@ -382,7 +331,8 @@ var ZoteroContextPane = new function () {
_contextPaneSplitter.setAttribute('hidden', true);
_contextPane.classList.add('stacked');
_contextPane.classList.remove('standard');
splitter = _contextPaneSplitterStacked;
_sidenav.classList.add('stacked');
_contextPaneInner.after(_sidenav);
}
else {
_contextPaneSplitter.setAttribute('hidden', false);
@ -390,24 +340,8 @@ var ZoteroContextPane = new function () {
_contextPaneSplitterStacked.setAttribute('state', 'open');
_contextPane.classList.add('standard');
_contextPane.classList.remove('stacked');
splitter = _contextPaneSplitter;
}
var collapsed = splitter.getAttribute('state') == 'collapsed';
var selectedIndex = _panesDeck.selectedIndex;
if (!collapsed && selectedIndex == 0) {
_itemPaneToggle.classList.add('toggled');
}
else {
_itemPaneToggle.classList.remove('toggled');
}
if (!collapsed && selectedIndex == 1) {
_notesPaneToggle.classList.add('toggled');
}
else {
_notesPaneToggle.classList.remove('toggled');
_sidenav.classList.remove('stacked');
_contextPane.after(_sidenav);
}
if (Zotero_Tabs.selectedIndex > 0) {
@ -420,7 +354,6 @@ var ZoteroContextPane = new function () {
}
_updatePaneWidth();
_updateToolbarWidth();
_updateAddToNote();
}
@ -537,11 +470,13 @@ var ZoteroContextPane = new function () {
var vbox = document.createXULElement('vbox');
vbox.style.flex = '1';
var input = document.createXULElement('search-textbox');
input.style.margin = '4px 7px';
input.setAttribute('data-l10n-id', 'context-notes-search');
input.setAttribute('data-l10n-attrs', 'placeholder');
input.style.margin = '6px 8px 7px 8px';
input.setAttribute('type', 'search');
input.setAttribute('timeout', '250');
input.addEventListener('command', () => {
notesListRef.current.setExpanded(false);
notesList.expanded = false;
_updateNotesList();
});
vbox.append(input);
@ -550,6 +485,7 @@ var ZoteroContextPane = new function () {
var listBox = document.createXULElement('vbox');
listBox.style.display = 'flex';
listBox.style.minWidth = '0';
listBox.setAttribute('flex', '1');
var listInner = document.createElement('div');
listInner.className = 'notes-list-container';
@ -559,7 +495,44 @@ var ZoteroContextPane = new function () {
list.append(head, listBox);
var notesListRef = React.createRef();
var notesList = document.createXULElement('context-notes-list');
notesList.addEventListener('note-click', (event) => {
let { id } = event.detail;
let item = Zotero.Items.get(id);
if (item) {
_setPinnedNote(item);
}
});
notesList.addEventListener('note-contextmenu', (event) => {
let { id, screenX, screenY } = event.detail;
let item = Zotero.Items.get(id);
if (item) {
document.getElementById('context-pane-list-move-to-trash').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-list-popup');
let handleCommand = (event) => _handleListPopupClick(id, event);
popup.addEventListener('popupshowing', () => {
popup.addEventListener('command', handleCommand, { once: true });
popup.addEventListener('popuphiding', () => {
popup.removeEventListener('command', handleCommand);
}, { once: true });
}, { once: true });
popup.openPopupAtScreen(screenX, screenY, true);
}
});
notesList.addEventListener('add-child', (event) => {
document.getElementById('context-pane-add-child-note').setAttribute('disabled', readOnly);
document.getElementById('context-pane-add-child-note-from-annotations').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-add-child-note-button-popup');
popup.onclick = _handleAddChildNotePopupClick;
popup.openPopup(event.detail.button, 'after_end');
});
notesList.addEventListener('add-standalone', (event) => {
document.getElementById('context-pane-add-standalone-note').setAttribute('disabled', readOnly);
document.getElementById('context-pane-add-standalone-note-from-annotations').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-add-standalone-note-button-popup');
popup.onclick = _handleAddStandaloneNotePopupClick;
popup.openPopup(event.detail.button, 'after_end');
});
function _isVisible() {
let splitter = Zotero.Prefs.get('layout') == 'stacked'
@ -653,18 +626,18 @@ var ZoteroContextPane = new function () {
var attachment = _getCurrentAttachment();
var parentID = attachment && attachment.parentID;
notesListRef.current.setHasParent(!!parentID);
notesListRef.current.setNotes(notes.map(note => ({
notesList.hasParent = !!parentID;
notesList.notes = notes.map(note => ({
...note,
isCurrentChild: parentID && note.parentID == parentID
})));
}));
}
var context = {
libraryID,
node: contextNode,
editor,
notesListRef,
notesList,
cachedNotes: [],
affectedIDs: new Set(),
update: Zotero.Utilities.throttle(_updateNotesList, 1000, { leading: false }),
@ -728,41 +701,8 @@ var ZoteroContextPane = new function () {
}
}
ReactDOM.render(
<NotesList
ref={notesListRef}
onClick={(id) => {
let item = Zotero.Items.get(id);
if (item) {
_setPinnedNote(item);
}
}}
onContextMenu={(id, event) => {
document.getElementById('context-pane-list-move-to-trash').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-list-popup');
popup.onclick = (event) => _handleListPopupClick(id, event);
popup.openPopupAtScreen(event.screenX, event.screenY);
}}
onAddChildButtonDown={(event) => {
document.getElementById('context-pane-add-child-note').setAttribute('disabled', readOnly);
document.getElementById('context-pane-add-child-note-from-annotations').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-add-child-note-button-popup');
popup.onclick = _handleAddChildNotePopupClick;
popup.openPopup(event.target, 'after_end');
}}
onAddStandaloneButtonDown={(event) => {
document.getElementById('context-pane-add-standalone-note').setAttribute('disabled', readOnly);
document.getElementById('context-pane-add-standalone-note-from-annotations').setAttribute('disabled', readOnly);
var popup = document.getElementById('context-pane-add-standalone-note-button-popup');
popup.onclick = _handleAddStandaloneNotePopupClick;
popup.openPopup(event.target, 'after_end');
}}
/>,
listInner,
() => {
_updateNotesList();
}
);
listInner.append(notesList);
_updateNotesList();
_notesContexts.push(context);
return context;
}
@ -870,9 +810,15 @@ var ZoteroContextPane = new function () {
}
function _selectItemContext(tabID) {
let selectedIndex = Array.from(_itemPaneDeck.children).findIndex(x => x.id == tabID + '-context');
if (selectedIndex != -1) {
_itemPaneDeck.setAttribute('selectedIndex', selectedIndex);
let selectedPanel = Array.from(_itemPaneDeck.children).find(x => x.id == tabID + '-context');
if (selectedPanel) {
_itemPaneDeck.selectedPanel = selectedPanel;
let div = selectedPanel.querySelector('.zotero-view-item');
// _addItemContext() awaits, so the div may not have been created yet. We'll set _sidenav.container
// below even if we don't set it here.
if (div) {
_sidenav.container = div;
}
}
}
@ -963,12 +909,6 @@ var ZoteroContextPane = new function () {
relatedBox.className = 'zotero-editpane-related';
relatedBox.setAttribute('data-pane', 'related');
div.append(relatedBox);
// item-pane-sidenav
var sidenav = document.createXULElement('item-pane-sidenav');
sidenav.className = 'zotero-view-item-sidenav';
hbox.append(sidenav);
sidenav.container = div;
paneHeader.mode = readOnly ? 'view' : 'edit';
paneHeader.item = parentItem;
@ -984,5 +924,9 @@ var ZoteroContextPane = new function () {
relatedBox.mode = readOnly ? 'view' : 'edit';
relatedBox.item = parentItem;
if (_itemPaneDeck.selectedPanel === container) {
_sidenav.container = div;
}
}
};

View file

@ -53,6 +53,8 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/collapsibl
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);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/contextNotesList.js', this);
Services.scriptloader.loadSubScript('chrome://zotero/content/elements/noteRow.js', this);
{
// Fix missing property bug that breaks arrow key navigation between <tab>s

View file

@ -74,14 +74,14 @@
}
set empty(val) {
this.toggleAttribute('empty', !!val);
this._runWithTransitionsDisabled(() => {
this.toggleAttribute('empty', !!val);
});
}
setCount(count) {
this.setAttribute('data-l10n-args', JSON.stringify({ count }));
this._runWithTransitionsDisabled(() => {
this.empty = !count;
});
this.empty = !count;
}
get label() {
@ -129,14 +129,20 @@
this._addButton = document.createXULElement('toolbarbutton');
this._addButton.className = 'add';
this._addButton.addEventListener('command', (event) => {
this.dispatchEvent(new CustomEvent('add', { ...event, bubbles: false }));
this.dispatchEvent(new CustomEvent('add', {
...event,
detail: { button: this._addButton },
bubbles: false
}));
});
this._head.append(this._addButton);
this._contextMenu = this._buildContextMenu();
let popupset = document.createXULElement('popupset');
popupset.append(this._contextMenu);
this._head.append(popupset);
if (this._contextMenu) {
let popupset = document.createXULElement('popupset');
popupset.append(this._contextMenu);
this._head.append(popupset);
}
let twisty = document.createXULElement('toolbarbutton');
twisty.className = 'twisty';
@ -157,13 +163,15 @@
}
_buildContextMenu() {
let containerRoot = this.closest('.zotero-view-item-container');
let containerRoot = this.closest('.zotero-view-item-container, context-notes-list');
let contextMenu = document.createXULElement('menupopup');
let collapseOtherSections = document.createXULElement('menuitem');
collapseOtherSections.classList.add('menuitem-iconic', 'zotero-menuitem-collapse-others');
collapseOtherSections.setAttribute('data-l10n-id', 'collapse-other-sections');
collapseOtherSections.addEventListener('command', () => {
// Scroll to the top (first section), so we don't end up scrolled past the end
containerRoot.querySelector('collapsible-section').scrollIntoView({ block: 'start' });
for (let section of containerRoot.querySelectorAll('collapsible-section')) {
if (section !== this) {
section.open = false;
@ -207,7 +215,7 @@
contextMenu.addEventListener('popupshowing', () => {
let sections = Array.from(containerRoot.querySelectorAll('collapsible-section'));
collapseOtherSections.disabled = sections.every(section => section === this || !section.open);
expandAllSections.disabled = sections.every(section => section.open);
expandAllSections.disabled = sections.every(section => section.open || section.empty);
let sidenav = this._getSidenav();
if (sidenav?.isPanePinnable(this.dataset.pane)) {
@ -268,7 +276,7 @@
_handleContextMenu = (event) => {
if (event.target.closest('.add')) return;
event.preventDefault();
this._contextMenu.openPopupAtScreen(event.screenX, event.screenY, true);
this._contextMenu?.openPopupAtScreen(event.screenX, event.screenY, true);
};
_getSidenav() {

View file

@ -0,0 +1,194 @@
/*
***** 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";
{
const MAX_UNEXPANDED_ALL_NOTES = 7;
class ContextNotesList extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div>
<collapsible-section data-pane="context-item-notes" show-add="true" class="item-notes">
<html:div class="body"/>
</collapsible-section>
</html:div>
<html:div>
<collapsible-section data-pane="context-all-notes" show-add="true" class="all-notes">
<html:div class="body"/>
</collapsible-section>
</html:div>
`, ['chrome://zotero/locale/zotero.dtd']);
_itemNotes = [];
_allNotes = [];
_expanded = false;
_numVisible = 0;
_hasParent = false;
get notes() {
return [...this._itemNotes, ...this._allNotes];
}
set notes(val) {
let itemNotes = [];
let allNotes = [];
for (let note of val) {
if (note.isCurrentChild) {
itemNotes.push(note);
}
else {
allNotes.push(note);
}
}
this._itemNotes = itemNotes;
this._allNotes = allNotes;
this.render();
}
get expanded() {
return this._expanded;
}
set expanded(val) {
this._expanded = val;
if (val) {
this.numVisible += 1000;
}
else {
this.numVisible = 0;
}
}
get numVisible() {
return this._numVisible;
}
set numVisible(val) {
this._numVisible = val;
this.render();
}
get hasParent() {
return this._hasParent;
}
set hasParent(val) {
this._hasParent = val;
this.render();
}
init() {
this._itemNotesSection = this.querySelector('.item-notes');
this._allNotesSection = this.querySelector('.all-notes');
this._itemNotesSection.label = Zotero.getString('pane.context.itemNotes');
this._allNotesSection.label = Zotero.getString('pane.context.allNotes');
this._itemNotesSection.addEventListener('add', this._handleAddNote);
this._allNotesSection.addEventListener('add', this._handleAddNote);
this.addEventListener('click', this._handleClick);
this.addEventListener('contextmenu', this._handleContextMenu);
this.render();
}
render() {
this._itemNotesSection.empty = !this._itemNotes.length;
this._allNotesSection.empty = !this._allNotes.length;
let itemNotesBody = this._itemNotesSection.querySelector('.body');
let allNotesBody = this._allNotesSection.querySelector('.body');
itemNotesBody.replaceChildren();
for (let note of this._itemNotes) {
itemNotesBody.append(this._makeRow(note));
}
allNotesBody.replaceChildren();
let visibleNotes = this._allNotes.slice(0, this._expanded ? this._numVisible : MAX_UNEXPANDED_ALL_NOTES);
for (let note of visibleNotes) {
allNotesBody.append(this._makeRow(note));
}
if (visibleNotes.length < this._allNotes.length) {
let moreButton = document.createElement('button');
moreButton.className = 'more';
moreButton.textContent = Zotero.getString('general.numMore', Zotero.Utilities.numberFormat(
[this._allNotes.length - visibleNotes.length], 0));
moreButton.addEventListener('click', () => {
this.expanded = true;
});
allNotesBody.append(moreButton);
}
}
_makeRow(note) {
let row = document.createXULElement('note-row');
row.note = note;
return row;
}
_handleClick = (event) => {
if (event.button !== 0) return;
let note = event.target.closest('note-row')?.note;
if (note) {
this.dispatchEvent(new CustomEvent('note-click', {
...event,
bubbles: true,
detail: { id: note.id }
}));
}
};
_handleContextMenu = (event) => {
let note = event.target.closest('note-row')?.note;
if (note) {
this.dispatchEvent(new CustomEvent('note-contextmenu', {
bubbles: true,
detail: {
screenX: event.screenX,
screenY: event.screenY,
id: note.id
}
}));
}
};
_handleAddNote = (event) => {
let eventName = event.target.closest('collapsible-section') == this._itemNotesSection
? 'add-child'
: 'add-standalone';
this.dispatchEvent(new CustomEvent(eventName, {
bubbles: true,
detail: { button: event.detail.button }
}));
};
}
customElements.define("context-notes-list", ContextNotesList);
}

View file

@ -28,6 +28,11 @@
{
class ItemPaneSidenav extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="toolbarbutton-container">
<toolbarbutton
data-pane="toggle-collapse"/>
</html:div>
<html:div class="divider"/>
<html:div class="toolbarbutton-container">
<toolbarbutton
data-l10n-id="sidenav-info"
@ -58,6 +63,12 @@
data-l10n-id="sidenav-related"
data-pane="related"/>
</html:div>
<html:div class="divider"/>
<html:div class="toolbarbutton-container">
<toolbarbutton
data-l10n-id="sidenav-notes"
data-pane="context-notes"/>
</html:div>
<popupset>
<menupopup class="context-menu">
@ -69,6 +80,8 @@
_container = null;
_contextNotesPane = null;
_contextMenuTarget = null;
_preserveMinScrollHeightTimeout = null;
@ -85,6 +98,16 @@
this.render();
}
get contextNotesPane() {
return this._contextNotesPane;
}
set contextNotesPane(val) {
if (this._contextNotesPane == val) return;
this._contextNotesPane = val;
this.render();
}
get pinnedPane() {
return this.getAttribute('pinnedPane');
}
@ -103,6 +126,49 @@
set _minScrollHeight(val) {
this._container.style.setProperty('--min-scroll-height', val + 'px');
}
get _contextNotesPaneVisible() {
return this._contextNotesPane
&& this._contextNotesPane.parentElement.selectedPanel == this._contextNotesPane;
}
set _contextNotesPaneVisible(val) {
if (!this._contextNotesPane) return;
// The context notes pane will always be a direct child of the deck we need to update
let deck = this._contextNotesPane.parentElement;
if (val) {
deck.selectedPanel = this._contextNotesPane;
this._collapsed = false;
}
else {
// But our _container is not a direct child of the deck,
// so find the child that contains it
deck.selectedPanel = Array.from(deck.children).find(child => child.contains(this._container));
}
this.render();
}
get _collapsed() {
let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
return collapsible.getAttribute('collapsed') === 'true';
}
set _collapsed(val) {
let collapsible = this.container.closest('splitter:not([hidden="true"]) + *');
let splitter = collapsible.previousElementSibling;
if (val) {
collapsible.setAttribute('collapsed', 'true');
splitter.setAttribute('state', 'collapsed');
splitter.setAttribute('substate', 'after');
}
else {
collapsible.removeAttribute('collapsed');
splitter.setAttribute('state', '');
splitter.setAttribute('substate', 'after');
}
window.dispatchEvent(new Event('resize'));
this.render();
}
static get observedAttributes() {
return ['pinnedPane'];
@ -113,6 +179,15 @@
}
scrollToPane(id, behavior = 'smooth') {
if (this._collapsed) {
this._collapsed = false;
behavior = 'instant';
}
if (this._contextNotesPane && this._contextNotesPaneVisible) {
this._contextNotesPaneVisible = false;
behavior = 'instant';
}
let pane = this.getPane(id);
if (!pane) return;
@ -175,7 +250,7 @@
isPanePinnable(id) {
return id !== 'info';
}
init() {
if (!this.container) {
this.container = document.getElementById('zotero-view-item');
@ -183,6 +258,29 @@
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
let pane = toolbarbutton.dataset.pane;
if (pane === 'context-notes') {
toolbarbutton.addEventListener('click', (event) => {
if (event.button !== 0) {
return;
}
if (event.detail == 2) {
this.pinnedPane = null;
}
this._contextNotesPaneVisible = true;
});
continue;
}
else if (pane === 'toggle-collapse') {
toolbarbutton.addEventListener('click', (event) => {
if (event.button !== 0) {
return;
}
this._collapsed = !this._collapsed;
});
continue;
}
let pinnable = this.isPanePinnable(pane);
toolbarbutton.parentElement.classList.toggle('pinnable', pinnable);
@ -232,7 +330,27 @@
let pinnedPane = this.pinnedPane;
for (let toolbarbutton of this.querySelectorAll('toolbarbutton')) {
let pane = toolbarbutton.dataset.pane;
toolbarbutton.setAttribute('aria-selected', pane == pinnedPane);
if (pane == 'context-notes') {
let hidden = !this._contextNotesPane;
let selected = this._contextNotesPaneVisible;
toolbarbutton.parentElement.hidden = hidden;
toolbarbutton.parentElement.previousElementSibling.hidden = hidden; // Divider
toolbarbutton.setAttribute('aria-selected', selected);
toolbarbutton.classList.toggle('active', selected);
continue;
}
else if (pane == 'toggle-collapse') {
toolbarbutton.setAttribute('data-l10n-id', 'sidenav-' + (this._collapsed ? 'expand' : 'collapse'));
toolbarbutton.classList.toggle('collapsed', this._collapsed);
continue;
}
toolbarbutton.setAttribute('aria-selected',
!this._contextNotesPaneVisible && pane == pinnedPane);
toolbarbutton.parentElement.hidden = !this.getPane(pane);
// Set .pinned on the container, for pin styling

View file

@ -44,7 +44,7 @@
<div id="note-editor" style="display: flex;flex-direction: column;flex-grow: 1;" xmlns="http://www.w3.org/1999/xhtml">
<iframe id="editor-view" style="border: 0;width: 100%;flex-grow: 1;" src="resource://zotero/note-editor/editor.html" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" type="content"/>
<div id="links-container">
<links-box id="links-box" style="display: flex" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
<links-box id="links-box" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>
</div>
</div>
</box>
@ -338,26 +338,12 @@
this._destroyed = false;
this.content = MozXULElement.parseXULToFragment(`
<div id="links-box" xmlns="http://www.w3.org/1999/xhtml">
<div class="grid">
<div id="parent-label" class="label" hidden="true"/>
<div id="parent-value" class="value zotero-clicky" hidden="true"/>
<div id="related-label" class="label"/>
<div id="related-value" class="value zotero-clicky"/>
<div id="tags-label" class="label"/>
<div id="tags-value" class="value zotero-clicky"/>
</div>
</div>
<popupset xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<menupopup id="related-popup" width="300">
<related-box id="related"/>
</menupopup>
<menupopup id="tags-popup" width="300" ignorekeys="true">
<tags-box id="tags"/>
</menupopup>
</popupset>
<!--
<html:div id="parent-label" class="label" hidden="true"/>
<html:div id="parent-value" class="value zotero-clicky" hidden="true"/>
-->
<tags-box id="tags"/>
<related-box id="related"/>
`, ['chrome://zotero/locale/zotero.dtd']);
}
@ -366,10 +352,6 @@
window.addEventListener("unload", () => this.destroy(), { once: true });
this.append(document.importNode(this.content, true));
this._id('parent-value').addEventListener('click', this._parentClickHandler);
this._id('related-value').addEventListener('click', this._relatedClickHandler);
this._id('tags-value').addEventListener('click', this._tagsClickHandler);
}
destroy() {
@ -390,15 +372,6 @@
this._id('tags').item = this._item;
this.refresh();
// Hide popup to prevent it being visible out of the context or
// in some cases even invisible but still blocking the next click
this._id('related-popup').addEventListener('click', (event) => {
let target = event.originalTarget;
if (target.classList.contains('zotero-box-label')) {
this._id('related-popup').hidePopup();
}
});
}
set mode(val) {
@ -419,110 +392,8 @@
}
refresh() {
this._updateParentRow();
this._updateTagsSummary();
this._updateRelatedSummary();
}
_updateParentRow() {
let hidden = !this._parentItem || !!this._notitle;
this._id('parent-value').hidden = hidden;
this._id('parent-label').hidden = hidden;
if (!hidden) {
this._id('parent-label').replaceChildren(Zotero.getString('pane.item.parentItem'));
this._id('parent-value').replaceChildren(document.createTextNode(this._parentItem.getDisplayTitle(true)));
}
}
_updateRelatedSummary() {
var r = '';
if (this._item) {
var keys = this._item.relatedItems;
if (keys.length) {
for (let key of keys) {
let item = Zotero.Items.getByLibraryAndKey(this._item.libraryID, key);
if (!item) {
Zotero.debug(`Related item ${this._item.libraryID}/${key} not found `
+ `for item ${this._item.libraryKey}`, 2);
continue;
}
r = r + item.getDisplayTitle() + ", ";
}
r = r.slice(0, -2);
}
}
let v = r;
if (!v || v == '') {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this._id('related-label').innerText = Zotero.getString('itemFields.related')
+ Zotero.getString('punctuation.colon');
this._id('related-value').innerText = v;
}
_updateTagsSummary() {
var r = '';
if (this._item) {
var tags = this._item.getTags();
// Sort tags alphabetically
var collation = Zotero.getLocaleCollation();
tags.sort((a, b) => collation.compareString(1, a.tag, b.tag));
for (let i = 0; i < tags.length; i++) {
r = r + tags[i].tag + ", ";
}
r = r.slice(0, -2);
}
let v = r;
if (!v || v == '') {
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
}
this._id('tags-label').innerText = Zotero.getString('itemFields.tags')
+ Zotero.getString('punctuation.colon');
this._id('tags-value').innerText = v;
}
_parentClickHandler = () => {
if (!this._item || !this._item.id) {
return;
}
var parentID = this._item.parentID;
var win = Zotero.getMainWindow();
if (win) {
win.ZoteroPane.selectItem(parentID);
win.Zotero_Tabs.select('zotero-pane');
win.focus();
}
};
_relatedClickHandler = (event) => {
var relatedList = this._item.relatedItems;
if (relatedList.length > 0) {
this._id('related-popup').openPopup(this, 'topleft topleft', 0, 0, false);
}
else if (this._mode == 'edit') {
this._id('related').add();
}
};
_tagsClickHandler = (event) => {
this._id('tags-popup').openPopup(this, 'topleft topleft', 0, 0, true);
// If editable and no existing tags, open new empty row
if (this._mode == 'edit' && !this._item.getTags().length) {
setTimeout(() => {
this._id('tags').addNew();
});
}
};
_id(id) {
return this.querySelector(`#${id}`);
}

View file

@ -0,0 +1,91 @@
/*
***** 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 NoteRow extends XULElementBase {
content = MozXULElement.parseXULToFragment(`
<html:div class="head">
<html:span class="icon"/>
<html:div class="parent-title"/>
</html:div>
<html:div class="body">
<html:div class="note-title"/>
<html:div class="note-content-container">
<html:div class="note-content"/>
<html:span class="note-date"/>
</html:div>
</html:div>
`);
_note = null;
get note() {
return this._note;
}
set note(val) {
this._note = val;
this.render();
}
init() {
this._parentTitle = this.querySelector('.parent-title');
this._noteTitle = this.querySelector('.note-title');
this._noteContent = this.querySelector('.note-content');
this._noteDate = this.querySelector('.note-date');
this.tabIndex = 0;
this.render();
}
render() {
if (!this.initialized) return;
let note = this._note;
if (!note) return;
if (note.parentItemType) {
this.querySelector('.icon').replaceWith(getCSSItemTypeIcon(note.parentItemType));
this._parentTitle.textContent = note.parentTitle;
this._noteTitle.hidden = false;
this._noteTitle.textContent = note.title;
}
else {
this.querySelector('.icon').replaceWith(getCSSItemTypeIcon('note'));
this._parentTitle.textContent = note.title;
this._noteTitle.hidden = true;
this._noteTitle.textContent = '';
}
this._noteContent.textContent = note.body;
this._noteContent.hidden = !note.body;
this._noteDate.textContent = note.date;
}
}
customElements.define('note-row', NoteRow);
}

View file

@ -338,14 +338,6 @@ class ReaderInstance {
this._onChangeSidebarWidthCallback(width);
}
},
onFocusSplitButton: () => {
if (this instanceof ReaderTab) {
let win = Zotero.getMainWindow();
if (win) {
win.document.getElementById('zotero-tb-toggle-item-pane').focus();
}
}
},
onFocusContextPane: () => {
if (this instanceof ReaderWindow || !this._window.ZoteroContextPane.focus()) {
this.focusFirst();

View file

@ -790,44 +790,7 @@ var ZoteroPane = new function()
*/
function handleKeyDown(event, from) {
if (Zotero_Tabs.selectedIndex > 0) {
let itemPaneToggle = document.getElementById('zotero-tb-toggle-item-pane');
let notesPaneToggle = document.getElementById('zotero-tb-toggle-notes-pane');
// Using ArrowDown and ArrowUp to be consistent with pdf-reader
if (!Zotero.rtl && event.key === 'ArrowRight'
|| Zotero.rtl && event.key === 'ArrowLeft'
|| event.key === 'ArrowDown') {
if (event.target === itemPaneToggle) {
notesPaneToggle.focus();
}
}
else if (!Zotero.rtl && event.key === 'ArrowLeft'
|| Zotero.rtl && event.key === 'ArrowRight'
|| event.key === 'ArrowUp') {
if (event.target === notesPaneToggle) {
itemPaneToggle.focus();
}
else if (event.target === itemPaneToggle) {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
reader.focusLastToolbarButton();
}
}
}
else if (event.key === 'Tab'
&& [itemPaneToggle, notesPaneToggle].includes(event.target)) {
if (event.shiftKey) {
ZoteroContextPane.focus();
}
else {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
reader.tabToolbar();
}
}
event.preventDefault();
event.stopPropagation();
}
else if (event.key === 'Escape') {
if (event.key === 'Escape') {
if (!document.activeElement.classList.contains('reader')) {
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
if (reader) {
@ -6209,13 +6172,16 @@ var ZoteroPane = new function()
this.updateLayout = function() {
var layoutSwitcher = document.getElementById("zotero-layout-switcher");
var itemsSplitter = document.getElementById("zotero-items-splitter");
var sidenav = document.getElementById("zotero-view-item-sidenav");
if(Zotero.Prefs.get("layout") === "stacked") { // itemsPane above itemPane
layoutSwitcher.setAttribute("orient", "vertical");
itemsSplitter.setAttribute("orient", "vertical");
sidenav.classList.add("stacked");
} else { // three-vertical-pane
layoutSwitcher.setAttribute("orient", "horizontal");
itemsSplitter.setAttribute("orient", "horizontal");
sidenav.classList.remove("stacked");
}
this.updateToolbarPosition();
@ -6323,6 +6289,7 @@ var ZoteroPane = new function()
var collectionsPane = document.getElementById("zotero-collections-pane");
var tagSelector = document.getElementById("zotero-tag-selector");
var sidenav = document.getElementById("zotero-view-item-sidenav");
var collectionsPaneWidth = collectionsPane.getBoundingClientRect().width;
tagSelector.style.maxWidth = collectionsPaneWidth + 'px';
@ -6331,6 +6298,8 @@ var ZoteroPane = new function()
}
this.handleTagSelectorResize();
sidenav.render();
}
/**

View file

@ -839,12 +839,6 @@
<div id="zotero-tab-cover" class="hidden" xmlns="http://www.w3.org/1999/xhtml">
<label>&zotero.general.loading;</label>
</div>
<div id="zotero-tab-toolbar" class="toolbar" hidden="true" xmlns="http://www.w3.org/1999/xhtml">
<div id="zotero-tb-split" class="split-button">
<button id="zotero-tb-toggle-item-pane" class="toolbarButton item" title="&zotero.toolbar.context.item;" tabindex="-1"><span/></button>
<button id="zotero-tb-toggle-notes-pane" class="toolbarButton notes" title="&zotero.toolbar.context.notes;" tabindex="-1"><span/></button>
</div>
</div>
<deck id="tabs-deck" flex="1">
<vbox id="zotero-pane"
onkeydown="ZoteroPane_Local.handleKeyDown(event, this.id)"
@ -1141,8 +1135,6 @@
<related-box id="zotero-editpane-related" class="zotero-editpane-related" data-pane="related"/>
</html:div>
</html:div>
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
</hbox>
<!-- Note item -->
@ -1182,6 +1174,8 @@
</vbox>
</deck>
</vbox>
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
</box>
</hbox>
</vbox>
@ -1222,6 +1216,8 @@
</vbox>
</box>
<item-pane-sidenav id="zotero-context-pane-sidenav" class="zotero-view-item-sidenav" hidden="true"/>
<popupset>
<menupopup id="context-pane-add-child-note-button-popup">
<menuitem id="context-pane-add-child-note" label="&zotero.context.addChildNote;"/>

View file

@ -272,6 +272,10 @@ sidenav-tags =
.tooltiptext = { pane-tags }
sidenav-related =
.tooltiptext = { pane-related }
sidenav-collapse =
.tooltiptext = Collapse Sidebar
sidenav-expand =
.tooltiptext = Expand Sidebar
pin-section =
.label = Pin Section
@ -288,3 +292,6 @@ abstract-field =
tagselector-search =
.placeholder = Filter Tags
context-notes-search =
.placeholder = Search Notes

View file

@ -458,7 +458,6 @@ pane.item.viewOnline.tooltip = Go to this item online
pane.context.noParent = No parent item
pane.context.itemNotes = Item Notes
pane.context.allNotes = All Notes
pane.context.noNotes = No notes
itemTypes.note = Note
itemTypes.annotation = Annotation

View file

@ -16,10 +16,6 @@
pointer-events: none;
}
#zotero-context-pane.stacked #zotero-context-toolbar-extension {
display: none;
}
#zotero-context-pane .stacked-context-placeholder {
min-height: 300px;
}
@ -34,6 +30,7 @@
#zotero-context-pane-inner {
width: 0;
background: var(--material-sidepane);
font: message-box;
font-size: inherit;
min-height: 100px;
@ -41,38 +38,12 @@
pointer-events: auto;
}
/*#zotero-context-pane.standard .stacked-splitter {*/
/* display: none;*/
/*}*/
#zotero-tab-toolbar {
position: fixed;
z-index: 1;
-moz-appearance: none;
background: linear-gradient(to top, #a9a9a9 0, #a9a9a9 1px, #f6f6f6 1px, #f6f6f6 100%);
}
#zotero-tab-toolbar .toolbarButton {
-moz-window-dragging: no-drag;
}
#zotero-item-toolbar {
height: 32px;
}
#zotero-context-toolbar-extension {
height: 32px;
/* To cover the splitter that has a higher stacking order than our parent */
opacity: 0.99;
width: 100%;
margin-inline-start: -5px;
background: linear-gradient(to top, #a9a9a9 0, #a9a9a9 1px, #f6f6f6 1px, #f6f6f6 100%);
-moz-window-dragging: drag;
}
.zotero-context-notes-list {
padding-top: 5px;
background-color: #d2d8e2;
background-color: var(--material-sidepane);
}
.zotero-context-pane-editor-parent-line {

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 18H16.75L16.75 2H18V18ZM12.4911 10.625L2 10.625V9.375L12.4911 9.375L9 5.88388L9.88388 5L14.8839 10L9.88388 15L9 14.1161L12.4911 10.625Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 315 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 18H16.75L16.75 2H18L18 18ZM4.50898 9.375L8.00009 5.88388L7.11621 5L2.11621 10L7.11621 15L8.00009 14.1161L4.50898 10.625L15 10.625V9.375L4.50898 9.375Z" fill="context-fill"/>
</svg>

After

Width:  |  Height:  |  Size: 330 B

View file

@ -74,3 +74,4 @@
@import "elements/attachmentsBox";
@import "elements/attachmentRow";
@import "elements/annotationRow";
@import "elements/noteRow";

View file

@ -454,44 +454,3 @@ $toolbar-btn-icon-active-offset: 0;
}
}
}
// Move this out from mixins
#zotero-tb-split {
direction: ltr;
$item-tool-icon: "mac/item";
$item-tool-icon-active: "mac/item-white";
$notes-tool-icon: "mac/notes";
$notes-tool-icon-active: "mac/notes-white";
&.hidden {
display: none;
}
.item {
@include icon($item-tool-icon);
&.toggled {
@include icon($item-tool-icon-active);
}
}
.notes {
@include icon($notes-tool-icon);
&.toggled {
@include icon($notes-tool-icon-active);
}
}
.toolbarButton {
box-sizing: border-box;
@include split-button(
$height: 24px,
$button: "menubutton",
$padding-x: 11px,
$padding-y: 4px
);
}
}

View file

@ -81,12 +81,12 @@ $z-index-loading-cover: 60;
// Item pane
// --------------------------------------------------
$item-pane-sections: (
"info": var(--accent-blue),
"abstract": var(--accent-teal),
"attachments": var(--accent-green),
"notes": var(--accent-yellow),
"tags": var(--accent-orange),
"related": var(--accent-wood),
"info": var(--accent-blue),
"abstract": var(--accent-teal),
"attachments": var(--accent-green),
"notes": var(--accent-yellow),
"tags": var(--accent-orange),
"related": var(--accent-wood),
);
$tagColorsLookup: (

View file

@ -4,150 +4,49 @@
overflow-y: auto;
flex-grow: 1;
background: var(--material-sidepane);
border-top: var(--material-border);
border-top: var(--material-border-quinary);
padding-inline: 8px;
scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-background);
}
.notes-list {
context-notes-list {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
height: 0;
flex-grow: 1;
padding-top: 2px;
& > section {
margin: 5px 0;
}
}
.header-row {
margin: 0 7px;
display: flex;
justify-content: space-between;
height: 24px;
h2 {
font-weight: bold;
margin: 0;
font-size: 13px;
align-self: center;
}
button {
height: 24px;
padding-left: 4px;
padding-right: 4px;
// Necessary on linux to horizontaly center text
line-height: 0;
}
}
.empty-row {
margin: 4px 7px 0;
text-align: center;
}
.note-row {
border: 1px solid var(--fill-quinary);
border-radius: 5px;
margin: 4px 7px;
background: var(--material-background);
&:active {
background: var(--accent-blue50);
}
.inner {
> *:not(:first-child) {
margin-top: 3px;
.item-notes > .body, .all-notes > .body {
display: flex;
flex-direction: column;
flex: 1;
gap: 6px;
@include comfortable {
gap: 8px;
}
.parent-line {
display: flex;
width: calc(100% - 16px);
color: var(--fill-secondary);
border-bottom: 1px solid var(--fill-quinary);
align-items: center;
padding: 5px 8px 4px;
margin-bottom: 5px;
&::before, &::after {
// Apply gap to start and end
content: '';
}
.parent-item-type {
margin-right: 3px;
width: 16px; // Don't show HiDPI icons at 2x size
}
.parent-title {
flex-grow: 1;
width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-line {
display: flex;
padding: 0 8px 0;
.title {
flex-grow: 1;
width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: bold;
}
}
.body-line {
display: flex;
padding: 0 8px 6px;
.date {
color: var(--fill-primary);
> .more {
appearance: none;
border-radius: 5px;
border: none;
outline: 1px solid var(--fill-quinary);
background: var(--material-background);
padding: 2px 8px;
@include comfortable {
padding-block: 4px;
}
.body {
flex-grow: 1;
width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-left: 10px;
color: var(--fill-primary);
&:active {
background-color: var(--accent-blue10);
}
}
}
}
.more-row {
color: var(--fill-primary);
border: 1px solid var(--fill-quinary);
border-radius: 5px;
margin: 4px 7px;
background: var(--material-background);
text-align: center;
padding: 5px;
&:active {
background-color: var(--accent-blue50);
}
}
.standalone-note-row {
.title-line {
padding-top: 6px !important;
}
}
.note-row, .more-row {
outline: 0;
&:-moz-focusring {
box-shadow: $toolbar-btn-focus-box-shadow;
z-index: 1;
@include retina {
box-shadow: $toolbar-btn-focus-box-shadow-2x;
}
}
}

View file

@ -24,12 +24,6 @@ collapsible-section {
gap: 4px;
color: var(--fill-secondary);
font-weight: 590;
&::before {
content: '';
width: 16px;
height: 16px;
}
}
toolbarbutton {
@ -78,6 +72,9 @@ collapsible-section {
@each $pane, $color in $item-pane-sections {
&[data-pane="#{$pane}"] {
& > .head > .title::before {
content: '';
width: 16px;
height: 16px;
background: icon-url("itempane/16/#{$pane}.svg") no-repeat center;
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
fill: $color;

View file

@ -1,10 +1,25 @@
@use "sass:map";
item-pane-sidenav {
display: flex;
&:not([hidden]) {
display: flex;
}
flex-direction: column;
padding: 6px 4px;
align-items: flex-start;
border-inline-start: var(--material-panedivider);
padding: 6px 4px 0;
align-items: center;
gap: 6px;
border-left: var(--material-panedivider);
background: var(--material-sidepane);
position: relative; // Stop item pane content from overlapping
&.stacked {
flex-direction: row;
border-inline-start: none;
border-top: var(--material-panedivider);
padding-inline-start: 6px;
padding-inline-end: 0;
padding-block: 4px;
}
.toolbarbutton-container {
position: relative;
@ -49,7 +64,7 @@ item-pane-sidenav {
background-color: var(--fill-quinary);
}
&:active {
&:active, &.active {
background-color: var(--fill-quarternary);
}
@ -65,6 +80,45 @@ item-pane-sidenav {
stroke: $color;
}
}
// Special case for Notes context pane button:
&[data-pane="context-notes"] {
list-style-image: url("chrome://zotero/skin/itempane/20/notes.svg");
fill: map.get($item-pane-sections, "notes");
stroke: map.get($item-pane-sections, "notes");
}
// Special case for Expand/Collapse button:
&[data-pane="toggle-collapse"] {
list-style-image: url("chrome://zotero/skin/itempane/20/collapse.svg");
&.collapsed {
list-style-image: url("chrome://zotero/skin/itempane/20/expand.svg");
}
:dir(rtl) {
transform: scaleX(-1);
}
fill: var(--fill-secondary);
stroke: var(--fill-secondary);
}
}
&.stacked toolbarbutton[data-pane="toggle-collapse"] {
transform: rotate(90deg);
}
&:not(.stacked) > .divider {
width: 20px;
height: 0;
border-bottom: 1px solid var(--fill-quinary);
}
&.stacked > .divider {
width: 0;
height: 20px;
border-inline-end: 1px solid var(--fill-quinary);
}
menupopup {

View file

@ -1,30 +1,7 @@
#links-box {
.grid {
display: grid;
grid-template-columns: auto 1fr;
links-box {
display: flex;
flex-direction: column;
padding-inline: 8px;
& > * {
margin-top: 1px !important;
margin-bottom: 1px !important;
padding: 0 2px 0 2px !important;
}
.label {
color: #7f7f7f;
text-align: right;
font-weight: bold;
min-width: 62px;
border: var(--material-border-transparent);
}
.value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
}
}
#related-popup, #tags-popup {
font: inherit;
border-top: 1px solid var(--fill-quinary);
}

View file

@ -0,0 +1,59 @@
note-row {
display: flex;
flex-direction: column;
border-radius: 5px;
outline: 1px solid var(--fill-quinary);
background: var(--material-background);
&:active {
background-color: var(--accent-blue10);
}
.head, .body {
padding: 2px 8px;
@include comfortable {
padding-block: 4px;
}
}
.head {
display: flex;
align-items: center;
gap: 5px;
border-bottom: 1px solid var(--fill-quinary);
.icon {
flex-shrink: 0;
}
.parent-title {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: var(--fill-secondary);
}
}
.body {
display: flex;
flex-direction: column;
@include comfortable {
gap: 2px;
}
.note-title {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
font-weight: 590;
}
.note-date {
float: right;
color: var(--fill-secondary);
}
}
}