From ac6c26096e916a50e3ea73635258e398e64b1120 Mon Sep 17 00:00:00 2001 From: Bogdan Abaev Date: Mon, 18 May 2026 19:07:09 -0700 Subject: [PATCH] all content generation in getContentFromItems - getContentFromItems is the single content producer for clipboard copy, drag-and-drop from library and reader tabs. Handles annotation wrapping, note-citation reformatting, and bib-mode regular-item filtering internally. - writeToClipboard replaces copyItemsToClipboard and exportItemsToClipboard. - getFormat takes { mode, items }; note-only selections return the note pref's format regardless of mode. getNoteFormat removed. --- .../content/zotero/elements/itemPaneHeader.js | 13 +- chrome/content/zotero/fileInterface.js | 181 ++++------------ .../content/zotero/standalone/standalone.js | 2 +- chrome/content/zotero/xpcom/quickCopy.js | 193 ++++++++++++------ chrome/content/zotero/xpcom/reader.js | 50 +---- .../zotero/xpcom/utilities_internal.js | 89 +++----- chrome/content/zotero/zoteroPane.js | 55 +---- test/tests/fileInterfaceTest.js | 58 +++--- test/tests/quickCopyTest.js | 55 ++--- 9 files changed, 277 insertions(+), 419 deletions(-) diff --git a/chrome/content/zotero/elements/itemPaneHeader.js b/chrome/content/zotero/elements/itemPaneHeader.js index 3da017133a..330355ef21 100644 --- a/chrome/content/zotero/elements/itemPaneHeader.js +++ b/chrome/content/zotero/elements/itemPaneHeader.js @@ -398,13 +398,16 @@ Zotero.Utilities.Internal.copyTextToClipboard(this.creatorYear.textContent); } else if (selectedMode === 'bibEntry') { - Zotero_File_Interface.copyItemsToClipboard( + let content = Zotero.QuickCopy.getContentFromItems( [this._item], - Zotero.Prefs.get(PREF_BIB_ENTRY_STYLE), - Zotero.Prefs.get(PREF_BIB_ENTRY_LOCALE), - false, - false + { + mode: 'bibliography', + id: Zotero.Prefs.get(PREF_BIB_ENTRY_STYLE), + contentType: '', + locale: Zotero.Prefs.get(PREF_BIB_ENTRY_LOCALE) + } ); + Zotero_File_Interface.writeToClipboard(content); } } diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js index 7c0c9cdc5f..857e9cfa09 100644 --- a/chrome/content/zotero/fileInterface.js +++ b/chrome/content/zotero/fileInterface.js @@ -179,7 +179,6 @@ var Zotero_File_Interface = new function () { var _unlock; this.exportCollection = exportCollection; - this.exportItemsToClipboard = exportItemsToClipboard; this.exportItems = exportItems; this.bibliographyFromItems = bibliographyFromItems; @@ -238,85 +237,42 @@ var Zotero_File_Interface = new function () { } - /* - * exports items to clipboard + /** + * Write QuickCopy content to the system clipboard. + * + * Accepts the `{ text, html? }` shape returned by getContentFromItems(). + * Each present flavor is added; consumers (plain vs. rich-text targets) + * each pick what they support. + * + * @param {{text: String, html?: String}} content */ - function exportItemsToClipboard(items, format) { - function _translate(items, format, callback) { - let translation = new Zotero.Translate.Export(); - translation.setItems(items.slice()); - translation.setTranslator(format.id); - if (format.options) { - translation.setDisplayOptions(format.options); - } - translation.setHandler("done", callback); - translation.translate(); + this.writeToClipboard = function (content) { + if (!content) return; + let { text, html } = content; + + let transferable = Components.classes['@mozilla.org/widget/transferable;1'] + .createInstance(Components.interfaces.nsITransferable); + let clipboardService = Components.classes['@mozilla.org/widget/clipboard;1'] + .getService(Components.interfaces.nsIClipboard); + transferable.init(null); + + if (html) { + let str = Components.classes['@mozilla.org/supports-string;1'] + .createInstance(Components.interfaces.nsISupportsString); + str.data = html; + transferable.addDataFlavor('text/html'); + transferable.setTransferData('text/html', str, html.length * 2); } - - // If translating with virtual "Markdown + Rich Text" translator, use Note Markdown and - // Note HTML instead - if (format.id == Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) { - let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, options: format.markdownOptions }; - let htmlFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML, options: format.htmlOptions }; - _translate(items, markdownFormat, (obj, worked) => { - if (!worked) { - Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); - return; - } - _translate(items, htmlFormat, (obj2, worked) => { - if (!worked) { - Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); - return; - } - - let text = obj.string.replace(/\r\n/g, '\n'); - let html = obj2.string.replace(/\r\n/g, '\n'); - - // copy to clipboard - let transferable = Components.classes['@mozilla.org/widget/transferable;1'] - .createInstance(Components.interfaces.nsITransferable); - let clipboardService = Components.classes['@mozilla.org/widget/clipboard;1'] - .getService(Components.interfaces.nsIClipboard); - - // Add Text - let str = Components.classes['@mozilla.org/supports-string;1'] - .createInstance(Components.interfaces.nsISupportsString); - str.data = text; - transferable.addDataFlavor('text/plain'); - transferable.setTransferData('text/plain', str, text.length * 2); - - // Add HTML - str = Components.classes['@mozilla.org/supports-string;1'] - .createInstance(Components.interfaces.nsISupportsString); - str.data = html; - transferable.addDataFlavor('text/html'); - transferable.setTransferData('text/html', str, html.length * 2); - - clipboardService.setData( - transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard - ); - }); - }); + if (text) { + let str = Components.classes['@mozilla.org/supports-string;1'] + .createInstance(Components.interfaces.nsISupportsString); + str.data = text; + transferable.addDataFlavor('text/plain'); + transferable.setTransferData('text/plain', str, text.length * 2); } - else { - _translate(items, format, (obj, worked) => { - if (!worked) { - Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); - return; - } - let text = obj.string; - // For Note HTML translator use body content only - if (format.id == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) { - let parser = new DOMParser(); - let doc = parser.parseFromString(text, 'text/html'); - text = doc.body.innerHTML; - } - Components.classes['@mozilla.org/widget/clipboardhelper;1'] - .getService(Components.interfaces.nsIClipboardHelper) - .copyString(text.replace(/\r\n/g, '\n')); - }); - } - } + + clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard); + }; this.getMendeleyDirectory = function () { @@ -755,70 +711,6 @@ var Zotero_File_Interface = new function () { } - /** - * Copies HTML and text citations or bibliography entries for passed items in given style - * - * Does not check that items are actual references (and not notes or attachments) - * - * @param {Zotero.Item[]} items - * @param {String} style - Style id string (e.g., 'http://www.zotero.org/styles/apa') - * @param {String} locale - Locale (e.g., 'en-US') - * @param {Boolean} [asHTML=false] - Use HTML source for plain-text data - * @param {Boolean} [asCitations=false] - Copy citation cluster instead of bibliography - */ - this.copyItemsToClipboard = function (items, style, locale, asHTML, asCitations) { - var d = new Date(); - - // copy to clipboard - var transferable = Components.classes["@mozilla.org/widget/transferable;1"]. - createInstance(Components.interfaces.nsITransferable); - var clipboardService = Components.classes["@mozilla.org/widget/clipboard;1"]. - getService(Components.interfaces.nsIClipboard); - style = Zotero.Styles.get(style); - var cslEngine = style.getCiteProc(locale, 'html', { cache: true }); - - if (asCitations) { - cslEngine.updateItems(items.map(item => item.id)); - var citation = { - citationItems: items.map(item => ({ id: item.id })), - properties: {} - }; - var output = cslEngine.previewCitationCluster(citation, [], [], "html"); - } - else { - var output = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html"); - } - - // add HTML - var str = Components.classes["@mozilla.org/supports-string;1"]. - createInstance(Components.interfaces.nsISupportsString); - str.data = output; - transferable.addDataFlavor("text/html"); - transferable.setTransferData("text/html", str, output.length * 2); - - // If not "Copy as HTML", add plaintext; otherwise use HTML from above and just mark as text - if(!asHTML) { - if (asCitations) { - output = cslEngine.previewCitationCluster(citation, [], [], "text"); - } - else { - output = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, 'text'); - } - } - cslEngine.free(); - - var str = Components.classes["@mozilla.org/supports-string;1"]. - createInstance(Components.interfaces.nsISupportsString); - str.data = output; - transferable.addDataFlavor("text/plain"); - transferable.setTransferData("text/plain", str, output.length * 2); - - clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard); - - Zotero.debug(`Copied bibliography to clipboard in ${new Date() - d} ms`); - } - - /* * Shows bibliography options and creates a bibliography */ @@ -852,7 +744,12 @@ var Zotero_File_Interface = new function () { // generate bibliography try { if(io.method == 'copy-to-clipboard') { - Zotero_File_Interface.copyItemsToClipboard(items, io.style, locale, false, io.mode === "citations"); + let content = Zotero.QuickCopy.getContentFromItems( + items, + { mode: 'bibliography', id: io.style, contentType: '', locale }, + { asCitations: io.mode === "citations" } + ); + Zotero_File_Interface.writeToClipboard(content); } else { var style = Zotero.Styles.get(io.style); diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js index 6f245e423d..dfff443f2c 100644 --- a/chrome/content/zotero/standalone/standalone.js +++ b/chrome/content/zotero/standalone/standalone.js @@ -367,7 +367,7 @@ const ZoteroStandalone = new function () { document.l10n.setAttributes(copyAnnotation, "menu-edit-copy-annotation", { count: selected.length }); // Label the export menu with the selected translator name - var exportFormat = Zotero.QuickCopy.getFormat('export'); + var exportFormat = Zotero.QuickCopy.getFormat({ mode: 'export' }); if (exportFormat.id) { try { let translator = Zotero.Translators.get(exportFormat.id); diff --git a/chrome/content/zotero/xpcom/quickCopy.js b/chrome/content/zotero/xpcom/quickCopy.js index 816844feb9..91965c13b9 100644 --- a/chrome/content/zotero/xpcom/quickCopy.js +++ b/chrome/content/zotero/xpcom/quickCopy.js @@ -235,11 +235,6 @@ Zotero.QuickCopy = new function () { return ''; }; - this.getNoteFormat = function () { - return this.unserializeSetting(Zotero.Prefs.get('export.noteQuickCopy.setting')); - }; - - this.getLocale = function () { return this.unserializeSetting( Zotero.Prefs.get('export.quickCopy.bibliographySetting') @@ -303,12 +298,21 @@ Zotero.QuickCopy = new function () { /** - * Get the Quick Copy format for the currently active URL if any. + * Get the Quick Copy format for the currently active URL. + * When `items` is a selection composed entirely of notes/annotations, + * return the user's note-format pref. * - * @param {String} [mode] - Optional 'bibliography' or 'export' - omit for drag-drop - * @return {Object} - `{mode, id, contentType, locale}` for bibliography; `{mode, id}` for export + * @param {Object} [options] + * @param {String} [options.mode] - 'bibliography' or 'export'; omit for drag-drop + * @param {Zotero.Item[]} [options.items] - Items the format will be applied to + * @return {Object} - `{mode, id, contentType, locale}` for bibliography; `{mode, id, ...}` for export */ - this.getFormat = function (mode) { + this.getFormat = function ({ mode, items } = {}) { + // Note-only selections always use the note format + if (items && items.length && items.every(item => item.isNote() || item.isAnnotation())) { + return this.unserializeSetting(Zotero.Prefs.get('export.noteQuickCopy.setting')); + } + var siteRaw = _findSiteFormatForURL(this.lastActiveURL); var site = siteRaw ? this.parseSiteFormat(siteRaw) : null; @@ -334,80 +338,141 @@ Zotero.QuickCopy = new function () { }; - /* - * Get text and (when applicable) HTML content from items + /** + * Produce QuickCopy content from items. Single entry point for clipboard + * copy, drag-and-drop, and the Bibliography dialog. Item-type prep is + * handled here: annotations are wrapped via annotationsToNote, notes are + * passed through reformatNoteCitations, and bibliography mode is filtered + * to regular items. Always returns { text, html }, + * unless export translator provides no html format. * - * |items| is an array of Zotero.Item objects - * - * |format| may be a Quick Copy format string - * (e.g. "bibliography=http://www.zotero.org/styles/apa") - * or an Quick Copy format object - * - * |callback| is only necessary if using an export format and should be - * a function suitable for Zotero.Translate.setHandler, taking parameters - * |obj| and |worked|. The generated content should be placed in obj.string - * and |worked| should be true if the operation is successful. - * - * If bibliography format, the process is synchronous and an object - * contain properties 'text' and 'html' is returned. + * @param {Zotero.Item[]} items + * @param {String|Object} format + * @param {Object} [options] + * @param {Boolean} [options.asCitations=false] - Bibliography only: in-text citation cluster + * @return {{text: String, html?: String} | null} */ - this.getContentFromItems = function (items, format, callback, modified) { - if (items.length > Zotero.Prefs.get('export.quickCopy.dragLimit')) { - Zotero.debug("Skipping quick copy for " + items.length + " items"); - return false; - } + this.getContentFromItems = function (items, format, options = {}) { + if (!items.length) return null; format = this.unserializeSetting(format); - - if (format.mode == 'export') { - var translation = new Zotero.Translate.Export; - translation.noWait = true; // needed not to break drags - // Allow to reuse items array - translation.setItems(items.slice()); - translation.setTranslator(format.id); - if (format.options) { - translation.setDisplayOptions(format.options); - } - translation.setHandler("done", callback); - translation.translate(); - return true; - } - else if (format.mode == 'bibliography') { - items = items.filter(item => !item.isNote()); - var locale = format.locale || this.getLocale(); - - // Copy citations if shift key pressed - if (modified) { - var csl = Zotero.Styles.get(format.id).getCiteProc(locale, "text", { cache: true }); + // Format-appropriate item transformations: + // annotations → wrap into a temp note, + // notes → reformat embedded citations in the current bib style, + // bibliography mode → keep only regular items. + if (items.every(item => item.isAnnotation())) { + items = [this.annotationsToNote(items)]; + } + if (items.every(item => item.isNote())) { + items = this.reformatNoteCitations(items); + } + if (format.mode === 'bibliography') { + items = items.filter(item => item.isRegularItem()); + } + if (!items.length) return null; + + if (format.mode === 'export') { + // Markdown+RichText virtual translator: produces both flavors, + // using its `markdownOptions` and `htmlOptions` independently. + if (format.id === Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) { + let text = _runExportTranslator(items, { + mode: 'export', + id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, + options: format.markdownOptions + }); + let html = _runExportTranslator(items, { + mode: 'export', + id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML, + options: format.htmlOptions + }); + if (text === null || html === null) return null; + return { + text: text.replace(/\r\n/g, '\n'), + html: html.replace(/\r\n/g, '\n') + }; + } + + // Note HTML: HTML output, exposed on both flavors so rich-text + // targets get the formatting and plain editors get the source. + if (format.id === Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) { + let output = _runExportTranslator(items, format); + if (output === null) return null; + output = output.replace(/\r\n/g, '\n'); + let parser = new DOMParser(); + let doc = parser.parseFromString(output, 'text/html'); + output = doc.body.innerHTML; + return { text: output, html: output }; + } + + // Other export translators (e.g. BibTeX): single-flavor output. + let output = _runExportTranslator(items, format); + if (output === null) return null; + return { text: output.replace(/\r\n/g, '\n') }; + } + + if (format.mode === 'bibliography') { + let locale = format.locale || this.getLocale(); + let style = Zotero.Styles.get(format.id); + if (!style) return null; + + // Bibliography mode always produces both flavors: plain text for + // `text/plain`, HTML for `text/html`. "Copy as HTML" (contentType + // === 'html') sends the HTML version to `text/plain` as well, so + // plain editors paste HTML source. + let html, text; + if (options.asCitations) { + let csl = style.getCiteProc(locale, 'html', { cache: true }); csl.updateItems(items.map(item => item.id)); - var citation = { + let citation = { citationItems: items.map(item => ({ id: item.id })), properties: {} }; - var html = csl.previewCitationCluster(citation, [], [], "html"); - var text = csl.previewCitationCluster(citation, [], [], "text"); + html = csl.previewCitationCluster(citation, [], [], 'html'); + text = csl.previewCitationCluster(citation, [], [], 'text'); csl.free(); } else { - var style = Zotero.Styles.get(format.id); - var cslEngine = style.getCiteProc(locale, 'html', { cache: true }); - var html = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html"); - cslEngine.free(); + let cslEngine = style.getCiteProc(locale, 'html', { cache: true }); + html = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, 'html'); + cslEngine.free(); cslEngine = style.getCiteProc(locale, 'text', { cache: true }); - var text = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "text"); + text = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, 'text'); cslEngine.free(); } - + return { - text: format.contentType == "html" ? html : text, - html, + text: format.contentType === 'html' ? html : text, + html }; } - - throw ("Invalid mode '" + format.mode + "' in Zotero.QuickCopy.getContentFromItems()"); + + throw new Error(`Invalid Quick Copy mode '${format.mode}'`); }; + + /** + * Run an export translator synchronously (relies on noWait + preloaded + * translator code) and return its string output, or null on failure. + */ + function _runExportTranslator(items, format) { + let result = null; + let translation = new Zotero.Translate.Export(); + translation.noWait = true; + translation.setItems(items.slice()); + translation.setTranslator(format.id); + if (format.options) { + translation.setDisplayOptions(format.options); + } + translation.setHandler("done", (obj, worked) => { + if (worked) { + result = obj.string; + } + }); + translation.translate(); + return result; + } + /** * Generate a note item to pass to getContentFromItems() from an array of annotations * @@ -462,7 +527,7 @@ Zotero.QuickCopy = new function () { // Use getFormat() so site-specific bibliography overrides apply when // the user is on a URL with a configured site setting - let format = this.getFormat('bibliography'); + let format = this.getFormat({ mode: 'bibliography' }); if (format.mode !== 'bibliography' || !format.id) { return items; } diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index 9d592dbf99..7c48ae65bb 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -410,48 +410,20 @@ class ReaderInstance { if (fromText) { return; } - // annotations are wrapped in a temp note for translation + // Convert the iframe's JSON annotations into a single note + // item; getContentFromItems handles the rest (reformatting + // embedded citations, picking the right output flavors). let items = [Zotero.QuickCopy.annotationsToNote(annotations)]; - // Reformat any citations in the annotation note using the - // current bibliography style (matches main pane drag behavior) - items = Zotero.QuickCopy.reformatNoteCitations(items); - let format = Zotero.QuickCopy.getNoteFormat(); + let format = Zotero.QuickCopy.getFormat({ items }); Zotero.debug(`Copying/dragging (${annotations.length}) annotation(s) with ${JSON.stringify(format)}`); - // Basically the same code is used in itemTree.jsx onDragStart - if (format.mode === 'export') { - // If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown - // and Note HTML translators instead - if (format.id === Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) { - let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, options: format.markdownOptions }; - let htmlFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML, options: format.htmlOptions }; - Zotero.QuickCopy.getContentFromItems(items, markdownFormat, (obj, worked) => { - if (!worked) { - return; - } - Zotero.QuickCopy.getContentFromItems(items, htmlFormat, (obj2, worked) => { - if (!worked) { - return; - } - dataTransfer.setData('text/plain', obj.string.replace(/\r\n/g, '\n')); - dataTransfer.setData('text/html', obj2.string.replace(/\r\n/g, '\n')); - }); - }); + + let content = Zotero.QuickCopy.getContentFromItems(items, format); + if (content) { + if (content.html) { + dataTransfer.setData('text/html', content.html); } - else { - Zotero.QuickCopy.getContentFromItems(items, format, (obj, worked) => { - if (!worked) { - return; - } - var text = obj.string.replace(/\r\n/g, '\n'); - // For Note HTML translator use body content only - if (format.id === Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) { - // Use body content only - let parser = new DOMParser(); - let doc = parser.parseFromString(text, 'text/html'); - text = doc.body.innerHTML; - } - dataTransfer.setData('text/plain', text); - }); + if (content.text) { + dataTransfer.setData('text/plain', content.text); } } } diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js index 174eb2f5fe..6c04b30902 100644 --- a/chrome/content/zotero/xpcom/utilities_internal.js +++ b/chrome/content/zotero/xpcom/utilities_internal.js @@ -3401,73 +3401,46 @@ Zotero.Utilities.Internal.onDragItems = function (event, itemIDs, dragImage = ev } } - // Get Quick Copy format for current URL (set via /ping from connector) - let format = Zotero.QuickCopy.getFormat(); - - // If all items are annotations, wrap them in a note object for translation + // Skip Quick Copy data for very large drags so we don't hang the drag + // preview while generating bibliography for the full set. The limit + // applies only to drag; keyboard-shortcut copy has no cap. + if (items.length > Zotero.Prefs.get('export.quickCopy.dragLimit')) { + Zotero.debug("Skipping Quick Copy drag data for " + items.length + " items"); + return; + } + // When dragging annotation items, also set the zotero/annotation flavor + // so the note editor knows how to handle them. if (items.every(item => item.isAnnotation())) { - items = [Zotero.QuickCopy.annotationsToNote(items)]; + let jsonAnnotations = items.map((annotation) => { + let json = Zotero.Annotations.toJSONSync(annotation); + json.id = annotation.key; + delete json.key; + json.attachmentItemID = annotation.parentItemID; + json.tags = json.tags || []; + return json; + }); + event.dataTransfer.setData('zotero/annotation', JSON.stringify(jsonAnnotations)); } - // If all items are notes, use one of the note export translators - if (items.every(item => item.isNote())) { - format = Zotero.QuickCopy.getNoteFormat(); - items = Zotero.QuickCopy.reformatNoteCitations(items); - } + // Get Quick Copy format for current URL (set via /ping from connector) + // and the items being dragged. getContentFromItems handles the rest + // (annotation wrapping, note-citation reformatting, regular-item filter). + let format = Zotero.QuickCopy.getFormat({ items }); Zotero.debug("Dragging with format " + JSON.stringify(format)); try { - if (format.mode == 'export') { - // If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown - // and Note HTML translators instead - if (format.id === Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) { - let markdownFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN, options: format.markdownOptions }; - let htmlFormat = { mode: 'export', id: Zotero.Translators.TRANSLATOR_ID_NOTE_HTML, options: format.htmlOptions }; - Zotero.QuickCopy.getContentFromItems(items, markdownFormat, (obj, worked) => { - if (!worked) { - Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); - return; - } - Zotero.QuickCopy.getContentFromItems(items, htmlFormat, (obj2, worked) => { - if (!worked) { - Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); - return; - } - event.dataTransfer.setData('text/plain', obj.string.replace(/\r\n/g, '\n')); - event.dataTransfer.setData('text/html', obj2.string.replace(/\r\n/g, '\n')); - }); - }); + let content = Zotero.QuickCopy.getContentFromItems(items, format, { + // Shift on drag → in-text citation (bibliography mode only) + asCitations: format.mode === 'bibliography' && event.shiftKey + }); + if (content) { + if (content.html) { + event.dataTransfer.setData('text/html', content.html); } - else { - Zotero.QuickCopy.getContentFromItems(items, format, (obj, worked) => { - if (!worked) { - Zotero.log(Zotero.getString('fileInterface.exportError'), 'warning'); - return; - } - let text = obj.string.replace(/\r\n/g, '\n'); - // For Note HTML translator use body content only - if (format.id == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) { - // Use body content only - let parser = new DOMParser(); - let doc = parser.parseFromString(text, 'text/html'); - text = doc.body.innerHTML; - } - event.dataTransfer.setData('text/plain', text); - }); + if (content.text) { + event.dataTransfer.setData('text/plain', content.text); } } - else if (format.mode == 'bibliography') { - let content = Zotero.QuickCopy.getContentFromItems(items, format, null, event.shiftKey); - if (content) { - if (content.html) { - event.dataTransfer.setData("text/html", content.html); - } - event.dataTransfer.setData("text/plain", content.text); - } - } - else { - Zotero.logError("Invalid Quick Copy mode"); - } } catch (e) { Zotero.debug(e); diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index fe1ac8da25..1dba92c990 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -2698,62 +2698,23 @@ var ZoteroPane = new function () { this.copySelectedItemsToClipboard = function (asCitations, mode = 'bibliography') { - var items = []; let itemIDs = this.getSelectedItems(true); // Get selected item IDs in the item tree order itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id)); - items = Zotero.Items.get(itemIDs); + let items = Zotero.Items.get(itemIDs); + if (!items.length) return; - if (!items.length) { - return; - } + let format = Zotero.QuickCopy.getFormat({ mode, items }); - var format = Zotero.QuickCopy.getFormat(mode); - if (items.every(item => item.isNote() || item.isAttachment() || item.isAnnotation())) { - format = Zotero.QuickCopy.getNoteFormat(); - } - // To copy annotations, wrap them in a temp note - if (items.every(item => item.isAnnotation())) { - items = [Zotero.QuickCopy.annotationsToNote(items)]; - } - if (items.every(item => item.isNote())) { - items = Zotero.QuickCopy.reformatNoteCitations(items); - } + // asCitations only applies to bibliography mode + if (asCitations && format.mode !== 'bibliography') return; - // In bibliography mode, remove notes and attachments - if (format.mode == 'bibliography') { - items = items.filter(item => item.isRegularItem()); - } - - // DEBUG: We could copy notes via keyboard shortcut if we altered - // Z_F_I.copyItemsToClipboard() to use Z.QuickCopy.getContentFromItems(), - // but 1) we'd need to override that function's drag limit and 2) when I - // tried it the OS X clipboard seemed to be getting text vs. HTML wrong, - // automatically converting text/html to plaintext rather than using - // text/unicode. (That may be fixable, however.) - // - // This isn't currently shown, because the commands are disabled when not relevant, so this - // function isn't called - if (!items.length) { + let content = Zotero.QuickCopy.getContentFromItems(items, format, { asCitations }); + if (!content) { Services.prompt.alert(null, "", Zotero.getString("fileInterface.noReferencesError")); return; } - - if (format.mode == 'bibliography') { - var locale = format.locale; - Zotero_File_Interface.copyItemsToClipboard( - items, format.id, locale, format.contentType == 'html', asCitations - ); - } - else if (format.mode == 'export') { - // Copy citations doesn't work in export mode - if (asCitations) { - return; - } - else { - Zotero_File_Interface.exportItemsToClipboard(items, format); - } - } + Zotero_File_Interface.writeToClipboard(content); } diff --git a/test/tests/fileInterfaceTest.js b/test/tests/fileInterfaceTest.js index 905d89875c..8d5ee62735 100644 --- a/test/tests/fileInterfaceTest.js +++ b/test/tests/fileInterfaceTest.js @@ -132,7 +132,7 @@ describe("Zotero_File_Interface", function () { }); - describe("#copyItemsToClipboard()", function () { + describe("#writeToClipboard()", function () { var clipboardService, item1, item2; before(function* () { @@ -164,73 +164,70 @@ describe("Zotero_File_Interface", function () { // Non-"Copy as HTML" mode // it("should copy HTML and text citations to the clipboard", async function () { - win.Zotero_File_Interface.copyItemsToClipboard( + let content = Zotero.QuickCopy.getContentFromItems( [item1, item2], - 'http://www.zotero.org/styles/apa', - 'en-US', - false, - true + { mode: 'bibliography', id: 'http://www.zotero.org/styles/apa', contentType: '', locale: 'en-US' }, + { asCitations: true } ); - + win.Zotero_File_Interface.writeToClipboard(content); + // HTML var str = getDataForFlavor('text/html'); assert.equal(str, '(A, 2016; B, 2016)'); - + // Plain text str = getDataForFlavor('text/plain'); assert.equal(str, '(A, 2016; B, 2016)'); }); - + it("should copy HTML and text bibliography to the clipboard", async function () { - win.Zotero_File_Interface.copyItemsToClipboard( + let content = Zotero.QuickCopy.getContentFromItems( [item1, item2], - 'http://www.zotero.org/styles/apa', - 'en-US' + { mode: 'bibliography', id: 'http://www.zotero.org/styles/apa', contentType: '', locale: 'en-US' } ); - + win.Zotero_File_Interface.writeToClipboard(content); + var str = getDataForFlavor('text/html'); assert.include(str, 'line-height'); assert.include(str, 'A'); assert.include(str, 'B'); - + // Plain text str = getDataForFlavor('text/plain'); assert.equal(str, 'A. (2016).\nB. (2016).\n'); }); - + // // "Copy as HTML" mode // it("should copy HTML and HTML source citations to the clipboard", async function () { - win.Zotero_File_Interface.copyItemsToClipboard( + let content = Zotero.QuickCopy.getContentFromItems( [item1, item2], - 'http://www.zotero.org/styles/apa', - 'en-US', - true, - true + { mode: 'bibliography', id: 'http://www.zotero.org/styles/apa', contentType: 'html', locale: 'en-US' }, + { asCitations: true } ); - + win.Zotero_File_Interface.writeToClipboard(content); + var str = getDataForFlavor('text/html'); assert.equal(str, '(A, 2016; B, 2016)'); - + // Plain text str = getDataForFlavor('text/plain'); assert.equal(str, '(A, 2016; B, 2016)'); }); - + it("should copy HTML and HTML source bibliography to the clipboard", async function () { - win.Zotero_File_Interface.copyItemsToClipboard( + let content = Zotero.QuickCopy.getContentFromItems( [item1, item2], - 'http://www.zotero.org/styles/apa', - 'en-US', - true + { mode: 'bibliography', id: 'http://www.zotero.org/styles/apa', contentType: 'html', locale: 'en-US' } ); - + win.Zotero_File_Interface.writeToClipboard(content); + var str = getDataForFlavor('text/html'); assert.include(str, 'line-height'); assert.include(str, 'A'); assert.include(str, 'B'); - + // Plain text str = getDataForFlavor('text/plain'); assert.include(str, 'line-height'); @@ -475,7 +472,8 @@ describe("Zotero_File_Interface", function () { markdownOptions: { includeAppLinks: false }, htmlOptions: { includeAppLinks: false } }; - win.Zotero_File_Interface.exportItemsToClipboard(items, format); + let content = Zotero.QuickCopy.getContentFromItems(items, format); + win.Zotero_File_Interface.writeToClipboard(content); await Zotero.Promise.delay(); var text = getClipboardText(); diff --git a/test/tests/quickCopyTest.js b/test/tests/quickCopyTest.js index ce7b8fba1e..966566a883 100644 --- a/test/tests/quickCopyTest.js +++ b/test/tests/quickCopyTest.js @@ -80,7 +80,7 @@ describe("Zotero.QuickCopy", function () { await Zotero.QuickCopy.loadSiteSettings(); Zotero.QuickCopy.lastActiveURL = 'https://wiki.test.org/page'; - let bib = Zotero.QuickCopy.getFormat('bibliography'); + let bib = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); assert.equal(bib.id, APA_STYLE_ID); }); @@ -107,14 +107,14 @@ describe("Zotero.QuickCopy", function () { await Zotero.QuickCopy.loadSiteSettings(); Zotero.QuickCopy.lastActiveURL = 'https://test.org/styles/apa'; - let bib = Zotero.QuickCopy.getFormat('bibliography'); + let bib = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); assert.equal(bib.id, APA_STYLE_ID); }); describe("default settings (no site match)", function () { it("should return the default bibliographySetting for mode='bibliography'", function () { Zotero.QuickCopy.lastActiveURL = 'https://no-match.test/'; - let result = Zotero.QuickCopy.getFormat('bibliography'); + let result = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); let expected = Zotero.QuickCopy.unserializeSetting( Zotero.Prefs.get('export.quickCopy.bibliographySetting') ); @@ -124,7 +124,7 @@ describe("Zotero.QuickCopy", function () { it("should return the default exportSetting for mode='export'", function () { Zotero.QuickCopy.lastActiveURL = 'https://no-match.test/'; - let result = Zotero.QuickCopy.getFormat('export'); + let result = Zotero.QuickCopy.getFormat({ mode: 'export' }); let expected = Zotero.QuickCopy.unserializeSetting( Zotero.Prefs.get('export.quickCopy.exportSetting') ); @@ -156,14 +156,14 @@ describe("Zotero.QuickCopy", function () { await setSiteSetting(legacy); // bibliography mode → site override - let bib = Zotero.QuickCopy.getFormat('bibliography'); + let bib = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); assert.equal(bib.mode, 'bibliography'); assert.equal(bib.id, APA_STYLE_ID); assert.equal(bib.contentType, 'html'); assert.equal(bib.locale, 'en-US'); // export mode → no site override → global default - let exp = Zotero.QuickCopy.getFormat('export'); + let exp = Zotero.QuickCopy.getFormat({ mode: 'export' }); let globalExport = Zotero.QuickCopy.unserializeSetting( Zotero.Prefs.get('export.quickCopy.exportSetting') ); @@ -185,12 +185,12 @@ describe("Zotero.QuickCopy", function () { await setSiteSetting(legacy); // export mode → site override - let exp = Zotero.QuickCopy.getFormat('export'); + let exp = Zotero.QuickCopy.getFormat({ mode: 'export' }); assert.equal(exp.mode, 'export'); assert.equal(exp.id, BIBTEX_TRANSLATOR_ID); // bibliography mode → no site override → global default - let bib = Zotero.QuickCopy.getFormat('bibliography'); + let bib = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); let globalBib = Zotero.QuickCopy.unserializeSetting( Zotero.Prefs.get('export.quickCopy.bibliographySetting') ); @@ -207,7 +207,7 @@ describe("Zotero.QuickCopy", function () { Zotero.QuickCopy.lastActiveURL = `https://${domain}/`; await setSiteSetting(`bibliography/html=${APA_STYLE_ID}`); - let bib = Zotero.QuickCopy.getFormat('bibliography'); + let bib = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); assert.equal(bib.mode, 'bibliography'); assert.equal(bib.id, APA_STYLE_ID); assert.equal(bib.contentType, 'html'); @@ -227,13 +227,13 @@ describe("Zotero.QuickCopy", function () { }); await setSiteSetting(siteFormat); - let bib = Zotero.QuickCopy.getFormat('bibliography'); + let bib = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); assert.equal(bib.mode, 'bibliography'); assert.equal(bib.id, APA_STYLE_ID); assert.equal(bib.locale, 'fr-FR'); // export → no site override → global default - let exp = Zotero.QuickCopy.getFormat('export'); + let exp = Zotero.QuickCopy.getFormat({ mode: 'export' }); let globalExport = Zotero.QuickCopy.unserializeSetting( Zotero.Prefs.get('export.quickCopy.exportSetting') ); @@ -254,11 +254,11 @@ describe("Zotero.QuickCopy", function () { }); await setSiteSetting(siteFormat); - let bib = Zotero.QuickCopy.getFormat('bibliography'); + let bib = Zotero.QuickCopy.getFormat({ mode: 'bibliography' }); assert.equal(bib.mode, 'bibliography'); assert.equal(bib.id, APA_STYLE_ID); - let exp = Zotero.QuickCopy.getFormat('export'); + let exp = Zotero.QuickCopy.getFormat({ mode: 'export' }); assert.equal(exp.mode, 'export'); assert.equal(exp.id, BIBTEX_TRANSLATOR_ID); @@ -288,11 +288,9 @@ describe("Zotero.QuickCopy", function () { describe("#getContentFromItems()", function () { it("should generate BibTeX", async function () { var item = await createDataObject('item'); - var content = ""; - var worked = false; await Zotero.Translators.init(); - + var translatorID = '9cb70025-a888-4a29-a210-93ec52da40d4'; // BibTeX var format = 'export=' + translatorID; Zotero.Prefs.set("export.quickCopy.exportSetting", format); @@ -301,17 +299,10 @@ describe("Zotero.QuickCopy", function () { while (!translator.code) { await Zotero.Promise.delay(50); } - - Zotero.QuickCopy.getContentFromItems( - [item], - format, - (obj, w) => { - content = obj.string; - worked = w; - } - ); - assert.isTrue(worked); - assert.isTrue(content.trim().startsWith('@')); + + let content = Zotero.QuickCopy.getContentFromItems([item], format); + assert.isString(content.text); + assert.isTrue(content.text.trim().startsWith('@')); }); }); @@ -319,13 +310,11 @@ describe("Zotero.QuickCopy", function () { var item = createUnsavedDataObject('item', { itemType: 'webpage', title: 'Foo' }); item.setField('date', '2020-03-11'); await item.saveTx(); - var content = ""; - var worked = false; - + // This shouldn't be used Zotero.Prefs.set('export.lastLocale', 'fr-FR'); await Zotero.Styles.init(); - + var format = 'bibliography=http://www.zotero.org/styles/apa'; Zotero.Prefs.set("export.quickCopy.bibliographySetting", format); @@ -357,7 +346,7 @@ describe("Zotero.QuickCopy", function () { await item.saveTx(); // Copy citation, not bibliography - let { text } = Zotero.QuickCopy.getContentFromItems([item], format, null, true); - assert.equal(text, '《新型数据财产的行为主义保护:基于财产权理论的分析》。'); + let content = Zotero.QuickCopy.getContentFromItems([item], format, { asCitations: true }); + assert.equal(content.text, '《新型数据财产的行为主义保护:基于财产权理论的分析》。'); }); })