mirror of
https://github.com/zotero/zotero.git
synced 2026-09-07 08:26:14 +00:00
annotations showing in itemTree (#3416)
- Annotations are displayed in itemTree under their file attachments on a third level. The annotation spans the entire row. - The title is constructed on the go. When possible, it includes annotation quote and comment as pseudo-columns of the row. The comment occupies about twice as much space as the quote. Otherwise, (if there is no quote) only the annotation comment is included as the "title" part of the row - Non-CJK segments of the quote part of the annotation row are italicized. CJK segments are left as is. If CJK segments are present, there is more padding between quote and comment parts. - Search matches the actual attachment instead of its parent file. - Can create child notes from annotations of the same item or standalone notes from annotations across different items from the context menu or the header button. - When an annotation (or multiple annotations) are selected, the annotationItemPane component is displayed where annotations are grouped by their top-level item. Annotations are displayed fully, without having their content cut off. - Special treatment for annotations to always prompt to erase the item regardless of what collectionTree row is selected (e.g., if a collection is selected, we still want one to be able to delete the annotation). This only applies if all selected items are annotations. If multiple items are selected, some annotations and some not, do nothing. This is until the trash is ready. In the future, we may send annotations to trash - strip all HTML tags from annotation for now, until the logic to properly render annotation markup is copied over from the reader (applies to both annotation-row component and the annotation item rendered in the itemTree) - Added a generalized "expandToItem" function to itemTree to expand all ancestors of a given item, similar to "expandToCollection" from collectionTree - add annotation conditions to advanced search - show [Image not available] if no annotation file for ink or image annotations - only keep annotation-specific context menu options when some annotations are selected in itemTree - enable Quick Copy of annotations from itemTree via drag-drop, shortcut key, or Edit → Copy Annotation - Minor refactoring of Zotero.Annotation.toJSON() to pull out async code that handles ink and image annotations, so that Zotero.Annotation.toJSONsync() for highlight, underline, and note annotations does not have to be awaited. Since ink and image annotation don't seem to work for drag-drop Quick Copy, they are just skipped for now.
This commit is contained in:
parent
3144314d20
commit
48fd23ccec
28 changed files with 828 additions and 110 deletions
|
|
@ -68,6 +68,7 @@ Services.scriptloader.loadSubScript('chrome://zotero/content/elements/itemTreeMe
|
|||
['attachment-row', 'chrome://zotero/content/elements/attachmentRow.js'],
|
||||
['attachment-annotations-box', 'chrome://zotero/content/elements/attachmentAnnotationsBox.js'],
|
||||
['annotation-row', 'chrome://zotero/content/elements/annotationRow.js'],
|
||||
['annotation-items-pane', 'chrome://zotero/content/elements/annotationItemsPane.js'],
|
||||
['context-notes-list', 'chrome://zotero/content/elements/contextNotesList.js'],
|
||||
['note-row', 'chrome://zotero/content/elements/noteRow.js'],
|
||||
['notes-context', 'chrome://zotero/content/elements/notesContext.js'],
|
||||
|
|
|
|||
134
chrome/content/zotero/elements/annotationItemsPane.js
Normal file
134
chrome/content/zotero/elements/annotationItemsPane.js
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2024 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 *****
|
||||
*/
|
||||
|
||||
{
|
||||
class AnnotationItemsPane extends XULElementBase {
|
||||
content = MozXULElement.parseXULToFragment(`
|
||||
<html:div class="custom-head"></html:div>
|
||||
<html:div class="body zotero-view-item"> </html:div>
|
||||
`);
|
||||
|
||||
set items(items) {
|
||||
if (items.some(item => !item.isAnnotation())) return;
|
||||
this._items = items;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items || [];
|
||||
}
|
||||
|
||||
init() {
|
||||
this._body = this.querySelector('.body');
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item']);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
}
|
||||
|
||||
notify(action, type, ids) {
|
||||
if (action == 'modify') {
|
||||
for (let id of ids) {
|
||||
let updatedItem = Zotero.Items.get(id);
|
||||
// If a selected annotation is renamed, re-render its annotation row
|
||||
if (updatedItem.isAnnotation()) {
|
||||
let row = this.querySelector(`annotation-row[annotation-id="${id}"]`);
|
||||
if (row) {
|
||||
row.render();
|
||||
}
|
||||
}
|
||||
// Update name of collapsible section if item is renamed
|
||||
else if (updatedItem.isRegularItem()) {
|
||||
let section = this.querySelector(`collapsible-section[data-item-id="${id}"]`);
|
||||
if (section) {
|
||||
section.summary = updatedItem.getDisplayTitle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
let topLevelItems = Zotero.Items.getTopLevel(this.items);
|
||||
|
||||
// Remove collapsible sections for top-level items whose annotations are no longer selected
|
||||
for (let section of [...this.querySelectorAll("collapsible-section")]) {
|
||||
let parentID = section.dataset.pane.split("-")[1];
|
||||
if (!topLevelItems.some(item => item.id == parentID)) {
|
||||
section.remove();
|
||||
}
|
||||
}
|
||||
for (let parentItem of topLevelItems) {
|
||||
let selectedAnnotations = this.items.filter(item => item.topLevelItem.id == parentItem.id);
|
||||
// Create a collapsible section for each top-level item if it does not exist yet
|
||||
let section = this.querySelector(`[data-pane="annotations-${parentItem.id}"]`);
|
||||
if (!section) {
|
||||
section = document.createXULElement("collapsible-section");
|
||||
section.dataset.l10nId = "section-attachments-annotations";
|
||||
section.dataset.pane = `annotations-${parentItem.id}`;
|
||||
section.summary = parentItem.getDisplayTitle();
|
||||
|
||||
let sectionBody = document.createElement("div");
|
||||
sectionBody.classList.add("body");
|
||||
|
||||
section.appendChild(sectionBody);
|
||||
this._body.append(section);
|
||||
}
|
||||
document.l10n.setArgs(section, { count: selectedAnnotations.length });
|
||||
// Add annotations into this collapsible section
|
||||
for (let annotation of selectedAnnotations) {
|
||||
// Skip rows that already exist
|
||||
if (this.querySelector(`annotation-row[annotation-id="${annotation.id}"]`)) continue;
|
||||
let row = document.createXULElement('annotation-row');
|
||||
row.annotation = annotation;
|
||||
section.querySelector('.body').append(row);
|
||||
}
|
||||
}
|
||||
// Remove annotation rows for annotations that are no longer selected
|
||||
for (let row of [...this.querySelectorAll("annotation-row")]) {
|
||||
let rowID = row.getAttribute("annotation-id");
|
||||
if (!this.items.some(obj => obj.id == rowID)) {
|
||||
row.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderCustomHead(callback) {
|
||||
let customHead = this.querySelector(".custom-head");
|
||||
customHead.replaceChildren();
|
||||
let append = (...args) => {
|
||||
customHead.append(...args);
|
||||
};
|
||||
if (callback) callback({
|
||||
doc: document,
|
||||
append,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("annotation-items-pane", AnnotationItemsPane);
|
||||
}
|
||||
|
|
@ -88,20 +88,34 @@
|
|||
img.src = Zotero.File.pathToFileURI(imagePath);
|
||||
img.draggable = false;
|
||||
this._body.append(img);
|
||||
// if the image could not be loaded for some reason (e.g. file is not there),
|
||||
// show a placeholder text
|
||||
img.addEventListener('error', () => {
|
||||
let placeholder = document.createElement('div');
|
||||
placeholder.classList.add('comment');
|
||||
document.l10n.setAttributes(placeholder, 'annotation-image-not-available');
|
||||
this._body.replaceChildren(placeholder);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Strip all html tags from comment and text for now until the algorithm for safe
|
||||
// rendering of relevant html tags is carried over from the reader
|
||||
let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
|
||||
|
||||
if (this._annotation.annotationText) {
|
||||
let text = document.createElement('div');
|
||||
text.classList.add('quote');
|
||||
text.textContent = this._annotation.annotationText;
|
||||
let plainQuote = parserUtils.convertToPlainText(this._annotation.annotationText, Ci.nsIDocumentEncoder.OutputRaw, 0);
|
||||
text.textContent = plainQuote;
|
||||
this._body.append(text);
|
||||
}
|
||||
|
||||
if (this._annotation.annotationComment) {
|
||||
let comment = document.createElement('div');
|
||||
comment.classList.add('comment');
|
||||
comment.textContent = this._annotation.annotationComment;
|
||||
let plainComment = parserUtils.convertToPlainText(this._annotation.annotationComment, Ci.nsIDocumentEncoder.OutputRaw, 0);
|
||||
comment.textContent = plainComment;
|
||||
this._body.append(comment);
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +124,9 @@
|
|||
this._tags.textContent = tags.map(tag => tag.tag).sort(Zotero.localeCompare).join(Zotero.getString('punctuation.comma') + ' ');
|
||||
|
||||
this.style.setProperty('--annotation-color', this._annotation.annotationColor);
|
||||
// A11y - make focusable + add screen reader's labels
|
||||
this.setAttribute("tabindex", 0);
|
||||
this.setAttribute("aria-label", this.annotation.getDisplayTitle());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -372,11 +372,11 @@
|
|||
}
|
||||
|
||||
get _disableCollapsing() {
|
||||
return !!this.closest('panel, menupopup, merge-pane, scaffold-item-preview');
|
||||
return !!this.closest('panel, menupopup, merge-pane, scaffold-item-preview, annotation-items-pane');
|
||||
}
|
||||
|
||||
get _disableSavingOpenState() {
|
||||
return !!this.closest('merge-pane, scaffold-item-preview');
|
||||
return !!this.closest('merge-pane, scaffold-item-preview, annotation-items-pane');
|
||||
}
|
||||
|
||||
_handleClick = (event) => {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
previousfocus="zotero-items-tree" />
|
||||
|
||||
<duplicates-merge-pane id="zotero-duplicates-merge-pane" />
|
||||
|
||||
<annotation-items-pane id="zotero-annotations-pane" />
|
||||
</deck>
|
||||
<item-pane-sidenav id="zotero-view-item-sidenav" class="zotero-view-item-sidenav"/>
|
||||
`);
|
||||
|
|
@ -45,6 +47,7 @@
|
|||
this._noteEditor = this.querySelector("#zotero-note-editor");
|
||||
this._duplicatesPane = this.querySelector("#zotero-duplicates-merge-pane");
|
||||
this._messagePane = this.querySelector("#zotero-item-message");
|
||||
this._annotationsPane = this.querySelector("#zotero-annotations-pane");
|
||||
this._sidenav = this.querySelector("#zotero-view-item-sidenav");
|
||||
this._deck = this.querySelector("#zotero-item-pane-content");
|
||||
|
||||
|
|
@ -108,6 +111,10 @@
|
|||
if (!this.data) return false;
|
||||
let hideSidenav = false;
|
||||
let renderStatus = false;
|
||||
// Only annotations selected
|
||||
if (this.data.length > 0 && this.data.every(item => item.isAnnotation())) {
|
||||
return renderStatus = this.renderAnnotations(this.data);
|
||||
}
|
||||
// Single item selected
|
||||
if (this.data.length == 1) {
|
||||
let item = this.data[0];
|
||||
|
|
@ -140,6 +147,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
renderAnnotations(annotations) {
|
||||
this.mode = "annotations";
|
||||
let annotationsViewer = document.getElementById("zotero-annotations-pane");
|
||||
annotationsViewer.items = annotations;
|
||||
annotationsViewer.render();
|
||||
return true;
|
||||
}
|
||||
|
||||
renderNoteEditor(item) {
|
||||
this.mode = "note";
|
||||
|
||||
|
|
@ -318,6 +333,11 @@
|
|||
this.updateReadLabel();
|
||||
return;
|
||||
}
|
||||
// Create note from annotations button
|
||||
if (this.data.every(item => item.isAnnotation())) {
|
||||
container.renderCustomHead(this.renderAnnotationsHead.bind(this));
|
||||
return;
|
||||
}
|
||||
|
||||
container.renderCustomHead();
|
||||
}
|
||||
|
|
@ -381,6 +401,21 @@
|
|||
this.setTranslateButton();
|
||||
}
|
||||
|
||||
renderAnnotationsHead(data) {
|
||||
let { doc, append } = data;
|
||||
let button = doc.createXULElement("button");
|
||||
button.id = 'zotero-item-pane-note-from-annotations';
|
||||
if (Zotero.Items.getTopLevel(this.data).length == 1) {
|
||||
button.label = Zotero.getString('pane.items.menu.addNoteFromAnnotations');
|
||||
button.addEventListener("command", () => ZoteroPane.addNoteFromAnnotationsFromSelected());
|
||||
}
|
||||
else {
|
||||
button.label = Zotero.getString('pane.items.menu.createNoteFromAnnotations');
|
||||
button.addEventListener("command", () => ZoteroPane.createStandaloneNoteFromAnnotationsFromSelected());
|
||||
}
|
||||
append(button);
|
||||
}
|
||||
|
||||
updateReadLabel() {
|
||||
var items = this.data;
|
||||
var isUnread = false;
|
||||
|
|
@ -493,8 +528,12 @@
|
|||
getCurrentPane(mode = undefined) {
|
||||
if (!mode) {
|
||||
// Guess a mode from the current data
|
||||
// Only annotation items selected
|
||||
if (this.data.every(item => item.isAnnotation())) {
|
||||
mode = "annotations";
|
||||
}
|
||||
// No/multiple objects are selected OR selected object is a trashed collection/search
|
||||
if (!this.data.length || this.data.length > 1
|
||||
else if (!this.data.length || this.data.length > 1
|
||||
|| this.data[0] instanceof Zotero.Collection || this.data[0] instanceof Zotero.Search) {
|
||||
mode = "message";
|
||||
}
|
||||
|
|
@ -510,6 +549,7 @@
|
|||
item: "_itemDetails",
|
||||
note: "_noteEditor",
|
||||
duplicates: "_duplicatesPane",
|
||||
annotations: "_annotationsPane"
|
||||
};
|
||||
return this[map[mode]];
|
||||
}
|
||||
|
|
@ -588,6 +628,10 @@
|
|||
}
|
||||
break;
|
||||
}
|
||||
case "annotations": {
|
||||
this._deck.selectedIndex = 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let isViewingItem = type == "item";
|
||||
if (previousViewType != "item" && isViewingItem) {
|
||||
|
|
|
|||
|
|
@ -268,8 +268,6 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
.concat(await this.collectionTreeRow.getTrashedCollections())
|
||||
.concat(await Zotero.Searches.getDeleted(this.collectionTreeRow.ref.libraryID));
|
||||
}
|
||||
// TEMP: Hide annotations
|
||||
newSearchItems = newSearchItems.filter(item => !item.isAnnotation());
|
||||
// Remove notes and attachments if necessary
|
||||
if (this.props.regularOnly) {
|
||||
newSearchItems = newSearchItems.filter((item) => {
|
||||
|
|
@ -309,7 +307,8 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
if (row.ref instanceof Zotero.Item && row.ref.parentID) {
|
||||
continue;
|
||||
}
|
||||
let isSearchParent = newSearchParentIDs.has(row.ref.treeViewID);
|
||||
let attachments = row.ref.isRegularItem() ? row.ref.getAttachments() : [];
|
||||
let isSearchParent = newSearchParentIDs.has(row.ref.treeViewID) || attachments.some(id => newSearchParentIDs.has(id));
|
||||
// If not showing children or no children match the search, close
|
||||
if (this.props.regularOnly || !isSearchParent) {
|
||||
row.isOpen = false;
|
||||
|
|
@ -333,8 +332,10 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
else if (skipChildren) {
|
||||
continue;
|
||||
}
|
||||
newRows.push(row);
|
||||
allItemIDs.add(row.ref.treeViewID);
|
||||
if (!allItemIDs.has(row.ref.id)) {
|
||||
newRows.push(row);
|
||||
allItemIDs.add(row.ref.treeViewID);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new items
|
||||
|
|
@ -348,6 +349,14 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
continue;
|
||||
}
|
||||
item = Zotero.Items.get(parentItemID);
|
||||
// Go up one more level to check for parents of annotation rows
|
||||
let parentsParent = item.parentItemID;
|
||||
if (parentsParent) {
|
||||
if (allItemIDs.has(parentsParent)) {
|
||||
continue;
|
||||
}
|
||||
item = Zotero.Items.get(parentsParent);
|
||||
}
|
||||
}
|
||||
// Parent item may have already been added from child
|
||||
else if (allItemIDs.has(item.treeViewID)) {
|
||||
|
|
@ -356,9 +365,11 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
|
||||
// Add new top-level items
|
||||
let row = new ItemTreeRow(item, 0, false);
|
||||
newRows.push(row);
|
||||
allItemIDs.add(item.treeViewID);
|
||||
addedItemIDs.add(item.treeViewID);
|
||||
if (!allItemIDs.has(item.treeViewID)) {
|
||||
newRows.push(row);
|
||||
allItemIDs.add(item.treeViewID);
|
||||
addedItemIDs.add(item.treeViewID);
|
||||
}
|
||||
}
|
||||
|
||||
this._rows = newRows;
|
||||
|
|
@ -1231,15 +1242,11 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Get the row of the parent, if there is one
|
||||
let parent = item.parentItemID;
|
||||
let parentRow = parent && this._rowMap[parent];
|
||||
|
||||
// If row with id isn't visible, check to see if it's hidden under a parent
|
||||
if (row == undefined) {
|
||||
if (!parent || parentRow === undefined) {
|
||||
// No parent -- it's not here
|
||||
|
||||
await this.expandToItem(id);
|
||||
if (!this._rowMap[id]) {
|
||||
// The row is still not found
|
||||
// Clear the quick search and tag selection and try again (once)
|
||||
if (!noRecurse && window.ZoteroPane) {
|
||||
let cleared1 = await window.ZoteroPane.clearQuicksearch();
|
||||
|
|
@ -1253,13 +1260,6 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
Zotero.debug(`Couldn't find row for item ${id} -- not selecting`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If parent is already open and we haven't found the item, the child
|
||||
// hasn't yet been added to the view, so close parent to allow refresh
|
||||
await this._closeContainer(parentRow);
|
||||
|
||||
// Open the parent
|
||||
await this.toggleOpenState(parentRow);
|
||||
}
|
||||
|
||||
// Since we're opening containers, we still need to reference by id
|
||||
|
|
@ -1276,7 +1276,6 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
}
|
||||
rowsToSelect.push(row);
|
||||
}
|
||||
|
||||
if (!rowsToSelect.length) {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1713,10 +1712,14 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
|
||||
//Get children
|
||||
var includeTrashed = this.collectionTreeRow.isTrash();
|
||||
var attachments = item.getAttachments(includeTrashed);
|
||||
var notes = item.getNotes(includeTrashed);
|
||||
var attachments = item.isRegularItem() ? item.getAttachments(includeTrashed) : [];
|
||||
var notes = item.isRegularItem() ? item.getNotes(includeTrashed) : [];
|
||||
var annotations = [];
|
||||
|
||||
var newRows;
|
||||
if (item.isFileAttachment()) {
|
||||
annotations = item.getAnnotations();
|
||||
}
|
||||
var newRows = [];
|
||||
if (attachments.length && notes.length) {
|
||||
newRows = notes.concat(attachments);
|
||||
}
|
||||
|
|
@ -1726,10 +1729,14 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
else if (notes.length) {
|
||||
newRows = notes;
|
||||
}
|
||||
if (annotations.length) {
|
||||
newRows = newRows.concat(annotations);
|
||||
}
|
||||
|
||||
if (newRows) {
|
||||
newRows = Zotero.Items.get(newRows);
|
||||
|
||||
if (!item.isFileAttachment()) {
|
||||
newRows = Zotero.Items.get(newRows);
|
||||
}
|
||||
for (let i = 0; i < newRows.length; i++) {
|
||||
count++;
|
||||
this._addRow(
|
||||
|
|
@ -1766,8 +1773,15 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
|
||||
var savedSelection = this.getSelectedObjects();
|
||||
for (var i=0; i<this.rowCount; i++) {
|
||||
var id = this.getRow(i).ref.id;
|
||||
if (searchParentIDs.has(id) && this.isContainer(i) && !this.isContainerOpen(i)) {
|
||||
if (!this.isContainer(i) || this.isContainerOpen(i)) {
|
||||
continue;
|
||||
}
|
||||
let item = this.getRow(i).ref;
|
||||
let attachments = item.isRegularItem() ? item.getAttachments() : [];
|
||||
// expand item row if it is a parent of a match
|
||||
// OR if it has a child that is a parent of a match
|
||||
let shouldBeOpened = searchParentIDs.has(item.id) || attachments.some(id => searchParentIDs.has(id));
|
||||
if (shouldBeOpened) {
|
||||
this.toggleOpenState(i, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1885,7 +1899,11 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
|
||||
let collectionTreeRow = this.collectionTreeRow;
|
||||
|
||||
if (collectionTreeRow.isBucket()) {
|
||||
// If all selected items are annotations, for now erase them skipping trash
|
||||
if (selectedItems.length && selectedItems.every(item => item.isAnnotation())) {
|
||||
await Zotero.Items.erase(selectedItemIDs);
|
||||
}
|
||||
else if (collectionTreeRow.isBucket()) {
|
||||
collectionTreeRow.ref.deleteItems(ids);
|
||||
}
|
||||
else if (collectionTreeRow.isTrash()) {
|
||||
|
|
@ -2061,8 +2079,9 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
};
|
||||
|
||||
isContainer = (index) => {
|
||||
return this.getRow(index).ref.isRegularItem();
|
||||
};
|
||||
let item = this.getRow(index).ref;
|
||||
return item.isRegularItem() || item.isFileAttachment();
|
||||
}
|
||||
|
||||
isContainerOpen = (index) => {
|
||||
return this.getRow(index).isOpen;
|
||||
|
|
@ -2074,6 +2093,9 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
}
|
||||
|
||||
var item = this.getRow(index).ref;
|
||||
if (item.isFileAttachment()) {
|
||||
return item.numAnnotations() == 0;
|
||||
}
|
||||
if (!item.isRegularItem()) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -2081,6 +2103,38 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
return item.numNotes(includeTrashed) === 0 && item.numAttachments(includeTrashed) == 0;
|
||||
};
|
||||
|
||||
// Expand all ancestors of the specified item id
|
||||
expandToItem = async (id) => {
|
||||
let item = Zotero.Items.get(id);
|
||||
// Stop if the row already exists of if the item is not found
|
||||
if (this._rowMap[id] || !item) return;
|
||||
let toExpand = [];
|
||||
// Collect all ancestors of the item
|
||||
while (item.parentItemID) {
|
||||
item = Zotero.Items.get(item.parentItemID);
|
||||
toExpand.push(item.id);
|
||||
}
|
||||
if (!this.getRow(this._rowMap[item.id])) return;
|
||||
// Go through ancestors starting from the top-most one
|
||||
// and expand them if needed
|
||||
while (toExpand.length > 0) {
|
||||
let ancestorID = toExpand.pop();
|
||||
let ancestorRow = this._rowMap[ancestorID];
|
||||
|
||||
// If the row for the next ancestor already exists, just move one
|
||||
if (toExpand.length > 0) {
|
||||
let nextAncestorID = toExpand[toExpand.length - 1];
|
||||
let nextAncestorRow = this._rowMap[nextAncestorID];
|
||||
if (this.getRow(nextAncestorRow)) continue;
|
||||
}
|
||||
|
||||
// Close and re-open the ancestor to reveal the next row until
|
||||
// we reach the desired item
|
||||
await this._closeContainer(ancestorRow);
|
||||
await this.toggleOpenState(ancestorRow);
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Drag-and-drop methods
|
||||
|
|
@ -2269,6 +2323,11 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Disallow drag of annotation items
|
||||
if (item.isAnnotation()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disallow cross-library child drag
|
||||
if (item.libraryID != collectionTreeRow.ref.libraryID) {
|
||||
return false;
|
||||
|
|
@ -2291,6 +2350,11 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Disallow drag of annotation items
|
||||
if (item.isAnnotation()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't allow web attachments to be dragged out of parents,
|
||||
// except for files that can be recognized
|
||||
if (item.isWebAttachment()
|
||||
|
|
@ -3142,8 +3206,12 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
: acc;
|
||||
}, { lowestOrdinal: Infinity, firstColumn: null });
|
||||
|
||||
let item = this.getRow(index).ref;
|
||||
for (let column of columns) {
|
||||
if (column.hidden) continue;
|
||||
// Annotation rows have a single cell, created below
|
||||
if (item.isAnnotation()) continue;
|
||||
|
||||
div.appendChild(this._renderCell(index, rowData[column.dataKey], column, column === firstColumn));
|
||||
}
|
||||
|
||||
|
|
@ -3188,6 +3256,46 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
}
|
||||
}
|
||||
|
||||
// Render annotation rows as a single cell with title/text and comment
|
||||
div.classList.toggle("annotation-row", item.isAnnotation());
|
||||
div.classList.remove("tight");
|
||||
if (item.isAnnotation()) {
|
||||
// Use "title" column as a blueprint to render the first part of annotation row
|
||||
let titleRowData = Object.assign({}, columns.find(column => column.dataKey == "title"));
|
||||
titleRowData.className = "title";
|
||||
let title;
|
||||
// Strip html tags from annotation comment and text until the algorithm
|
||||
// for safe rendering of relevant html tags is carried over from the reader
|
||||
let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
|
||||
let plainText = parserUtils.convertToPlainText(item.annotationText || "", Ci.nsIDocumentEncoder.OutputRaw, 0);
|
||||
let plainComment = parserUtils.convertToPlainText(item.annotationComment || "", Ci.nsIDocumentEncoder.OutputRaw, 0);
|
||||
if (["highlight", "underline"].includes(item.annotationType)) {
|
||||
title = this._renderCell(index, plainText, titleRowData, true);
|
||||
let titleCell = title.querySelector(".cell-text");
|
||||
// Quote text is in italics
|
||||
titleCell.classList.add("italics");
|
||||
// Add quotation marks around the quoted text
|
||||
titleCell.setAttribute("q-mark-open", Zotero.getString("punctuation.openingQMark"));
|
||||
title.setAttribute("q-mark-close", Zotero.getString("punctuation.closingQMark"));
|
||||
if (item.annotationComment) {
|
||||
let comment = renderCell(null, plainComment, { className: "annotation-comment" });
|
||||
div.appendChild(comment);
|
||||
}
|
||||
// only keep default wider spacing if there are CJK characters
|
||||
let containsCJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(item.annotationText);
|
||||
div.classList.toggle("tight", !containsCJK);
|
||||
}
|
||||
else if (item.annotationComment) {
|
||||
// If there is a comment for image, ink, note annotations, use that as the title
|
||||
title = this._renderCell(index, plainComment, titleRowData, true);
|
||||
}
|
||||
else {
|
||||
// Catch all - use annotation type as the title
|
||||
let annotationTypeName = Zotero.getString(`pdfReader.${item.annotationType}Annotation`);
|
||||
title = this._renderCell(index, annotationTypeName, titleRowData, true);
|
||||
}
|
||||
div.prepend(title);
|
||||
}
|
||||
return div;
|
||||
};
|
||||
|
||||
|
|
@ -3682,6 +3790,7 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
_saveOpenState(close) {
|
||||
if (!this.tree) return [];
|
||||
var itemIDs = [];
|
||||
var toClose = [];
|
||||
if (close) {
|
||||
if (!this.selection.selectEventsSuppressed) {
|
||||
var unsuppress = this.selection.selectEventsSuppressed = true;
|
||||
|
|
@ -3689,13 +3798,18 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
}
|
||||
for (var i=0; i<this._rows.length; i++) {
|
||||
if (this.isContainer(i) && this.isContainerOpen(i)) {
|
||||
itemIDs.push(this.getRow(i).ref.id);
|
||||
if (close) {
|
||||
this._closeContainer(i, true);
|
||||
let row = this.getRow(i);
|
||||
itemIDs.push(row.ref.id);
|
||||
if (close && row.level == 0) {
|
||||
toClose.push(this.getRow(i).ref.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (close) {
|
||||
for (i = toClose.length - 1; i >= 0; i--) {
|
||||
let row = this._rowMap[toClose[i]];
|
||||
this._closeContainer(row, true);
|
||||
}
|
||||
this._refreshRowMap();
|
||||
if (unsuppress) {
|
||||
this.selection.selectEventsSuppressed = false;
|
||||
|
|
@ -3704,13 +3818,17 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
return itemIDs;
|
||||
}
|
||||
|
||||
_rememberOpenState(itemIDs) {
|
||||
_rememberOpenState(itemIDs, secondLevel = false) {
|
||||
if (!this.tree) return;
|
||||
var rowsToOpen = [];
|
||||
var nextLevelToOpen = [];
|
||||
for (let id of itemIDs) {
|
||||
var row = this._rowMap[id];
|
||||
// Item may not still exist
|
||||
if (row == undefined) {
|
||||
if (!secondLevel) {
|
||||
nextLevelToOpen.push(id);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
rowsToOpen.push(row);
|
||||
|
|
@ -3727,6 +3845,10 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
this.toggleOpenState(rowsToOpen[i], true);
|
||||
}
|
||||
this._refreshRowMap();
|
||||
|
||||
if (nextLevelToOpen.length) {
|
||||
this._rememberOpenState(nextLevelToOpen, true);
|
||||
}
|
||||
if (unsuppress) {
|
||||
this.selection.selectEventsSuppressed = false;
|
||||
}
|
||||
|
|
@ -3940,6 +4062,17 @@ var ItemTree = class ItemTree extends LibraryTree {
|
|||
return icon;
|
||||
}
|
||||
|
||||
if (item.isAnnotation()) {
|
||||
let img = document.createElement("img");
|
||||
img.className = "annotation-icon";
|
||||
let type = item.annotationType;
|
||||
if (type == 'image') {
|
||||
type = 'area';
|
||||
}
|
||||
img.src = 'chrome://zotero/skin/16/universal/annotate-' + type + '.svg';
|
||||
img.style.fill = item.annotationColor;
|
||||
return img;
|
||||
}
|
||||
var itemType = item.getItemTypeIconName();
|
||||
return getCSSItemTypeIcon(itemType);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ var Zotero_LocateMenu = new function() {
|
|||
var selectedItems = [];
|
||||
while (selectedItems.length < 50 && allSelectedItems.length) {
|
||||
var item = allSelectedItems.shift();
|
||||
if (!item.isNote()) selectedItems.push(item);
|
||||
if (!item.isNote() && !item.isAnnotation()) selectedItems.push(item);
|
||||
}
|
||||
return selectedItems;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,7 +322,8 @@ const ZoteroStandalone = new function() {
|
|||
|
||||
var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
|
||||
var exportingNotes = selected.every(item => item.isNote() || item.isAttachment());
|
||||
if (exportingNotes) {
|
||||
var exportingAnnotations = selected.every(item => item.isAnnotation());
|
||||
if (exportingNotes || exportingAnnotations) {
|
||||
format = Zotero.QuickCopy.getNoteFormat();
|
||||
}
|
||||
format = Zotero.QuickCopy.unserializeSetting(format);
|
||||
|
|
@ -331,11 +332,15 @@ const ZoteroStandalone = new function() {
|
|||
var copyBibliography = document.getElementById('menu_copyBibliography');
|
||||
var copyExport = document.getElementById('menu_copyExport');
|
||||
var copyNote = document.getElementById('menu_copyNote');
|
||||
var copyAnnotation = document.getElementById('menu_copyAnnotation');
|
||||
|
||||
copyCitation.hidden = !selected.length || format.mode != 'bibliography';
|
||||
copyBibliography.hidden = !selected.length || format.mode != 'bibliography';
|
||||
copyExport.hidden = !selected.length || format.mode != 'export' || exportingNotes;
|
||||
copyNote.hidden = !selected.length || format.mode != 'export' || !exportingNotes;
|
||||
copyAnnotation.hidden = !selected.length || format.mode != 'export' || !exportingAnnotations;
|
||||
document.l10n.setAttributes(copyAnnotation, "menu-edit-copy-annotation", { count: selected.length });
|
||||
|
||||
if (format.mode == 'export') {
|
||||
try {
|
||||
let obj = Zotero.Translators.get(format.id);
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ Zotero.Annotations = new function () {
|
|||
};
|
||||
|
||||
|
||||
this.toJSON = async function (item) {
|
||||
this.toJSONSync = function (item) {
|
||||
var o = {};
|
||||
o.libraryID = item.libraryID;
|
||||
o.key = item.key;
|
||||
|
|
@ -142,12 +142,6 @@ Zotero.Annotations = new function () {
|
|||
if (['highlight', 'underline'].includes(o.type)) {
|
||||
o.text = item.annotationText;
|
||||
}
|
||||
else if (['image', 'ink'].includes(o.type)) {
|
||||
let file = this.getCacheImagePath(item);
|
||||
if (await OS.File.exists(file)) {
|
||||
o.image = await Zotero.File.generateDataURI(file, 'image/png');
|
||||
}
|
||||
}
|
||||
o.comment = item.annotationComment;
|
||||
o.pageLabel = item.annotationPageLabel;
|
||||
o.color = item.annotationColor;
|
||||
|
|
@ -184,6 +178,17 @@ Zotero.Annotations = new function () {
|
|||
o.dateModified = Zotero.Date.sqlToISO8601(item.dateModified);
|
||||
return o;
|
||||
};
|
||||
|
||||
this.toJSON = async function (item) {
|
||||
var o = this.toJSONSync(item);
|
||||
if (['image', 'ink'].includes(o.type)) {
|
||||
let file = this.getCacheImagePath(item);
|
||||
if (await OS.File.exists(file)) {
|
||||
o.image = await Zotero.File.generateDataURI(file, 'image/png');
|
||||
}
|
||||
}
|
||||
return o;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -254,11 +254,14 @@ Zotero.Items = function() {
|
|||
item.updateDisplayTitle()
|
||||
}
|
||||
catch (e) {
|
||||
// A few item types need creators to be loaded. Instead of making
|
||||
// updateDisplayTitle() async and loading conditionally, just catch the error
|
||||
// A few item types need creators or tags to be loaded. Annotations need to be loaded
|
||||
// to be displayed in itemTree.
|
||||
// Instead of making updateDisplayTitle() async and loading conditionally, just catch the error
|
||||
// and load on demand
|
||||
if (e instanceof Zotero.Exception.UnloadedDataException) {
|
||||
yield item.loadDataType('creators');
|
||||
yield item.loadDataType('annotation');
|
||||
yield item.loadDataType('tags');
|
||||
item.updateDisplayTitle()
|
||||
}
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -607,7 +607,10 @@ Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable
|
|||
sql += " OR itemID IN (SELECT itemID FROM itemAttachments"
|
||||
+ " WHERE parentItemID IN (SELECT itemID FROM " + tmpTable + ")) OR "
|
||||
+ "itemID IN (SELECT itemID FROM itemNotes"
|
||||
+ " WHERE parentItemID IN (SELECT itemID FROM " + tmpTable + "))";
|
||||
+ " WHERE parentItemID IN (SELECT itemID FROM " + tmpTable + "))"
|
||||
+ " OR itemID IN ( SELECT itemID FROM itemAnnotations WHERE "
|
||||
+ " parentItemID IN ( SELECT itemID FROM itemAttachments WHERE "
|
||||
+ " parentItemID IN ( SELECT itemID FROM " + tmpTable + ")))";
|
||||
}
|
||||
sql += ")";
|
||||
|
||||
|
|
@ -948,10 +951,7 @@ Zotero.Search.idsToTempTable = Zotero.Promise.coroutine(function* (ids) {
|
|||
Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
|
||||
this._requireData('conditions');
|
||||
|
||||
// TEMP: Match parent attachment for annotation matches
|
||||
// var sql = 'SELECT itemID FROM items';
|
||||
var sql = "SELECT COALESCE(IA.parentItemID, itemID) AS itemID FROM items "
|
||||
+ "LEFT JOIN itemAnnotations IA USING (itemID)";
|
||||
var sql = 'SELECT itemID FROM items';
|
||||
|
||||
var sqlParams = [];
|
||||
// Separate ANY conditions for 'required' condition support
|
||||
|
|
@ -1216,16 +1216,14 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
|
|||
}
|
||||
|
||||
switch (condition.name) {
|
||||
// TEMP: Match parent attachments of matching annotations
|
||||
case 'tag':
|
||||
condSQL += "SELECT COALESCE(IAnT.parentItemID, itemID) FROM itemTags "
|
||||
condSQL += "SELECT itemID FROM itemTags "
|
||||
+ "LEFT JOIN itemAnnotations IAnT USING (itemID) WHERE (";
|
||||
break;
|
||||
|
||||
// TEMP: Match parent attachments of matching annotations
|
||||
case 'annotationText':
|
||||
case 'annotationComment':
|
||||
condSQL += `SELECT parentItemID FROM ${condition.table} WHERE (`
|
||||
condSQL += `SELECT itemID FROM ${condition.table} WHERE (`
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
@ -1807,11 +1805,7 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
|
|||
|
||||
// Add on quicksearch conditions
|
||||
if (quicksearchSQLSet) {
|
||||
// TEMP: Match parent attachments for annotations
|
||||
//sql = "SELECT itemID FROM items WHERE itemID IN (" + sql + ") "
|
||||
sql = "SELECT COALESCE(IAn.parentItemID, itemID) AS itemID FROM items "
|
||||
+ "LEFT JOIN itemAnnotations IAn USING (itemID) "
|
||||
+ "WHERE itemID IN (" + sql + ") "
|
||||
sql = "SELECT itemID FROM items WHERE itemID IN (" + sql + ") "
|
||||
+ "AND ((" + quicksearchSQLSet.join(') AND (') + "))";
|
||||
|
||||
for (var k=0; k<quicksearchParamsSet.length; k++) {
|
||||
|
|
|
|||
|
|
@ -572,7 +572,7 @@ Zotero.SearchConditions = new function(){
|
|||
},
|
||||
table: 'itemAnnotations',
|
||||
field: 'text',
|
||||
special: true,
|
||||
special: false,
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -583,7 +583,7 @@ Zotero.SearchConditions = new function(){
|
|||
},
|
||||
table: 'itemAnnotations',
|
||||
field: 'comment',
|
||||
special: true,
|
||||
special: false,
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -308,7 +308,43 @@ Zotero.QuickCopy = new function() {
|
|||
|
||||
throw ("Invalid mode '" + format.mode + "' in Zotero.QuickCopy.getContentFromItems()");
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a note item to pass to getContentFromItems() from an array of annotations
|
||||
*
|
||||
* @param {Zotero.Item[]|Object[]} annotations - An array of Zotero.Item annotations or JSON
|
||||
* annotations from Zotero.Annotations.toJSON()
|
||||
* @return {Zotero.Item} - A note item with the annotations serialized as HTML
|
||||
*/
|
||||
this.annotationsToNote = function (annotations) {
|
||||
let jsonAnnotations = [];
|
||||
for (let annotation of annotations) {
|
||||
if (annotation instanceof Zotero.Item) {
|
||||
// Skip ink and image annotations because fetching them
|
||||
// requires awaiting Zotero.Annotations.toJSON()
|
||||
if (["ink", "image"].includes(annotation.type)) {
|
||||
continue;
|
||||
}
|
||||
let json = Zotero.Annotations.toJSONSync(annotation);
|
||||
json.attachmentItemID = annotation.parentItemID;
|
||||
jsonAnnotations.push(json);
|
||||
}
|
||||
else {
|
||||
jsonAnnotations.push(annotation);
|
||||
}
|
||||
}
|
||||
for (let annotation of jsonAnnotations) {
|
||||
if (annotation.image && !annotation.imageAttachmentKey) {
|
||||
annotation.imageAttachmentKey = 'none';
|
||||
delete annotation.image;
|
||||
}
|
||||
}
|
||||
let { html } = Zotero.EditorInstanceUtilities.serializeAnnotations(jsonAnnotations);
|
||||
let tmpNote = new Zotero.Item('note');
|
||||
tmpNote.libraryID = Zotero.Libraries.userLibraryID;
|
||||
tmpNote.setNote(html);
|
||||
return tmpNote;
|
||||
};
|
||||
|
||||
/**
|
||||
* If an export translator is the selected output format, load its code (which must be done
|
||||
|
|
|
|||
|
|
@ -367,17 +367,8 @@ class ReaderInstance {
|
|||
if (fromText) {
|
||||
return;
|
||||
}
|
||||
for (let annotation of annotations) {
|
||||
if (annotation.image && !annotation.imageAttachmentKey) {
|
||||
annotation.imageAttachmentKey = 'none';
|
||||
delete annotation.image;
|
||||
}
|
||||
}
|
||||
let res = Zotero.EditorInstanceUtilities.serializeAnnotations(annotations);
|
||||
let tmpNote = new Zotero.Item('note');
|
||||
tmpNote.libraryID = Zotero.Libraries.userLibraryID;
|
||||
tmpNote.setNote(res.html);
|
||||
let items = [tmpNote];
|
||||
// annotations are wrapped in a temp note for translation
|
||||
let items = [Zotero.QuickCopy.annotationsToNote(annotations)];
|
||||
let format = Zotero.QuickCopy.getNoteFormat();
|
||||
Zotero.debug(`Copying/dragging (${annotations.length}) annotation(s) with ${format}`);
|
||||
format = Zotero.QuickCopy.unserializeSetting(format);
|
||||
|
|
|
|||
|
|
@ -3359,6 +3359,12 @@ Zotero.Utilities.Internal.onDragItems = function (event, itemIDs, dragImage = ev
|
|||
format = Zotero.QuickCopy.getNoteFormat();
|
||||
}
|
||||
|
||||
// If all items are annotations, wrap them in a note object for translation
|
||||
if (items.every(item => item.isAnnotation())) {
|
||||
format = Zotero.QuickCopy.getNoteFormat();
|
||||
items = [Zotero.QuickCopy.annotationsToNote(items)];
|
||||
}
|
||||
|
||||
Zotero.debug("Dragging with format " + format);
|
||||
format = Zotero.QuickCopy.unserializeSetting(format);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2009,7 +2009,7 @@ var ZoteroPane = new function()
|
|||
let format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
|
||||
format = Zotero.QuickCopy.unserializeSetting(format);
|
||||
if (format.mode == 'bibliography') {
|
||||
canCopy = selectedItems.some(item => item.isRegularItem());
|
||||
canCopy = selectedItems.some(item => item.isRegularItem() || item.isAnnotation());
|
||||
}
|
||||
else {
|
||||
canCopy = true;
|
||||
|
|
@ -2018,6 +2018,7 @@ var ZoteroPane = new function()
|
|||
|
||||
document.getElementById('cmd_zotero_copyCitation').setAttribute('disabled', !canCopy);
|
||||
document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', !canCopy);
|
||||
document.getElementById('cmd_zotero_copyAnnotation').setAttribute('disabled', !canCopy);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -2177,6 +2178,13 @@ var ZoteroPane = new function()
|
|||
else if (collectionTreeRow.isShare()) {
|
||||
return false;
|
||||
}
|
||||
// If multiple items are selected, some are annotations and some are not, do nothing,
|
||||
// since annotations have different treatment from other items
|
||||
let selected = this.itemsView.getSelectedItems();
|
||||
if (!selected.every(item => item.isAnnotation())
|
||||
&& selected.some(item => item.isAnnotation())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -2226,15 +2234,19 @@ var ZoteroPane = new function()
|
|||
if (!this.canDeleteSelectedItems()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (collectionTreeRow.isPublications()) {
|
||||
var prompt;
|
||||
// Backspace on annotation items = prompt to erase
|
||||
if (this.itemsView.getSelectedItems().every(item => item.isAnnotation())) {
|
||||
prompt = toDelete;
|
||||
}
|
||||
else if (collectionTreeRow.isPublications()) {
|
||||
let toRemoveFromPublications = {
|
||||
title: Zotero.getString('pane.items.removeFromPublications.title'),
|
||||
text: Zotero.getString(
|
||||
'pane.items.removeFromPublications' + (this.itemsView.selection.count > 1 ? '.multiple' : '')
|
||||
)
|
||||
};
|
||||
var prompt = force ? toTrash : toRemoveFromPublications;
|
||||
prompt = force ? toTrash : toRemoveFromPublications;
|
||||
}
|
||||
else if (collectionTreeRow.isLibrary(true)
|
||||
|| collectionTreeRow.isSearch()
|
||||
|
|
@ -2242,11 +2254,11 @@ var ZoteroPane = new function()
|
|||
|| collectionTreeRow.isRetracted()
|
||||
|| collectionTreeRow.isDuplicates()) {
|
||||
// In library, don't prompt if meta key was pressed
|
||||
var prompt = (force && !fromMenu) ? false : toTrash;
|
||||
prompt = (force && !fromMenu) ? false : toTrash;
|
||||
}
|
||||
else if (collectionTreeRow.isCollection()) {
|
||||
if (force) {
|
||||
var prompt = toTrash;
|
||||
prompt = toTrash;
|
||||
}
|
||||
else {
|
||||
// Ignore unmodified action if only child items are selected
|
||||
|
|
@ -2275,12 +2287,12 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
else {
|
||||
var prompt = toRemove;
|
||||
prompt = toRemove;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (collectionTreeRow.isTrash() || collectionTreeRow.isBucket()) {
|
||||
var prompt = toDelete;
|
||||
prompt = toDelete;
|
||||
}
|
||||
|
||||
if (!prompt || Services.prompt.confirm(window, prompt.title, prompt.text)) {
|
||||
|
|
@ -2662,6 +2674,11 @@ var ZoteroPane = new function()
|
|||
if (items.every(item => item.isNote() || item.isAttachment())) {
|
||||
format = Zotero.QuickCopy.getNoteFormat();
|
||||
}
|
||||
// To copy annotations, wrap them in a temp note
|
||||
if (items.every(item => item.isAnnotation())) {
|
||||
format = Zotero.QuickCopy.getNoteFormat();
|
||||
items = [Zotero.QuickCopy.annotationsToNote(items)];
|
||||
}
|
||||
format = Zotero.QuickCopy.unserializeSetting(format);
|
||||
|
||||
// In bibliography mode, remove notes and attachments
|
||||
|
|
@ -3699,7 +3716,8 @@ var ZoteroPane = new function()
|
|||
&& !collectionTreeRow.isDuplicates()
|
||||
&& !collectionTreeRow.isFeedsOrFeed()) {
|
||||
if (items.some(item => attachmentsWithExtractableAnnotations(item).length)
|
||||
|| items.some(item => isAttachmentWithExtractableAnnotations(item))) {
|
||||
|| items.some(item => isAttachmentWithExtractableAnnotations(item))
|
||||
|| items.some(item => item.isAnnotation())) {
|
||||
let menuitem = menu.childNodes[m.createNoteFromAnnotations];
|
||||
show.add(m.createNoteFromAnnotations);
|
||||
let key;
|
||||
|
|
@ -3789,7 +3807,7 @@ var ZoteroPane = new function()
|
|||
}
|
||||
}
|
||||
// Show "(Create|Add) Note from Annotations" on attachment with extractable annotations
|
||||
else if (isAttachmentWithExtractableAnnotations(item)) {
|
||||
else if (isAttachmentWithExtractableAnnotations(item) || item.isAnnotation()) {
|
||||
show.add(m.createNoteFromAnnotations);
|
||||
show.add(m.sep2);
|
||||
}
|
||||
|
|
@ -3852,6 +3870,9 @@ var ZoteroPane = new function()
|
|||
show.add(m.sep5);
|
||||
}
|
||||
}
|
||||
else if (item.isAnnotation()) {
|
||||
// Some annotation specific menus?
|
||||
}
|
||||
else if (item.isFeedItem) {
|
||||
show.add(m.toggleRead);
|
||||
if (item.isRead) {
|
||||
|
|
@ -4005,6 +4026,17 @@ var ZoteroPane = new function()
|
|||
show.add(m.changeParentItem);
|
||||
}
|
||||
|
||||
// Only keep annotation-specific options if annotations are selected
|
||||
let annotationsSelected = items.some(item => item.isAnnotation());
|
||||
if (annotationsSelected) {
|
||||
for (let i in m) {
|
||||
if (i == 'createNoteFromAnnotations') {
|
||||
continue;
|
||||
}
|
||||
show.delete(m[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Set labels, plural if necessary
|
||||
menu.childNodes[m.findFile].setAttribute('label', Zotero.getString('pane.items.menu.findAvailableFile'));
|
||||
menu.childNodes[m.moveToTrash].setAttribute('label', Zotero.getString('pane.items.menu.moveToTrash' + multiple));
|
||||
|
|
@ -4031,7 +4063,10 @@ var ZoteroPane = new function()
|
|||
for (let x of show) {
|
||||
menu.childNodes[x].setAttribute('hidden', false);
|
||||
}
|
||||
|
||||
|
||||
// No locate menu options if annotations are selected
|
||||
if (annotationsSelected) return;
|
||||
|
||||
// add locate menu options
|
||||
yield Zotero_LocateMenu.buildContextMenu(menu, true);
|
||||
});
|
||||
|
|
@ -4955,6 +4990,9 @@ var ZoteroPane = new function()
|
|||
else if (item.isAttachment()) {
|
||||
yield this.viewAttachment(item.id, event);
|
||||
}
|
||||
else if (item.isAnnotation()) {
|
||||
this.viewPDF(item.parentItemID, { annotationID: item.key });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -5741,6 +5779,7 @@ var ZoteroPane = new function()
|
|||
}
|
||||
|
||||
var attachments = [];
|
||||
var annotations = [];
|
||||
for (let item of items) {
|
||||
if (item.isRegularItem()) {
|
||||
// Find all child items with extractable annotations
|
||||
|
|
@ -5752,17 +5791,19 @@ var ZoteroPane = new function()
|
|||
else if (isAttachmentWithExtractableAnnotations(item)) {
|
||||
attachments.push(item);
|
||||
}
|
||||
else if (item.isAnnotation() && item.annotationType != 'ink') {
|
||||
annotations.push(item);
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!attachments.length) {
|
||||
if (!attachments.length && !annotations.length) {
|
||||
Zotero.debug("No attachments found", 2);
|
||||
return;
|
||||
}
|
||||
|
||||
var annotations = [];
|
||||
for (let attachment of attachments) {
|
||||
if (attachment.isPDFAttachment()) {
|
||||
try {
|
||||
|
|
@ -5858,6 +5899,9 @@ var ZoteroPane = new function()
|
|||
else if (isAttachmentWithExtractableAnnotations(item)) {
|
||||
attachments.push(item);
|
||||
}
|
||||
else if (item.isAnnotation()) {
|
||||
annotations.push(item);
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -5870,7 +5914,7 @@ var ZoteroPane = new function()
|
|||
Zotero.logError(e);
|
||||
}
|
||||
}
|
||||
annotations.push(...attachment.getAnnotations().filter(x => x.annotationType != 'ink'));
|
||||
annotations.push(...attachment.getAnnotations().filter(x => x.annotationType != 'ink' && x.annotationType != 'image'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,9 @@
|
|||
<command id="cmd_zotero_copyBibliography"
|
||||
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard();"
|
||||
disabled="true"/>
|
||||
<command id="cmd_zotero_copyAnnotation"
|
||||
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard();"
|
||||
disabled="true"/>
|
||||
<command id="cmd_zotero_createTimeline" oncommand="Zotero_Timeline_Interface.loadTimeline();"/>
|
||||
<command id="cmd_zotero_rtfScan" oncommand="window.openDialog('chrome://zotero/content/rtfScan.xhtml', 'rtfScan', 'chrome,centerscreen')"/>
|
||||
<command id="cmd_zotero_newCollection" oncommand="ZoteroPane_Local.newCollection(ZoteroPane_Local.getSelectedCollection()?.key)"/>
|
||||
|
|
@ -388,6 +391,11 @@
|
|||
command="cmd_zotero_copyBibliography"
|
||||
key="key_copyBibliography"
|
||||
hidden="true"/>
|
||||
<menuitem id="menu_copyAnnotation"
|
||||
data-l10n-id="menu-edit-copy-annotation"
|
||||
command="cmd_zotero_copyAnnotation"
|
||||
key="key_copyBibliography"
|
||||
hidden="true"/>
|
||||
<menuitem id="menu_paste"
|
||||
key="key_paste"
|
||||
command="cmd_paste" data-l10n-id="text-action-paste"/>
|
||||
|
|
|
|||
|
|
@ -96,6 +96,12 @@ menu-view-columns-move-left =
|
|||
menu-view-columns-move-right =
|
||||
.label = Move Column Right
|
||||
|
||||
menu-edit-copy-annotation =
|
||||
.label = { $count ->
|
||||
[one] Copy Annotation
|
||||
*[other] Copy { $count } Annotations
|
||||
}
|
||||
|
||||
main-window-command =
|
||||
.label = Library
|
||||
main-window-key =
|
||||
|
|
@ -592,6 +598,8 @@ toggle-preview =
|
|||
*[unknown] Toggle
|
||||
} Attachment Preview
|
||||
|
||||
annotation-image-not-available = [Image not available]
|
||||
|
||||
quicksearch-mode =
|
||||
.aria-label = Quick Search mode
|
||||
quicksearch-input =
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@
|
|||
@import "elements/librariesCollectionsBox";
|
||||
@import "elements/duplicatesMergePane";
|
||||
@import "elements/itemMessagePane";
|
||||
@import "elements/itemPaneAnnotations";
|
||||
@import "elements/itemDetails";
|
||||
@import "elements/itemPane";
|
||||
@import "elements/itemPaneCustomSection";
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ $font-size-base: 13px;
|
|||
$font-size-h1: 20px;
|
||||
$font-size-h2: 16px;
|
||||
|
||||
// Font size used in tag selector and annotation rows of itemTree
|
||||
$font-size-small: 0.923076923em;
|
||||
|
||||
$line-height-base: 1.539;
|
||||
$line-height-computed: ceil($font-size-base * $line-height-base);
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@
|
|||
}
|
||||
|
||||
.icon-item-type + .tag-swatch,
|
||||
.icon-item-type + .colored-tag-swatches {
|
||||
.icon-item-type + .colored-tag-swatches,
|
||||
.annotation-icon + .tag-swatch,
|
||||
.annotation-icon + .colored-tag-swatches {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +220,40 @@
|
|||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.annotation-row {
|
||||
.cell {
|
||||
font-size: $font-size-small;
|
||||
max-width: fit-content;
|
||||
// Do not italicize CJK characters
|
||||
font-synthesis: none;
|
||||
&.title {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
max-width: fit-content;
|
||||
.cell-text::before {
|
||||
content: attr(q-mark-open)
|
||||
}
|
||||
&::after {
|
||||
content: attr(q-mark-close)
|
||||
}
|
||||
.italics {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
&.annotation-comment {
|
||||
flex-grow: 2;
|
||||
flex-basis: 0;
|
||||
}
|
||||
}
|
||||
&.tight .cell {
|
||||
padding: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.cell .annotation-icon {
|
||||
-moz-context-properties: fill;
|
||||
}
|
||||
|
||||
.numNotes, .hasAttachment {
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
.tag-selector-item {
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.916666667em;
|
||||
font-size: $font-size-small;
|
||||
line-height: 1.272727273;
|
||||
overflow: hidden;
|
||||
padding: 1px 4px;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ annotation-row {
|
|||
border-radius: 5px;
|
||||
border: 1px solid var(--color-quinary-on-sidepane);
|
||||
background: var(--material-background);
|
||||
@include focus-ring;
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
|
|
@ -56,7 +57,7 @@ annotation-row {
|
|||
padding: 3px 8px;
|
||||
|
||||
@include comfortable {
|
||||
padding-block: 4px;
|
||||
margin-block: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ item-message-pane {
|
|||
background: var(--material-toolbar);
|
||||
border-bottom: var(--material-panedivider);
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
|
@ -19,6 +20,9 @@ item-message-pane {
|
|||
button {
|
||||
height: 26px;
|
||||
margin: 0;
|
||||
@media (-moz-platform: macos) {
|
||||
margin: 0px -2px;
|
||||
}
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
scss/elements/_itemPaneAnnotations.scss
Normal file
69
scss/elements/_itemPaneAnnotations.scss
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
annotation-items-pane {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.custom-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-self: stretch;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
background: var(--material-sidepane);
|
||||
border-bottom: var(--material-panedivider);
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 26px;
|
||||
margin: 0;
|
||||
@media (-moz-platform: macos) {
|
||||
margin: 0px -2px;
|
||||
}
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the summary containing name of top-level item is always visible
|
||||
// and titles are cut off by letter and not by word
|
||||
collapsible-section > .head .title-box .summary {
|
||||
opacity: 1 !important;
|
||||
width: 0;
|
||||
white-space: nowrap;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
// Annotation icon for collapsible section
|
||||
collapsible-section > .head .title::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: icon-url("itempane/16/attachment-annotations.svg") no-repeat center;
|
||||
-moz-context-properties: fill, fill-opacity, stroke, stroke-opacity;
|
||||
fill: var(--tag-purple);
|
||||
stroke: var(--tag-purple);
|
||||
}
|
||||
|
||||
collapsible-section > .body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@include comfortable {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
collapsible-section:not(:last-child) {
|
||||
border-bottom: 1px solid var(--fill-quinary);
|
||||
}
|
||||
|
||||
// Do not cut off annotation text and comment
|
||||
annotation-row .body .comment,
|
||||
annotation-row .body .quote {
|
||||
-webkit-line-clamp: inherit !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -2083,4 +2083,45 @@ describe("Item pane", function () {
|
|||
Zotero.ItemPaneManager.unregisterSection(registeredID);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AnnotationItemsPane", function () {
|
||||
it("should display selected annotations groupped by parent item", async () => {
|
||||
let toplevelItemOne = await createDataObject('item', { title: "Item one" });
|
||||
let attachmentOne = await importFileAttachment('test.pdf', { title: 'PDF', parentItemID: toplevelItemOne.id });
|
||||
let highlightOne = await createAnnotation('highlight', attachmentOne);
|
||||
|
||||
let toplevelItemTwo = await createDataObject('item', { title: "Item two" });
|
||||
let attachmentTwo = await importFileAttachment('test.pdf', { title: 'PDF', parentItemID: toplevelItemTwo.id });
|
||||
let highlightTwo = await createAnnotation('highlight', attachmentTwo);
|
||||
|
||||
ZoteroPane.itemsView.expandAllRows();
|
||||
|
||||
await ZoteroPane.itemsView.selectItems([highlightOne.id, highlightTwo.id]);
|
||||
|
||||
let sections = [...win.document.querySelectorAll("annotation-items-pane collapsible-section")];
|
||||
// Top level items' titles are in section summaries
|
||||
assert.equal(sections[0].summary, toplevelItemOne.getDisplayTitle());
|
||||
assert.equal(sections[1].summary, toplevelItemTwo.getDisplayTitle());
|
||||
// Each item's section contains its annotation
|
||||
assert.equal(sections[0].querySelector("annotation-row").annotation.id, highlightOne.id);
|
||||
assert.equal(sections[1].querySelector("annotation-row").annotation.id, highlightTwo.id);
|
||||
});
|
||||
|
||||
it("should refresh when annotation is updated", async () => {
|
||||
let toplevelItemOne = await createDataObject('item', { title: "Item one" });
|
||||
let attachmentOne = await importFileAttachment('test.pdf', { title: 'PDF', parentItemID: toplevelItemOne.id });
|
||||
let highlightOne = await createAnnotation('highlight', attachmentOne);
|
||||
|
||||
highlightOne.annotationText = "Annotation";
|
||||
await highlightOne.saveTx();
|
||||
|
||||
ZoteroPane.itemsView.expandAllRows();
|
||||
await ZoteroPane.itemsView.selectItems([highlightOne.id]);
|
||||
|
||||
assert.equal(win.document.querySelector("annotation-items-pane annotation-row .quote").textContent, "Annotation");
|
||||
highlightOne.annotationText = "Updated";
|
||||
await highlightOne.saveTx();
|
||||
assert.equal(win.document.querySelector("annotation-items-pane annotation-row .quote").textContent, "Updated");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -225,23 +225,28 @@ describe("Zotero.ItemTree", function() {
|
|||
var note1 = await createDataObject('item', { itemType: 'note', parentID: item1.id });
|
||||
var note2 = await createDataObject('item', { itemType: 'note', parentID: item2.id });
|
||||
var note3 = await createDataObject('item', { itemType: 'note', parentID: item3.id });
|
||||
|
||||
// one of the items has an attachment with annotations
|
||||
var attachment = await importFileAttachment('test.pdf', { title: 'PDF', parentItemID: item1.id });
|
||||
var highlight = await createAnnotation('highlight', attachment);
|
||||
var underline = await createAnnotation('underline', attachment);
|
||||
|
||||
var toSelect = [note1.id, note2.id, note3.id];
|
||||
var toSelect = [note1.id, note2.id, note3.id, highlight.id, underline.id];
|
||||
itemsView.collapseAllRows();
|
||||
|
||||
var numSelected = await itemsView.selectItems(toSelect);
|
||||
assert.equal(numSelected, 3);
|
||||
assert.equal(numSelected, 5);
|
||||
var selected = itemsView.getSelectedItems(true);
|
||||
assert.lengthOf(selected, 3);
|
||||
assert.lengthOf(selected, 5);
|
||||
assert.sameMembers(selected, toSelect);
|
||||
|
||||
// Again with the ids given in reverse order
|
||||
itemsView.collapseAllRows();
|
||||
toSelect = toSelect.reverse();
|
||||
var numSelected = await itemsView.selectItems(toSelect);
|
||||
assert.equal(numSelected, 3);
|
||||
var selected = itemsView.getSelectedItems(true);
|
||||
assert.lengthOf(selected, 3);
|
||||
numSelected = await itemsView.selectItems(toSelect);
|
||||
assert.equal(numSelected, 5);
|
||||
selected = itemsView.getSelectedItems(true);
|
||||
assert.lengthOf(selected, 5);
|
||||
assert.sameMembers(selected, toSelect);
|
||||
});
|
||||
});
|
||||
|
|
@ -1587,4 +1592,133 @@ describe("Zotero.ItemTree", function() {
|
|||
assert.equal(cellText.innerHTML, 'Review of <i xmlns="http://www.w3.org/1999/xhtml">Review of <span style="font-style: normal;">B<sub>oo</sub>k</span> <another-tag/></i>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Annotations", function() {
|
||||
let toplevelItem, attachment, highlight, underline, ink, image, note;
|
||||
|
||||
before(async () => {
|
||||
var collection = await createDataObject('collection');
|
||||
await select(win, collection);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
toplevelItem = await createDataObject('item', { title: "Item" });
|
||||
attachment = await importFileAttachment('test.pdf', { title: 'PDF', parentItemID: toplevelItem.id });
|
||||
highlight = await createAnnotation('highlight', attachment);
|
||||
underline = await createAnnotation('underline', attachment);
|
||||
ink = await createAnnotation('ink', attachment);
|
||||
image = await createAnnotation('image', attachment);
|
||||
note = await createAnnotation('image', attachment);
|
||||
});
|
||||
|
||||
it("should display annotations as child rows of attachments", async () => {
|
||||
zp.itemsView.expandAllRows();
|
||||
|
||||
var attachmentRowIndex = zp.itemsView.getRowIndexByID(attachment.id);
|
||||
|
||||
let offset = 0;
|
||||
for (let annotation of attachment.getAnnotations()) {
|
||||
let annotationRowIndex = zp.itemsView.getRowIndexByID(annotation.id);
|
||||
offset += 1;
|
||||
assert.equal(annotationRowIndex, attachmentRowIndex + offset);
|
||||
}
|
||||
});
|
||||
|
||||
it("should preserve order of annotation rows after sorting", async () => {
|
||||
let itemAboveOne = await createDataObject('item', { title: "AAA" });
|
||||
let itemAboveTwo = await createDataObject('item', { title: "BBB" });
|
||||
let itemBelowOne = await createDataObject('item', { title: "ZZZ" });
|
||||
|
||||
// Initially, everything is sorted by title
|
||||
var colIndex = itemsView.tree._getColumns().findIndex(column => column.dataKey == 'title');
|
||||
await zp.itemsView.tree._columns.toggleSort(colIndex);
|
||||
|
||||
// Expand annotations
|
||||
var itemRowIndex = zp.itemsView.getRowIndexByID(toplevelItem.id);
|
||||
await zp.itemsView.toggleOpenState(itemRowIndex);
|
||||
|
||||
var attachmentRowIndex = zp.itemsView.getRowIndexByID(attachment.id);
|
||||
await zp.itemsView.toggleOpenState(attachmentRowIndex);
|
||||
|
||||
// Record sequence of items
|
||||
let rowIDs = zp.itemsView._rows.map(row => row.id);
|
||||
|
||||
// Sort by title in reverse
|
||||
await zp.itemsView.tree._columns.toggleSort(colIndex);
|
||||
|
||||
attachmentRowIndex = zp.itemsView.getRowIndexByID(attachment.id);
|
||||
// Make sure annotations appear after the attachment
|
||||
let offset = 0;
|
||||
for (let annotation of attachment.getAnnotations()) {
|
||||
let annotationRowIndex = zp.itemsView.getRowIndexByID(annotation.id);
|
||||
offset += 1;
|
||||
assert.equal(annotationRowIndex, attachmentRowIndex + offset);
|
||||
}
|
||||
|
||||
// Sort back and make sure the order of rows is the same as in the beginning
|
||||
await zp.itemsView.tree._columns.toggleSort(colIndex);
|
||||
assert.deepEqual(rowIDs, zp.itemsView._rows.map(row => row.id));
|
||||
});
|
||||
|
||||
it("should erase annotation on escape when row is selected", async () => {
|
||||
zp.itemsView.expandAllRows();
|
||||
|
||||
// Select and delete ink annotation
|
||||
let inkID = ink.id;
|
||||
await zp.itemsView.selectItems([inkID]);
|
||||
|
||||
await zp.itemsView.deleteSelection();
|
||||
|
||||
// Make sure it is deleted and the row is gone
|
||||
assert.isFalse(Zotero.Items.get(inkID));
|
||||
assert.isFalse(zp.itemsView.getRowIndexByID(inkID));
|
||||
});
|
||||
|
||||
it("should add note from selected annotation rows of the same parent item", async () => {
|
||||
zp.itemsView.expandAllRows();
|
||||
|
||||
// make sure underline has some text, just like highlight
|
||||
underline.annotationText = "underline";
|
||||
await underline.saveTx();
|
||||
await zp.itemsView.selectItems([highlight.id, underline.id]);
|
||||
|
||||
// Click button in the header of annotations pane
|
||||
win.document.querySelector("annotation-items-pane .custom-head button").click();
|
||||
await waitForItemEvent('add');
|
||||
await waitForItemEvent('modify');
|
||||
|
||||
// Make sure note is created as a child of top level item
|
||||
let note = Zotero.Items.get(toplevelItem.getNotes()[0]);
|
||||
assert.exists(note);
|
||||
let text = note.getNote();
|
||||
// Only two paragraphs, one for each annotation, should be added
|
||||
assert.equal(text.split("<p>").length - 1, 2);
|
||||
});
|
||||
|
||||
it("should create note from selected annotation rows of different parent items", async () => {
|
||||
let toplevelItemTwo = await createDataObject('item', { title: "Another entry" });
|
||||
let attachmentTwo = await importFileAttachment('test.pdf', { title: 'PDF two', parentItemID: toplevelItemTwo.id });
|
||||
let highlightTwo = await createAnnotation('highlight', attachmentTwo);
|
||||
|
||||
zp.itemsView.expandAllRows();
|
||||
|
||||
await zp.itemsView.selectItems([highlight.id, highlightTwo.id]);
|
||||
|
||||
// Click button in the header of annotations pane
|
||||
win.document.querySelector("annotation-items-pane .custom-head button").click();
|
||||
await waitForItemEvent('add');
|
||||
await waitForItemEvent('modify');
|
||||
|
||||
let note = zp.getSelectedItems()[0];
|
||||
assert.isTrue(note.isNote());
|
||||
assert.isFalse(note.parentID);
|
||||
|
||||
let text = note.getNote();
|
||||
// Only two paragraphs, one for each annotation, should be added
|
||||
assert.equal(text.split("<p>").length - 1, 2);
|
||||
// Headers of both top level items are present
|
||||
assert.include(text, toplevelItem.getDisplayTitle());
|
||||
assert.include(text, toplevelItemTwo.getDisplayTitle());
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ describe("Zotero.Search", function() {
|
|||
s.libraryID = userLibraryID;
|
||||
s.addCondition('tag', 'is', tag);
|
||||
var matches = await s.search();
|
||||
assert.sameMembers(matches, [attachment.id]);
|
||||
assert.sameMembers(matches, [annotation.id]);
|
||||
});
|
||||
|
||||
// TEMP
|
||||
|
|
@ -421,7 +421,7 @@ describe("Zotero.Search", function() {
|
|||
s.addCondition('annotationText', 'contains', str);
|
||||
var matches = await s.search();
|
||||
// TEMP: Match parent attachment
|
||||
assert.sameMembers(matches, [attachment.id]);
|
||||
assert.sameMembers(matches, [annotation.id]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -437,7 +437,7 @@ describe("Zotero.Search", function() {
|
|||
s.addCondition('annotationComment', 'contains', str);
|
||||
var matches = await s.search();
|
||||
// TEMP: Match parent attachment
|
||||
assert.sameMembers(matches, [attachment.id]);
|
||||
assert.sameMembers(matches, [annotation.id]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -659,7 +659,7 @@ describe("Zotero.Search", function() {
|
|||
s.addCondition('quicksearch-fields', 'contains', tag);
|
||||
var matches = await s.search();
|
||||
// TEMP: Match parent attachment
|
||||
assert.sameMembers(matches, [attachment.id]);
|
||||
assert.sameMembers(matches, [annotation.id]);
|
||||
});
|
||||
})
|
||||
|
||||
|
|
@ -674,7 +674,7 @@ describe("Zotero.Search", function() {
|
|||
s.addCondition('quicksearch-everything', 'contains', comment);
|
||||
var matches = await s.search();
|
||||
// TEMP: Match parent attachment
|
||||
assert.sameMembers(matches, [attachment.id]);
|
||||
assert.sameMembers(matches, [annotation.id]);
|
||||
});
|
||||
|
||||
it("should not include items outside of scope during phrase search", async function () {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue