From 583c6328a0c201bf22195934863115ec1bfb7f30 Mon Sep 17 00:00:00 2001 From: Abe Jellinek Date: Fri, 22 Dec 2023 23:54:08 -0800 Subject: [PATCH] Unify context pane into new item pane/sidenav design --- .../content/zotero-platform/mac/overlay.css | 51 +- .../zotero/components/itemPane/notesList.jsx | 168 ------- .../zotero/components/itemPane/tagsBox.jsx | 442 ------------------ .../zotero/containers/tagsBoxContainer.jsx | 126 ----- chrome/content/zotero/contextPane.js | 206 +++----- chrome/content/zotero/customElements.js | 2 + .../zotero/elements/collapsibleSection.js | 30 +- .../zotero/elements/contextNotesList.js | 194 ++++++++ .../zotero/elements/itemPaneSidenav.js | 122 ++++- chrome/content/zotero/elements/noteEditor.js | 143 +----- chrome/content/zotero/elements/noteRow.js | 91 ++++ chrome/content/zotero/xpcom/reader.js | 8 - chrome/content/zotero/zoteroPane.js | 45 +- chrome/content/zotero/zoteroPane.xhtml | 12 +- chrome/locale/en-US/zotero/zotero.ftl | 7 + chrome/locale/en-US/zotero/zotero.properties | 1 - chrome/skin/default/zotero/contextPane.css | 33 +- .../default/zotero/itempane/20/collapse.svg | 3 + .../default/zotero/itempane/20/expand.svg | 3 + scss/_zotero.scss | 1 + scss/abstracts/_split-button.scss | 41 -- scss/abstracts/_variables.scss | 12 +- scss/components/_notesList.scss | 161 ++----- scss/elements/_collapsibleSection.scss | 9 +- scss/elements/_itemPaneSidenav.scss | 64 ++- scss/elements/_noteEditor.scss | 33 +- scss/elements/_noteRow.scss | 59 +++ 27 files changed, 722 insertions(+), 1345 deletions(-) delete mode 100644 chrome/content/zotero/components/itemPane/notesList.jsx delete mode 100644 chrome/content/zotero/components/itemPane/tagsBox.jsx delete mode 100644 chrome/content/zotero/containers/tagsBoxContainer.jsx create mode 100644 chrome/content/zotero/elements/contextNotesList.js create mode 100644 chrome/content/zotero/elements/noteRow.js create mode 100644 chrome/skin/default/zotero/itempane/20/collapse.svg create mode 100644 chrome/skin/default/zotero/itempane/20/expand.svg create mode 100644 scss/elements/_noteRow.scss diff --git a/chrome/content/zotero-platform/mac/overlay.css b/chrome/content/zotero-platform/mac/overlay.css index 578332313f..ddc6a6c350 100644 --- a/chrome/content/zotero-platform/mac/overlay.css +++ b/chrome/content/zotero-platform/mac/overlay.css @@ -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; diff --git a/chrome/content/zotero/components/itemPane/notesList.jsx b/chrome/content/zotero/components/itemPane/notesList.jsx deleted file mode 100644 index ba6db84770..0000000000 --- a/chrome/content/zotero/components/itemPane/notesList.jsx +++ /dev/null @@ -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 . - - ***** 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 ( -
onClick(id)} - onContextMenu={(event) => onContextMenu(id, event)} - onKeyDown={onKeyDown} - > -
- { parentItemType - ?
- - {parentTitle} -
- : null - } -
-
{title}
-
-
-
{date}
-
{body}
-
-
-
- ); -}); - -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 ( -
- {hasParent &&
-
-

{Zotero.getString('pane.context.itemNotes')}

- -
- {!childNotes.length &&
{Zotero.getString('pane.context.noNotes')}
} - {childNotes.map(note => )} -
} -
-
-

{Zotero.getString('pane.context.allNotes')}

- -
- {!allNotes.length &&
{Zotero.getString('pane.context.noNotes')}
} - {visibleNotes.map(note => )} - {allNotes.length > visibleNotes.length - &&
{ - Zotero.getString('general.numMore', Zotero.Utilities.numberFormat( - [allNotes.length - visibleNotes.length], 0)) - }
- } -
-
- ); -}); - -export default NotesList; diff --git a/chrome/content/zotero/components/itemPane/tagsBox.jsx b/chrome/content/zotero/components/itemPane/tagsBox.jsx deleted file mode 100644 index 8d5f0db0c5..0000000000 --- a/chrome/content/zotero/components/itemPane/tagsBox.jsx +++ /dev/null @@ -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 . - - ***** 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 ( -
  • - {title} window.Zotero_Tooltip.start(title)} - onMouseOut={() => window.Zotero_Tooltip.stop()} - style={{ width: "16px", height: "16px" }} - onClick={props.editable ? (() => setSelectedTag(tag.tag)) : undefined} - /> -
    - -
    - {props.editable - && ()} -
  • - ); - } - - // 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 ( -
    -
    -
    {renderCount()}
    - { props.editable &&
    } -
    -
    -
      - {displayTags.map(tag => renderTagRow(tag))} -
    - { props.editable && } -
    -
    - ); -}); - -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; \ No newline at end of file diff --git a/chrome/content/zotero/containers/tagsBoxContainer.jsx b/chrome/content/zotero/containers/tagsBoxContainer.jsx deleted file mode 100644 index 72fc46af87..0000000000 --- a/chrome/content/zotero/containers/tagsBoxContainer.jsx +++ /dev/null @@ -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 . - - ***** 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 ; -} - -export default React.forwardRef(TagsBoxContainer); diff --git a/chrome/content/zotero/contextPane.js b/chrome/content/zotero/contextPane.js index 2d2602c8f1..a7f1035fce 100644 --- a/chrome/content/zotero/contextPane.js +++ b/chrome/content/zotero/contextPane.js @@ -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( - { - 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; + } } }; diff --git a/chrome/content/zotero/customElements.js b/chrome/content/zotero/customElements.js index 794ad5d6ba..350d7c022d 100644 --- a/chrome/content/zotero/customElements.js +++ b/chrome/content/zotero/customElements.js @@ -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 s diff --git a/chrome/content/zotero/elements/collapsibleSection.js b/chrome/content/zotero/elements/collapsibleSection.js index c96955c4e7..ce9977720c 100644 --- a/chrome/content/zotero/elements/collapsibleSection.js +++ b/chrome/content/zotero/elements/collapsibleSection.js @@ -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() { diff --git a/chrome/content/zotero/elements/contextNotesList.js b/chrome/content/zotero/elements/contextNotesList.js new file mode 100644 index 0000000000..f1639bab09 --- /dev/null +++ b/chrome/content/zotero/elements/contextNotesList.js @@ -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 . + + ***** END LICENSE BLOCK ***** +*/ + +"use strict"; + +{ + const MAX_UNEXPANDED_ALL_NOTES = 7; + + class ContextNotesList extends XULElementBase { + content = MozXULElement.parseXULToFragment(` + + + + + + + + + + + `, ['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); +} diff --git a/chrome/content/zotero/elements/itemPaneSidenav.js b/chrome/content/zotero/elements/itemPaneSidenav.js index 06708aed7a..2bf8d62245 100644 --- a/chrome/content/zotero/elements/itemPaneSidenav.js +++ b/chrome/content/zotero/elements/itemPaneSidenav.js @@ -28,6 +28,11 @@ { class ItemPaneSidenav extends XULElementBase { content = MozXULElement.parseXULToFragment(` + + + + + + + + @@ -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 diff --git a/chrome/content/zotero/elements/noteEditor.js b/chrome/content/zotero/elements/noteEditor.js index ad92244d3a..c3dd8c3b66 100644 --- a/chrome/content/zotero/elements/noteEditor.js +++ b/chrome/content/zotero/elements/noteEditor.js @@ -44,7 +44,7 @@