diff --git a/chrome/content/zotero/customElements.js b/chrome/content/zotero/customElements.js index 44c4c0445e..b755d8f672 100644 --- a/chrome/content/zotero/customElements.js +++ b/chrome/content/zotero/customElements.js @@ -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'], diff --git a/chrome/content/zotero/elements/annotationItemsPane.js b/chrome/content/zotero/elements/annotationItemsPane.js new file mode 100644 index 0000000000..aa06a643c0 --- /dev/null +++ b/chrome/content/zotero/elements/annotationItemsPane.js @@ -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 . + + ***** END LICENSE BLOCK ***** +*/ + +{ + class AnnotationItemsPane extends XULElementBase { + content = MozXULElement.parseXULToFragment(` + + + `); + + 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); +} diff --git a/chrome/content/zotero/elements/annotationRow.js b/chrome/content/zotero/elements/annotationRow.js index a5d8f23e49..68e2fddcab 100644 --- a/chrome/content/zotero/elements/annotationRow.js +++ b/chrome/content/zotero/elements/annotationRow.js @@ -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()); } } diff --git a/chrome/content/zotero/elements/collapsibleSection.js b/chrome/content/zotero/elements/collapsibleSection.js index 1dfa3c575f..b3fc51e5ca 100644 --- a/chrome/content/zotero/elements/collapsibleSection.js +++ b/chrome/content/zotero/elements/collapsibleSection.js @@ -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) => { diff --git a/chrome/content/zotero/elements/itemPane.js b/chrome/content/zotero/elements/itemPane.js index f95af15acf..2edfb0dc04 100644 --- a/chrome/content/zotero/elements/itemPane.js +++ b/chrome/content/zotero/elements/itemPane.js @@ -36,6 +36,8 @@ previousfocus="zotero-items-tree" /> + + `); @@ -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) { diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx index 9f925c6e96..de8ae4d773 100644 --- a/chrome/content/zotero/itemTree.jsx +++ b/chrome/content/zotero/itemTree.jsx @@ -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 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= 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); } diff --git a/chrome/content/zotero/locateMenu.js b/chrome/content/zotero/locateMenu.js index 7e702ae5c2..b7169d1ceb 100644 --- a/chrome/content/zotero/locateMenu.js +++ b/chrome/content/zotero/locateMenu.js @@ -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; } diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js index 8585db8f20..e3cb8240a0 100644 --- a/chrome/content/zotero/standalone/standalone.js +++ b/chrome/content/zotero/standalone/standalone.js @@ -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); diff --git a/chrome/content/zotero/xpcom/annotations.js b/chrome/content/zotero/xpcom/annotations.js index 92ec4bd0b6..330c8f74aa 100644 --- a/chrome/content/zotero/xpcom/annotations.js +++ b/chrome/content/zotero/xpcom/annotations.js @@ -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; + }; /** diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index 4ebb920dfd..81e083a43c 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -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 { diff --git a/chrome/content/zotero/xpcom/data/search.js b/chrome/content/zotero/xpcom/data/search.js index 55f25b247d..bda25b9158 100644 --- a/chrome/content/zotero/xpcom/data/search.js +++ b/chrome/content/zotero/xpcom/data/search.js @@ -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 item.isAnnotation())) { + format = Zotero.QuickCopy.getNoteFormat(); + items = [Zotero.QuickCopy.annotationsToNote(items)]; + } + Zotero.debug("Dragging with format " + format); format = Zotero.QuickCopy.unserializeSetting(format); try { diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index f00f00c459..01e3b9330a 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -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')); } } diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml index 08103a5c49..595f90399f 100644 --- a/chrome/content/zotero/zoteroPane.xhtml +++ b/chrome/content/zotero/zoteroPane.xhtml @@ -110,6 +110,9 @@ + @@ -388,6 +391,11 @@ command="cmd_zotero_copyBibliography" key="key_copyBibliography" hidden="true"/> +