From b2a884bf5f4a8d9ace3a1dd8cc019313d848e4ae Mon Sep 17 00:00:00 2001
From: Bogdan Abaev
Date: Mon, 4 May 2026 16:58:33 -0700
Subject: [PATCH] quickCopy refactoring
1. Separate export.quickCopy.setting into two separate settings
for Bibliography and Export modes. Updated prefs pane to
reflect that. CMD+Shift+C will copy regular items in Bibliography format.
New shortcut CMD+Shift+E will copy regular items in Export format.
New setting export.quickCopy.preferredFormatOnDrag determines which format
to use on drag. CMD+Shift+E also exposed as a new Edit > Export as {}
command. Bibliography and Export prefs are stored as stringified JSON
in the same format as site-specific settings for consistency.
2. Format citations in notes. On quick-copy, via keyboard shortcut or on drag,
if all items are notes (including mock notes that annotations are wrapped into),
go through all citations in the note, manually format them in the currently selected
export.quickCopy.bibliographySetting format and return a new unsaved mock note
with those citations replacing original default ones.
3. Site-specific settings for QuickCopy to allow the user to override
both Bibliography and Export modes. Updated quickCopySiteEditor to
allow to select two different formats to override. Updated
virtualized-table in prefs pane to display new site-specific settings.
General refactoring of Zotero.QuickCopy to store new format of site-specific setting
{ bibliography: {id, contentType, locale}, export: {id}, drag: 'bibliography|export' }
while also handling settings stored in previous format.
4. Refactoring of Zotero.QuickCopy.getFormatFromURL to Zotero.QuickCopy.getFormat
which handles the logic of handling the last viewed URL internally.
---
.../zotero/preferences/preferences_export.jsx | 506 +++++++++---------
.../preferences/preferences_export.xhtml | 38 +-
.../preferences/quickCopySiteEditor.xhtml | 193 ++++---
.../content/zotero/standalone/standalone.js | 33 +-
chrome/content/zotero/xpcom/prefs.js | 28 +-
chrome/content/zotero/xpcom/quickCopy.js | 433 ++++++++++++---
chrome/content/zotero/xpcom/reader.js | 6 +-
chrome/content/zotero/xpcom/style.js | 1 -
.../zotero/xpcom/utilities_internal.js | 17 +-
chrome/content/zotero/zoteroPane.js | 47 +-
chrome/content/zotero/zoteroPane.xhtml | 14 +-
chrome/locale/en-US/zotero/preferences.ftl | 12 +
defaults/preferences/zotero.js | 5 +-
test/tests/fileInterfaceTest.js | 180 +++++++
test/tests/quickCopyTest.js | 334 ++++++++++--
test/tests/utilities_internalTest.js | 186 +++++++
16 files changed, 1518 insertions(+), 515 deletions(-)
diff --git a/chrome/content/zotero/preferences/preferences_export.jsx b/chrome/content/zotero/preferences/preferences_export.jsx
index a99e6ff1d4..2a1b315f06 100644
--- a/chrome/content/zotero/preferences/preferences_export.jsx
+++ b/chrome/content/zotero/preferences/preferences_export.jsx
@@ -35,6 +35,7 @@ Zotero_Preferences.Export = {
this.updateQuickCopyInstructions();
await this.populateQuickCopyList();
await this.populateNoteQuickCopyList();
+ this.updateQuickCopyUI();
},
@@ -52,65 +53,44 @@ Zotero_Preferences.Export = {
/*
- * Builds the main Quick Copy drop-down from the current global pref
+ * Builds the bibliography, export, and locale dropdowns. Selection state
+ * is applied later by updateQuickCopyUI() reading from prefs.
*/
- populateQuickCopyList: async function() {
- // Initialize default format drop-down
- var format = Zotero.Prefs.get("export.quickCopy.setting");
- format = Zotero.QuickCopy.unserializeSetting(format);
- var menulist = document.getElementById("zotero-quickCopy-menu");
+ populateQuickCopyList: async function () {
await Zotero.Styles.init();
- var translators = await this.getQuickCopyTranslators();
- this.buildQuickCopyFormatDropDown(
- menulist, format.contentType, format, translators
+
+ this.buildBibliographyQuickCopyDropDown(
+ document.getElementById("zotero-quickCopy-bibliography-menu")
);
- menulist.setAttribute('preference', "extensions.zotero.export.quickCopy.setting");
-
- // Initialize locale drop-down
- var localeMenulist = document.getElementById("zotero-quickCopy-locale-menu");
- Zotero.Styles.populateLocaleList(localeMenulist);
- localeMenulist.addEventListener('syncfrompreference', () => {
- this._lastSelectedLocale = Zotero.Prefs.get("export.quickCopy.locale");
- this.updateQuickCopyUI();
- });
- localeMenulist.setAttribute('preference', "extensions.zotero.export.quickCopy.locale");
-
+
+ this.buildExportQuickCopyDropDown(
+ document.getElementById("zotero-quickCopy-export-menu"),
+ await this.getQuickCopyTranslators()
+ );
+
+ // Initial locale options; updateQuickCopyUI re-filters by selected style
+ Zotero.Styles.populateLocaleList(document.getElementById("zotero-quickCopy-locale-menu"));
+
await this.refreshQuickCopySiteList();
},
/*
- * Builds the note Quick Copy drop-down from the current global pref
+ * Builds the note Quick Copy drop-down. Items carry just the translator ID;
+ * options live in the dedicated checkboxes and are read by updateQuickCopyPrefs.
*/
populateNoteQuickCopyList: async function () {
document.getElementById('noteQuickCopy-format-options').removeAttribute('hidden');
-
- // Initialize default format drop-down
- var format = Zotero.Prefs.get("export.noteQuickCopy.setting");
- format = Zotero.QuickCopy.unserializeSetting(format);
- var menulist = document.getElementById("zotero-noteQuickCopy-menu");
- menulist.setAttribute('preference', "extensions.zotero.export.noteQuickCopy.setting");
- menulist.removeEventListener('command', this.updateNoteQuickCopyUI);
- menulist.addEventListener('command', this.updateNoteQuickCopyUI);
- if (!format) {
- format = menulist.value;
- }
-
- format = Zotero.QuickCopy.unserializeSetting(format);
-
- menulist.selectedItem = null;
+ var menulist = document.getElementById("zotero-noteQuickCopy-menu");
menulist.removeAllItems();
-
var popup = document.createXULElement('menupopup');
menulist.appendChild(popup);
- // add export formats to list
var translation = new Zotero.Translate("export");
var translators = await translation.getTranslators();
-
translators.sort((a, b) => a.label.localeCompare(b.label));
-
+
// Remove "Note" prefix from Note HTML translator
let htmlTranslator = translators.find(
x => x.translatorID == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML
@@ -118,7 +98,7 @@ Zotero_Preferences.Export = {
if (htmlTranslator) {
htmlTranslator.label = 'HTML';
}
-
+
// Make sure virtual "Markdown + Rich Text" translator doesn't actually exist
translators = translators.filter(
x => x.translatorID != Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT
@@ -127,186 +107,55 @@ Zotero_Preferences.Export = {
let markdownTranslatorIdx = translators.findIndex(
x => x.translatorID == Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN
);
- // Make sure we actually have both translators
if (markdownTranslatorIdx != -1 && htmlTranslator) {
- // Exclude standalone Note Markdown translator
translators.splice(markdownTranslatorIdx, 1);
- // Add virtual "Markdown + Rich Text" translator to the top
translators.unshift({
translatorID: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT,
label: 'Markdown + ' + Zotero.getString('general.richText'),
- configOptions: {
- noteTranslator: true
- }
+ configOptions: { noteTranslator: true }
});
}
-
+
translators.forEach(function (translator) {
- // Allow only note export translators
if (!translator.configOptions || !translator.configOptions.noteTranslator) {
return;
}
-
- var value = { mode: 'export', id: translator.translatorID };
- if (translator.translatorID == format.id) {
- value = format;
- }
- else if (translator.translatorID == Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) {
- value = {
- mode: 'export',
- id: translator.translatorID,
- markdownOptions: {
- includeAppLinks: true
- },
- htmlOptions: {
- includeAppLinks: false
- }
- };
- if (format.id == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML && format.options) {
- value.htmlOptions = format.options;
- }
- }
- else if (translator.translatorID == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) {
- value = {
- mode: 'export',
- id: translator.translatorID,
- options: {
- includeAppLinks: false
- }
- };
- if (format.id == Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT && format.htmlOptions) {
- value.options = format.htmlOptions;
- }
- }
-
- value = JSON.stringify(value);
var itemNode = document.createXULElement('menuitem');
- itemNode.setAttribute('value', value);
+ itemNode.setAttribute('value', translator.translatorID);
itemNode.setAttribute('label', translator.label);
popup.appendChild(itemNode);
-
- if (format.mode == 'export' && format.id == translator.translatorID) {
- menulist.selectedItem = itemNode;
- }
});
-
- menulist.click();
- this.updateNoteQuickCopyUI();
},
- updateNoteQuickCopyUI: () => {
- var format = document.getElementById('zotero-noteQuickCopy-menu').value;
- format = JSON.parse(format);
- var markdownOptions = document.getElementById('noteQuickCopy-markdown-options');
- var htmlOptions = document.getElementById('noteQuickCopy-html-options');
- var markdownOptionsLabel = document.querySelector('#noteQuickCopy-markdown-options label');
- var htmlOptionsLabel = document.querySelector('#noteQuickCopy-html-options label');
- var markdownIncludeAppLinks = document.getElementById("noteQuickCopy-markdown-includeAppLinks");
- var htmlIncludeAppLinks = document.getElementById("noteQuickCopy-html-includeAppLinks");
-
- markdownOptionsLabel.value = Zotero.Utilities.Internal.stringWithColon("Markdown");
- htmlOptionsLabel.value = Zotero.Utilities.Internal.stringWithColon(
- Zotero.getString('zotero.preferences.export.quickCopy.note.htmlOptions.label')
- );
- markdownIncludeAppLinks.label = Zotero.getString('exportOptions.includeAppLinks', Zotero.appName);
- htmlIncludeAppLinks.label = Zotero.getString('exportOptions.includeAppLinks', Zotero.appName);
-
- if (format.id == Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) {
- markdownOptions.hidden = false;
- htmlOptions.hidden = false;
- markdownIncludeAppLinks.checked = format.markdownOptions && format.markdownOptions.includeAppLinks;
- htmlIncludeAppLinks.checked = format.htmlOptions && format.htmlOptions.includeAppLinks;
- }
- else if (format.id == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) {
- markdownOptions.hidden = true;
- htmlOptions.hidden = false;
- htmlIncludeAppLinks.checked = format.options && format.options.includeAppLinks;
- }
- else {
- markdownOptions.hidden = true;
- htmlOptions.hidden = true;
- }
- },
-
- onUpdateNoteExportOptions() {
- var menulist = document.getElementById("zotero-noteQuickCopy-menu");
- var markdownIncludeAppLinks = document.getElementById("noteQuickCopy-markdown-includeAppLinks");
- var htmlIncludeAppLinks = document.getElementById("noteQuickCopy-html-includeAppLinks");
-
- for (let i = 0; i < menulist.itemCount; i++) {
- let item = menulist.getItemAtIndex(i);
- let format = JSON.parse(item.getAttribute('value'));
- if (format.id == Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) {
- if (!format.markdownOptions) {
- format.markdownOptions = {};
- }
- if (!format.htmlOptions) {
- format.htmlOptions = {};
- }
- format.markdownOptions.includeAppLinks = markdownIncludeAppLinks.checked;
- format.htmlOptions.includeAppLinks = htmlIncludeAppLinks.checked;
- }
- else if (format.id == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) {
- if (!format.options) {
- format.options = {};
- }
- format.options.includeAppLinks = htmlIncludeAppLinks.checked;
- }
- else {
- continue;
- }
- item.value = JSON.stringify(format);
- }
- // After updating item's value we have to wait before dispatching event.
- // menulist.value does not reflect changes immediately item.value is updated.
- setTimeout(() => menulist.dispatchEvent(new Event("change", { bubbles: true })), 50);
- },
-
-
/*
- * Builds a Quick Copy drop-down
+ * Builds the bibliography Quick Copy drop-down. Items carry the styleID;
+ * selection is applied by updateQuickCopyUI().
*/
- buildQuickCopyFormatDropDown: function (menulist, contentType, format, translators) {
- if (!format) {
- format = menulist.value;
- }
-
- format = Zotero.QuickCopy.unserializeSetting(format);
-
- menulist.selectedItem = null;
+ buildBibliographyQuickCopyDropDown: function (menulist) {
menulist.removeAllItems();
-
var popup = document.createXULElement('menupopup');
menulist.appendChild(popup);
-
- var itemNode = document.createXULElement("menuitem");
- itemNode.setAttribute("label", Zotero.getString('zotero.preferences.export.quickCopy.citationStyles'));
- itemNode.setAttribute("disabled", true);
- popup.appendChild(itemNode);
-
- // add styles to list
- var styles = Zotero.Styles.getVisible();
- styles.forEach(function (style) {
- var val = 'bibliography' + (contentType == 'html' ? '/html' : '') + '=' + style.styleID;
+
+ Zotero.Styles.getVisible().forEach(function (style) {
var itemNode = document.createXULElement("menuitem");
- itemNode.setAttribute("value", val);
+ itemNode.setAttribute("value", style.styleID);
itemNode.setAttribute("label", style.title);
- itemNode.setAttribute("oncommand", 'Zotero_Preferences.Export.updateQuickCopyUI()');
popup.appendChild(itemNode);
-
- if (format.mode == 'bibliography' && format.id == style.styleID) {
- menulist.selectedItem = itemNode;
- }
});
-
- var itemNode = document.createXULElement("menuitem");
- itemNode.setAttribute("label", Zotero.getString('zotero.preferences.export.quickCopy.exportFormats'));
- itemNode.setAttribute("disabled", true);
- popup.appendChild(itemNode);
-
- // add export formats to list
- translators.sort((a, b) => a.label.localeCompare(b.label))
+ },
+
+
+ /*
+ * Builds the export Quick Copy drop-down. Items carry the translatorID;
+ * selection is applied by updateQuickCopyUI().
+ */
+ buildExportQuickCopyDropDown: function (menulist, translators) {
+ menulist.removeAllItems();
+ var popup = document.createXULElement('menupopup');
+ menulist.appendChild(popup);
+
+ translators.sort((a, b) => a.label.localeCompare(b.label));
translators.forEach(function (translator) {
// Skip RDF formats
switch (translator.translatorID) {
@@ -314,46 +163,147 @@ Zotero_Preferences.Export = {
case '14763d24-8ba0-45df-8f52-b8d1108e7ac9':
return;
}
- var val = 'export=' + translator.translatorID;
var itemNode = document.createXULElement("menuitem");
- itemNode.setAttribute("value", val);
+ itemNode.setAttribute("value", translator.translatorID);
itemNode.setAttribute("label", translator.label);
- itemNode.setAttribute("oncommand", 'Zotero_Preferences.Export.updateQuickCopyUI()');
popup.appendChild(itemNode);
-
- if (format.mode == 'export' && format.id == translator.translatorID) {
- menulist.selectedItem = itemNode;
- }
});
-
- menulist.click();
},
-
-
- onCopyAsHTMLChange: async function (checked) {
- var menulist = document.getElementById('zotero-quickCopy-menu');
- var translators = await this.getQuickCopyTranslators();
- this.buildQuickCopyFormatDropDown(menulist, checked ? 'html' : '', null, translators);
+
+
+ /*
+ * Single writer: read the current state of every Quick Copy control and
+ * persist it across the bibliography, export, note-format, drag-preference,
+ * and locale prefs. Then call updateQuickCopyUI to refresh anything that
+ * cascades off the new state (locale list filtered by style, note option
+ * visibility for the new translator, etc).
+ */
+ updateQuickCopyPrefs: function () {
+ var bibMenu = document.getElementById('zotero-quickCopy-bibliography-menu');
+ var asHTML = document.getElementById('zotero-quickCopy-copyAsHTML').checked;
+ Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({
+ mode: 'bibliography',
+ id: bibMenu.value || '',
+ contentType: asHTML ? 'html' : '',
+ locale: document.getElementById('zotero-quickCopy-locale-menu').value || ''
+ }));
+
+ var exportMenu = document.getElementById('zotero-quickCopy-export-menu');
+ Zotero.Prefs.set('export.quickCopy.exportSetting', JSON.stringify({
+ mode: 'export',
+ id: exportMenu.value || ''
+ }));
+
+ var noteMenu = document.getElementById('zotero-noteQuickCopy-menu');
+ var noteId = noteMenu.value || '';
+ var notePref = { mode: 'export', id: noteId };
+ var markdownLinks = document.getElementById('noteQuickCopy-markdown-includeAppLinks').checked;
+ var htmlLinks = document.getElementById('noteQuickCopy-html-includeAppLinks').checked;
+ if (noteId == Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) {
+ notePref.markdownOptions = { includeAppLinks: markdownLinks };
+ notePref.htmlOptions = { includeAppLinks: htmlLinks };
+ }
+ else if (noteId == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) {
+ notePref.options = { includeAppLinks: htmlLinks };
+ }
+ Zotero.Prefs.set('export.noteQuickCopy.setting', JSON.stringify(notePref));
+
+ Zotero.Prefs.set(
+ 'export.quickCopy.preferredFormatOnDrag',
+ document.getElementById('zotero-quickCopy-preferredFormatOnDrag').value || 'bibliography'
+ );
+
+ this.updateQuickCopyUI();
},
-
-
+
+
+ /*
+ * Single reader: pull every Quick Copy pref and apply it to the matching
+ * control. Programmatic property assignments here do not fire `command`,
+ * so this does not loop back into updateQuickCopyPrefs.
+ */
updateQuickCopyUI: function () {
- var format = document.getElementById('zotero-quickCopy-menu').value;
-
- var mode, contentType;
-
- [mode, format] = format.split('=');
- [mode, contentType] = mode.split('/');
-
- var checkbox = document.getElementById('zotero-quickCopy-copyAsHTML');
- checkbox.checked = contentType == 'html';
- checkbox.disabled = mode != 'bibliography';
-
+ var bibPref = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.bibliographySetting')
+ );
+ this._selectMenuItemByValue(
+ document.getElementById('zotero-quickCopy-bibliography-menu'),
+ bibPref.id
+ );
+
+ document.getElementById('zotero-quickCopy-copyAsHTML').checked
+ = bibPref.contentType == 'html';
+
Zotero.Styles.updateLocaleList(
document.getElementById('zotero-quickCopy-locale-menu'),
- mode == 'bibliography' ? Zotero.Styles.get(format) : null,
- this._lastSelectedLocale
+ bibPref.id ? Zotero.Styles.get(bibPref.id) : null,
+ bibPref.locale
);
+
+ var exportPref = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.exportSetting')
+ );
+ this._selectMenuItemByValue(
+ document.getElementById('zotero-quickCopy-export-menu'),
+ exportPref.id
+ );
+
+ var notePref = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.noteQuickCopy.setting')
+ );
+ this._selectMenuItemByValue(
+ document.getElementById('zotero-noteQuickCopy-menu'),
+ notePref.id
+ );
+ this._updateNoteOptionUI(notePref);
+
+ document.getElementById('zotero-quickCopy-preferredFormatOnDrag').value
+ = Zotero.Prefs.get('export.quickCopy.preferredFormatOnDrag') || 'bibliography';
+ },
+
+
+ _selectMenuItemByValue: function (menulist, value) {
+ for (let i = 0; i < menulist.itemCount; i++) {
+ let item = menulist.getItemAtIndex(i);
+ if (item.value == value) {
+ menulist.selectedItem = item;
+ return;
+ }
+ }
+ menulist.selectedItem = null;
+ },
+
+
+ _updateNoteOptionUI: function (notePref) {
+ var markdownGroup = document.getElementById('noteQuickCopy-markdown-options');
+ var htmlGroup = document.getElementById('noteQuickCopy-html-options');
+ var markdownLabel = document.querySelector('#noteQuickCopy-markdown-options label');
+ var htmlLabel = document.querySelector('#noteQuickCopy-html-options label');
+ var markdownCheckbox = document.getElementById('noteQuickCopy-markdown-includeAppLinks');
+ var htmlCheckbox = document.getElementById('noteQuickCopy-html-includeAppLinks');
+
+ markdownLabel.value = Zotero.Utilities.Internal.stringWithColon('Markdown');
+ htmlLabel.value = Zotero.Utilities.Internal.stringWithColon(
+ Zotero.getString('zotero.preferences.export.quickCopy.note.htmlOptions.label')
+ );
+ markdownCheckbox.label = Zotero.getString('exportOptions.includeAppLinks', Zotero.appName);
+ htmlCheckbox.label = Zotero.getString('exportOptions.includeAppLinks', Zotero.appName);
+
+ if (notePref.id == Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT) {
+ markdownGroup.hidden = false;
+ htmlGroup.hidden = false;
+ markdownCheckbox.checked = !!(notePref.markdownOptions && notePref.markdownOptions.includeAppLinks);
+ htmlCheckbox.checked = !!(notePref.htmlOptions && notePref.htmlOptions.includeAppLinks);
+ }
+ else if (notePref.id == Zotero.Translators.TRANSLATOR_ID_NOTE_HTML) {
+ markdownGroup.hidden = true;
+ htmlGroup.hidden = false;
+ htmlCheckbox.checked = !!(notePref.options && notePref.options.includeAppLinks);
+ }
+ else {
+ markdownGroup.hidden = true;
+ htmlGroup.hidden = true;
+ }
},
/**
@@ -389,65 +339,81 @@ Zotero_Preferences.Export = {
if (editExisting) {
index = this._tree.selection.focused;
}
- var formattedName = document.getElementById('zotero-quickCopy-menu').label;
- var locale = this._lastSelectedLocale;
- var asHTML = document.getElementById('zotero-quickCopy-copyAsHTML').checked;
-
+
+ var domain = '';
+ var existingSiteSetting = null;
+
if (index !== undefined && index > -1 && index < this._rows.length) {
- var row = this._rows[index];
- var domain = row.domain;
- formattedName = row.format;
- locale = row.locale;
- asHTML = row.copyAsHTML;
+ let row = this._rows[index];
+ domain = row.domain;
+ existingSiteSetting = row.siteSetting;
}
-
- var format = await Zotero.QuickCopy.getSettingFromFormattedName(formattedName);
- if (asHTML) {
- format = format.replace('bibliography=', 'bibliography/html=');
- }
-
- var styles = Zotero.Styles.getVisible();
+
var translation = new Zotero.Translate("export");
var translators = await translation.getTranslators();
-
- var io = { domain, format, locale, asHTML, ok: false, styles, translators };
+
+ var io = {
+ domain,
+ siteSetting: existingSiteSetting,
+ translators,
+ ok: false
+ };
window.openDialog('chrome://zotero/content/preferences/quickCopySiteEditor.xhtml',
"zotero-preferences-quickCopySiteEditor", "chrome,modal,centerscreen", io);
-
- if (!io.ok || !io.domain) {
+
+ if (!io.ok || !io.domain || !io.siteSetting) {
return;
}
-
+ // Defensive: at least one of bibliography/export must be present
+ if (!io.siteSetting.bibliography && !io.siteSetting.export) {
+ return;
+ }
+
if (domain && domain != io.domain) {
await Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='quickCopySite' AND key=?", [domain]);
}
-
- var quickCopysetting = Zotero.QuickCopy.unserializeSetting(io.format);
- quickCopysetting.locale = io.locale;
-
- await Zotero.DB.queryAsync("REPLACE INTO settings VALUES ('quickCopySite', ?, ?)", [io.domain, JSON.stringify(quickCopysetting)]);
-
+
+ await Zotero.DB.queryAsync("REPLACE INTO settings VALUES ('quickCopySite', ?, ?)",
+ [io.domain, JSON.stringify(io.siteSetting)]);
+
await Zotero.QuickCopy.loadSiteSettings();
-
+
await this.refreshQuickCopySiteList();
},
-
-
+
+
refreshQuickCopySiteList: async function () {
var sql = "SELECT key AS domainPath, value AS format FROM settings "
+ "WHERE setting='quickCopySite' ORDER BY domainPath COLLATE NOCASE";
var siteData = await Zotero.DB.queryAsync(sql);
-
+
this._rows = [];
for (let row of siteData) {
- var formattedName = await Zotero.QuickCopy.getFormattedNameFromSetting(row.format);
- var format = Zotero.QuickCopy.unserializeSetting(row.format);
+ let site = Zotero.QuickCopy.parseSiteFormat(row.format);
+ let bibName = '';
+ let exportName = '';
+ if (site.bibliography && site.bibliography.id) {
+ bibName = await Zotero.QuickCopy.getFormattedNameFromSetting(site.bibliography);
+ }
+ if (site.export && site.export.id) {
+ exportName = await Zotero.QuickCopy.getFormattedNameFromSetting(site.export);
+ }
+ let dragLabel = '';
+ if (site.drag === 'bibliography') {
+ dragLabel = Zotero.getString('preferences-quickCopy-preferredFormatOnDrag-bibliography');
+ }
+ else if (site.drag === 'export') {
+ dragLabel = Zotero.getString('preferences-quickCopy-preferredFormatOnDrag-export');
+ }
this._rows.push({
domain: row.domainPath,
- format: formattedName,
- locale: format.locale,
- copyAsHTML: format.contentType == 'html',
+ exportFormat: exportName,
+ bibliographyFormat: bibName,
+ locale: site.bibliography && site.bibliography.locale || '',
+ copyAsHTML: !!(site.bibliography && site.bibliography.contentType === 'html'),
+ useOnDrag: dragLabel,
+ siteSetting: site,
});
}
@@ -459,14 +425,20 @@ Zotero_Preferences.Export = {
flex: 2
},
{
- dataKey: "format",
- label: "zotero.preferences.quickCopy.siteEditor.format",
- flex: 4
+ dataKey: "exportFormat",
+ label: "preferences-quickCopy-siteEditor-export-format",
+ flex: 3
+ },
+ {
+ dataKey: "bibliographyFormat",
+ label: "preferences-quickCopy-siteEditor-bibliography-format",
+ flex: 3
},
{
dataKey: "locale",
label: "zotero.preferences.quickCopy.siteEditor.locale",
- flex: 1
+ fixedWidth: true,
+ width: 85,
},
{
dataKey: "copyAsHTML",
@@ -474,6 +446,12 @@ Zotero_Preferences.Export = {
type: 'checkbox',
fixedWidth: true,
width: 55,
+ },
+ {
+ dataKey: "useOnDrag",
+ label: "preferences-quickCopy-siteEditor-use-on-drag",
+ fixedWidth: true,
+ width: 85,
}
];
var handleKeyDown = (event) => {
diff --git a/chrome/content/zotero/preferences/preferences_export.xhtml b/chrome/content/zotero/preferences/preferences_export.xhtml
index e41d975e3e..0a803ede58 100644
--- a/chrome/content/zotero/preferences/preferences_export.xhtml
+++ b/chrome/content/zotero/preferences/preferences_export.xhtml
@@ -35,36 +35,50 @@
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
+
+
+
+
-
+
-
+
diff --git a/chrome/content/zotero/preferences/quickCopySiteEditor.xhtml b/chrome/content/zotero/preferences/quickCopySiteEditor.xhtml
index 7eabe3130d..91d6ca2b41 100644
--- a/chrome/content/zotero/preferences/quickCopySiteEditor.xhtml
+++ b/chrome/content/zotero/preferences/quickCopySiteEditor.xhtml
@@ -38,96 +38,157 @@
-
diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js
index a7723e998b..6f245e423d 100644
--- a/chrome/content/zotero/standalone/standalone.js
+++ b/chrome/content/zotero/standalone/standalone.js
@@ -99,6 +99,8 @@ const ZoteroStandalone = new function () {
.setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemCitationsToClipboard'));
document.getElementById('key_copyBibliography')
.setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemsToClipboard'));
+ document.getElementById('key_copyExport')
+ .setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemsToClipboardAsExport'));
document.getElementById('key_showTabsMenu')
.setAttribute('key', Zotero.Keys.getKeyForCommand('showTabsMenu'));
// Force menu to update with shortcut key at startup -- as of fx128, this is necessary
@@ -348,32 +350,29 @@ const ZoteroStandalone = new function () {
win.ZoteroPane.updateQuickCopyCommands(selected);
}
- var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
var exportingNotes = selected.every(item => item.isNote() || item.isAttachment());
var exportingAnnotations = selected.every(item => item.isAnnotation());
- if (exportingNotes || exportingAnnotations) {
- format = Zotero.QuickCopy.getNoteFormat();
- }
- format = Zotero.QuickCopy.unserializeSetting(format);
-
+
var copyCitation = document.getElementById('menu_copyCitation');
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;
+
+ copyCitation.hidden = !selected.length || exportingNotes || exportingAnnotations;
+ copyBibliography.hidden = !selected.length || exportingNotes || exportingAnnotations;
+ copyExport.hidden = !selected.length || exportingNotes || exportingAnnotations;
+ copyNote.hidden = !selected.length || !exportingNotes;
+ copyAnnotation.hidden = !selected.length || !exportingAnnotations;
document.l10n.setAttributes(copyAnnotation, "menu-edit-copy-annotation", { count: selected.length });
-
- if (format.mode == 'export') {
+
+ // Label the export menu with the selected translator name
+ var exportFormat = Zotero.QuickCopy.getFormat('export');
+ if (exportFormat.id) {
try {
- let obj = Zotero.Translators.get(format.id);
- if (obj) {
- copyExport.label = Zotero.getString('quickCopy.copyAs', obj.label);
+ let translator = Zotero.Translators.get(exportFormat.id);
+ if (translator) {
+ copyExport.label = Zotero.getString('quickCopy.copyAs', translator.label);
}
else {
copyExport.hidden = true;
diff --git a/chrome/content/zotero/xpcom/prefs.js b/chrome/content/zotero/xpcom/prefs.js
index bdbd864331..9a2540a1c3 100644
--- a/chrome/content/zotero/xpcom/prefs.js
+++ b/chrome/content/zotero/xpcom/prefs.js
@@ -46,7 +46,7 @@ Zotero.Prefs = new function () {
// Process pref version updates
var fromVersion = this.get('prefVersion');
- var toVersion = 22;
+ var toVersion = 23;
if (!fromVersion) {
this.set('prefVersion', toVersion);
}
@@ -235,6 +235,32 @@ Zotero.Prefs = new function () {
this.clear('downloadPDFViaBrowser.downloadTimeout');
}
break;
+
+ // Migrate the combined export.quickCopy.setting pref to the
+ // per-mode bibliographySetting / exportSetting prefs.
+ case 23:
+ if (this.prefHasUserValue('export.quickCopy.setting')) {
+ let setting = this.get('export.quickCopy.setting');
+ let parsed = Zotero.QuickCopy.unserializeSetting(setting);
+ if (parsed.mode === 'bibliography') {
+ this.set('export.quickCopy.bibliographySetting', JSON.stringify(parsed));
+ this.set('export.quickCopy.preferredFormatOnDrag', 'bibliography');
+ }
+ else if (parsed.mode === 'export') {
+ this.set('export.quickCopy.exportSetting', JSON.stringify(parsed));
+ this.set('export.quickCopy.preferredFormatOnDrag', 'export');
+ }
+ // Move the locale pref into the bibliography pref JSON
+ let locale = this.get('export.quickCopy.locale');
+ if (locale) {
+ let bibFormat = Zotero.QuickCopy.unserializeSetting(this.get('export.quickCopy.bibliographySetting'));
+ bibFormat.locale = locale;
+ this.set('export.quickCopy.bibliographySetting', JSON.stringify(bibFormat));
+ }
+ this.clear('export.quickCopy.setting');
+ this.clear('export.quickCopy.locale');
+ }
+ break;
}
}
this.set('prefVersion', toVersion);
diff --git a/chrome/content/zotero/xpcom/quickCopy.js b/chrome/content/zotero/xpcom/quickCopy.js
index c4a17d1e0b..816844feb9 100644
--- a/chrome/content/zotero/xpcom/quickCopy.js
+++ b/chrome/content/zotero/xpcom/quickCopy.js
@@ -45,7 +45,10 @@ Zotero.QuickCopy = new function () {
// Make sure export translator code is loaded whenever the output format changes
this._prefObserverIDs = [
Zotero.Prefs.registerObserver(
- "export.quickCopy.setting", _loadOutputFormat
+ "export.quickCopy.bibliographySetting", _loadOutputFormat
+ ),
+ Zotero.Prefs.registerObserver(
+ "export.quickCopy.exportSetting", _loadOutputFormat
),
Zotero.Prefs.registerObserver(
"export.noteQuickCopy.setting", _loadNoteOutputFormat
@@ -86,8 +89,16 @@ Zotero.QuickCopy = new function () {
format: row.format
};
});
- for (let row of rows) {
- await _preloadFormat(row.format);
+ // Preload every translator/style referenced by a site-specific entry
+ // so drag-drop has them available synchronously
+ for (let row of _siteSettings) {
+ let site = this.parseSiteFormat(row.format);
+ if (site.bibliography) {
+ await _preloadFormat(site.bibliography);
+ }
+ if (site.export) {
+ await _preloadFormat(site.export);
+ }
}
};
@@ -96,7 +107,63 @@ Zotero.QuickCopy = new function () {
return _siteSettings && _siteSettings.length > 0;
};
-
+
+ /**
+ * Parse a site-specific Quick Copy setting from the DB into a normalized
+ * shape. Supports both the legacy single-mode format and the new shape;
+ * always returns:
+ *
+ * {
+ * bibliography?: { mode: 'bibliography', id, contentType, locale },
+ * export?: { mode: 'export', id },
+ * drag?: 'bibliography' | 'export'
+ * }
+ * @param {String|Object} raw - DB row.format value
+ * @return {Object} - Normalized site setting
+ */
+ this.parseSiteFormat = function (raw) {
+ var parsed = this.unserializeSetting(raw);
+ if (!parsed || typeof parsed !== 'object') {
+ return {};
+ }
+ // Legacy single-mode shape (top-level `mode` field)
+ if (parsed.mode === 'bibliography') {
+ return {
+ bibliography: {
+ mode: 'bibliography',
+ id: parsed.id,
+ contentType: parsed.contentType || '',
+ locale: parsed.locale || ''
+ },
+ drag: 'bibliography'
+ };
+ }
+ if (parsed.mode === 'export') {
+ return {
+ export: { mode: 'export', id: parsed.id },
+ drag: 'export'
+ };
+ }
+ // Ensure each sub-object is a complete single-mode setting
+ var out = {};
+ if (parsed.bibliography) {
+ out.bibliography = {
+ mode: 'bibliography',
+ id: parsed.bibliography.id,
+ contentType: parsed.bibliography.contentType || '',
+ locale: parsed.bibliography.locale || ''
+ };
+ }
+ if (parsed.export) {
+ out.export = { mode: 'export', id: parsed.export.id };
+ }
+ if (parsed.drag) {
+ out.drag = parsed.drag;
+ }
+ return out;
+ };
+
+
/*
* Return Quick Copy setting object from string, stringified object, or object
*
@@ -111,26 +178,37 @@ Zotero.QuickCopy = new function () {
*/
this.unserializeSetting = function (setting) {
var settingObject = {};
-
+
if (typeof setting === 'string') {
try {
// First test if string input is a stringified object
settingObject = JSON.parse(setting);
} catch (e) {
- // Try parsing as formatted string
+ // Try parsing as formatted string. contentType and locale are
+ // only meaningful for bibliography mode.
var parsedSetting = setting.match(/(bibliography|export)(?:\/([^=]+))?=(.+)$/);
if (parsedSetting) {
settingObject.mode = parsedSetting[1];
- settingObject.contentType = parsedSetting[2] || '';
+ if (parsedSetting[1] === 'bibliography') {
+ settingObject.contentType = parsedSetting[2] || '';
+ }
settingObject.id = parsedSetting[3];
- settingObject.locale = '';
+ if (parsedSetting[1] === 'bibliography') {
+ settingObject.locale = '';
+ }
}
}
} else {
// Return input if not a string; it might already be an object
return setting;
}
-
+
+ // Ensure bibliography mode always has contentType and locale fields
+ if (settingObject.mode === 'bibliography') {
+ if (settingObject.contentType === undefined) settingObject.contentType = '';
+ if (settingObject.locale === undefined) settingObject.locale = '';
+ }
+
return settingObject;
};
@@ -156,21 +234,26 @@ Zotero.QuickCopy = new function () {
}
return '';
};
-
+
this.getNoteFormat = function () {
- var pref = Zotero.Prefs.get('export.noteQuickCopy.setting');
- pref = JSON.stringify(this.unserializeSetting(pref));
- return pref;
+ return this.unserializeSetting(Zotero.Prefs.get('export.noteQuickCopy.setting'));
};
-
- this.getFormatFromURL = function (url) {
- var quickCopyPref = Zotero.Prefs.get("export.quickCopy.setting");
- quickCopyPref = JSON.stringify(this.unserializeSetting(quickCopyPref));
-
- if (!url) {
- return quickCopyPref;
- }
-
+
+
+ this.getLocale = function () {
+ return this.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.bibliographySetting')
+ ).locale;
+ };
+
+
+ /**
+ * Find the best-matching site setting row for the given URL (longest domain,
+ * then longest path). Returns the raw DB row.format string, or null.
+ */
+ var _findSiteFormatForURL = function (url) {
+ if (!url) return null;
+
var nsIURI;
try {
nsIURI = Services.io.newURI(url, null, null);
@@ -180,56 +263,74 @@ Zotero.QuickCopy = new function () {
var urlPath = nsIURI.pathQueryRef;
}
catch (e) {}
-
+
// Skip non-HTTP URLs
if (!nsIURI || !/^https?$/.test(nsIURI.scheme)) {
- return quickCopyPref;
+ return null;
}
-
+
if (!_siteSettings) {
Zotero.debug("Quick Copy site settings not loaded", 2);
- return quickCopyPref;
+ return null;
}
-
+
var matches = [];
- for (let i=0; i<_siteSettings.length; i++) {
+ for (let i = 0; i < _siteSettings.length; i++) {
let row = _siteSettings[i];
- let domain = row.domainPath.split('/',1)[0];
+ let domain = row.domainPath.split('/', 1)[0];
let path = row.domainPath.substr(domain.length) || '/';
if (urlHostPort.endsWith(domain) && urlPath.startsWith(path)) {
matches.push({
- format: JSON.stringify(this.unserializeSetting(row.format)),
+ format: row.format,
domainLength: domain.length,
pathLength: path.length
});
}
}
-
+
+ if (!matches.length) return null;
+
// Give priority to longer domains, then longer paths
- var sort = function (a, b) {
- if (a.domainLength > b.domainLength) {
- return -1;
- }
- else if (a.domainLength < b.domainLength) {
- return 1;
- }
-
- if (a.pathLength > b.pathLength) {
- return -1;
- }
- else if (a.pathLength < b.pathLength) {
- return 1;
- }
-
+ matches.sort(function (a, b) {
+ if (a.domainLength > b.domainLength) return -1;
+ if (a.domainLength < b.domainLength) return 1;
+ if (a.pathLength > b.pathLength) return -1;
+ if (a.pathLength < b.pathLength) return 1;
return -1;
- };
-
- if (matches.length) {
- matches.sort(sort);
- return matches[0].format;
- } else {
- return quickCopyPref;
+ });
+ return matches[0].format;
+ };
+
+
+ /**
+ * Get the Quick Copy format for the currently active URL if any.
+ *
+ * @param {String} [mode] - Optional 'bibliography' or 'export' - omit for drag-drop
+ * @return {Object} - `{mode, id, contentType, locale}` for bibliography; `{mode, id}` for export
+ */
+ this.getFormat = function (mode) {
+ var siteRaw = _findSiteFormatForURL(this.lastActiveURL);
+ var site = siteRaw ? this.parseSiteFormat(siteRaw) : null;
+
+ // If no mode was requested (drag-drop), pick one from the site's
+ // `drag` pref or fall back to the user's preferredFormatOnDrag pref
+ if (!mode) {
+ if (site?.drag) {
+ mode = site.drag;
+ }
+ else {
+ mode = Zotero.Prefs.get('export.quickCopy.preferredFormatOnDrag');
+ }
}
+
+ // Site override for that mode wins, else fall back to global
+ if (site && site[mode] && site[mode].id) {
+ return site[mode];
+ }
+ var globalFormat = mode === 'export'
+ ? Zotero.Prefs.get('export.quickCopy.exportSetting')
+ : Zotero.Prefs.get('export.quickCopy.bibliographySetting');
+ return this.unserializeSetting(globalFormat);
};
@@ -273,8 +374,8 @@ Zotero.QuickCopy = new function () {
}
else if (format.mode == 'bibliography') {
items = items.filter(item => !item.isNote());
-
- var locale = _getLocale(format);
+
+ var locale = format.locale || this.getLocale();
// Copy citations if shift key pressed
if (modified) {
@@ -343,16 +444,222 @@ Zotero.QuickCopy = new function () {
tmpNote.setNote(html);
return tmpNote;
};
-
+
/**
- * If an export translator is the selected output format, load its code (which must be done
- * asynchronously) ahead of time, since drag-and-drop requires synchronous operation
+ * Reformat citation spans in note items using the QuickCopy CSL style
+ *
+ * Parses citation data from each in the note HTML,
+ * resolves item URIs, and reformats using the CSL engine. Returns new temporary
+ * note items with updated HTML -- the originals are not modified.
+ *
+ * @param {Zotero.Item[]} items - Note items to process
+ * @return {Zotero.Item[]} - New note items with CSL-formatted citations
+ */
+ this.reformatNoteCitations = function (items) {
+ if (!items.every(item => item.isNote())) {
+ return items;
+ }
+
+ // 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');
+ if (format.mode !== 'bibliography' || !format.id) {
+ return items;
+ }
+
+ let style = Zotero.Styles.get(format.id);
+ if (!style) {
+ return items;
+ }
+
+ let locale = format.locale || this.getLocale();
+ let cslEngine;
+ try {
+ cslEngine = style.getCiteProc(locale, 'html', { cache: true });
+ }
+ catch (e) {
+ Zotero.logError('Failed to initialize CSL engine for note citation reformatting: ' + e);
+ return items;
+ }
+
+ try {
+ // First pass: collect all unique item IDs across all notes,
+ // and build a fallback map of embedded CSL JSON for deleted items
+ let allItemIDs = new Set();
+ let embeddedCSLByFakeID = {};
+ let parsedNotes = [];
+
+ for (let item of items) {
+ if (!item.isNote()) {
+ parsedNotes.push(null);
+ continue;
+ }
+
+ let noteHTML = item.getNote();
+ if (!noteHTML) {
+ parsedNotes.push(null);
+ continue;
+ }
+
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(noteHTML, 'text/html');
+ let citationSpans = doc.querySelectorAll('span.citation[data-citation]');
+
+ if (!citationSpans.length) {
+ parsedNotes.push(null);
+ continue;
+ }
+
+ // Parse embedded citation item data from the note wrapper
+ // for fallback when items have been deleted
+ let embeddedItemDataByURI = {};
+ let containerNode = doc.querySelector('div[data-citation-items]');
+ if (containerNode) {
+ try {
+ let storedItems = JSON.parse(decodeURIComponent(
+ containerNode.getAttribute('data-citation-items')
+ ));
+ if (Array.isArray(storedItems)) {
+ for (let si of storedItems) {
+ if (si.uris && si.itemData) {
+ for (let uri of si.uris) {
+ embeddedItemDataByURI[uri] = si.itemData;
+ }
+ }
+ }
+ }
+ }
+ catch (e) {
+ // Ignore parse errors
+ }
+ }
+
+ let spanData = [];
+ for (let span of citationSpans) {
+ let citation;
+ try {
+ citation = JSON.parse(decodeURIComponent(span.getAttribute('data-citation')));
+ }
+ catch (e) {
+ spanData.push(null);
+ continue;
+ }
+
+ if (!citation || !citation.citationItems || !citation.citationItems.length) {
+ spanData.push(null);
+ continue;
+ }
+
+ let cslItems = [];
+ for (let ci of citation.citationItems) {
+ if (!ci.uris || !ci.uris.length) continue;
+ let uri = ci.uris[0];
+ let itemID = Zotero.URI.getURIItemID(uri);
+
+ if (!itemID) {
+ // Item deleted -- use embedded CSL JSON if available
+ let embeddedData = embeddedItemDataByURI[uri];
+ if (!embeddedData) continue;
+
+ let fakeID = Zotero.Utilities.randomString();
+ let cslData = Zotero.Utilities.deepCopy(embeddedData);
+ cslData.id = fakeID;
+ embeddedCSLByFakeID[fakeID] = cslData;
+ itemID = fakeID;
+ }
+
+ allItemIDs.add(itemID);
+ let cslItem = { id: itemID };
+ if (ci.locator) {
+ cslItem.locator = ci.locator;
+ cslItem.label = ci.label || 'page';
+ }
+ if (ci.prefix) {
+ cslItem.prefix = ci.prefix;
+ }
+ if (ci.suffix) {
+ cslItem.suffix = ci.suffix;
+ }
+ cslItems.push(cslItem);
+ }
+
+ spanData.push(cslItems.length ? { span, cslItems } : null);
+ }
+
+ parsedNotes.push({ doc, spanData });
+ }
+
+ if (!allItemIDs.size) {
+ cslEngine.free();
+ return items;
+ }
+
+ // If we have embedded items for deleted citations, temporarily
+ // patch retrieveItem so citeproc can resolve fake IDs
+ let originalRetrieveItem;
+ if (Object.keys(embeddedCSLByFakeID).length) {
+ originalRetrieveItem = cslEngine.sys.retrieveItem;
+ cslEngine.sys.retrieveItem = function (id) {
+ if (embeddedCSLByFakeID[id]) {
+ return embeddedCSLByFakeID[id];
+ }
+ return originalRetrieveItem.call(this, id);
+ };
+ }
+
+ cslEngine.updateItems([...allItemIDs]);
+
+ // Second pass: format citations and build new note items
+ let result = [];
+ for (let i = 0; i < items.length; i++) {
+ let parsed = parsedNotes[i];
+ if (!parsed) {
+ result.push(items[i]);
+ continue;
+ }
+
+ for (let data of parsed.spanData) {
+ if (!data) continue;
+ let citation = {
+ citationItems: data.cslItems,
+ properties: {}
+ };
+ let formatted = cslEngine.previewCitationCluster(citation, [], [], 'html');
+ data.span.innerHTML = '(' + formatted + ')';
+ }
+
+ let tmpNote = new Zotero.Item('note');
+ tmpNote.libraryID = items[i].libraryID;
+ tmpNote.setNote(parsed.doc.body.innerHTML);
+ result.push(tmpNote);
+ }
+
+ // Restore original retrieveItem if patched
+ if (originalRetrieveItem) {
+ cslEngine.sys.retrieveItem = originalRetrieveItem;
+ }
+ cslEngine.free();
+ return result;
+ }
+ catch (e) {
+ Zotero.logError('Failed to reformat note citations: ' + e);
+ if (cslEngine) {
+ cslEngine.free();
+ }
+ return items;
+ }
+ };
+
+ /**
+ * Preload the global default bibliography and export translators/styles so
+ * drag-and-drop has them available synchronously. Site-specific formats
+ * are preloaded by loadSiteSettings().
*
* @return {Promise}
*/
var _loadOutputFormat = async function () {
- var format = Zotero.Prefs.get("export.quickCopy.setting");
- return _preloadFormat(format);
+ await _preloadFormat(Zotero.Prefs.get('export.quickCopy.bibliographySetting'));
+ await _preloadFormat(Zotero.Prefs.get('export.quickCopy.exportSetting'));
};
@@ -390,17 +697,13 @@ Zotero.QuickCopy = new function () {
}
else if (format.mode === 'bibliography') {
let style = Zotero.Styles.get(format.id);
- let locale = _getLocale(format);
+ let locale = format.locale || Zotero.QuickCopy.getLocale();
// Cache a single CiteProc instance (format-independent)
style.getCiteProc(locale, 'html', { cache: true });
}
};
- function _getLocale(format) {
- return format.locale || Zotero.Prefs.get('export.quickCopy.locale');
- }
-
var _loadFormattedNames = async function () {
var t = new Date;
Zotero.debug("Loading formatted names for Quick Copy");
diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js
index b4cecf3d70..9d592dbf99 100644
--- a/chrome/content/zotero/xpcom/reader.js
+++ b/chrome/content/zotero/xpcom/reader.js
@@ -412,9 +412,11 @@ class ReaderInstance {
}
// annotations are wrapped in a temp note for translation
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();
- Zotero.debug(`Copying/dragging (${annotations.length}) annotation(s) with ${format}`);
- format = Zotero.QuickCopy.unserializeSetting(format);
+ 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
diff --git a/chrome/content/zotero/xpcom/style.js b/chrome/content/zotero/xpcom/style.js
index 93c4cce44b..70e52c4fd3 100644
--- a/chrome/content/zotero/xpcom/style.js
+++ b/chrome/content/zotero/xpcom/style.js
@@ -87,7 +87,6 @@ Zotero.Styles = new function () {
var bibliographyLocale = Zotero.Prefs.get("export.bibliographyLocale");
if (bibliographyLocale) {
Zotero.Prefs.set("export.lastLocale", bibliographyLocale);
- Zotero.Prefs.set("export.quickCopy.locale", bibliographyLocale);
Zotero.Prefs.clear("export.bibliographyLocale");
}
diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js
index 8d2c19bc3c..174eb2f5fe 100644
--- a/chrome/content/zotero/xpcom/utilities_internal.js
+++ b/chrome/content/zotero/xpcom/utilities_internal.js
@@ -3402,21 +3402,20 @@ 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.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
+ let format = Zotero.QuickCopy.getFormat();
+
+ // If all items are annotations, wrap them in a note object for translation
+ if (items.every(item => item.isAnnotation())) {
+ items = [Zotero.QuickCopy.annotationsToNote(items)];
+ }
// 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);
}
- // 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);
+ Zotero.debug("Dragging with format " + JSON.stringify(format));
try {
if (format.mode == 'export') {
// If exporting with virtual "Markdown + Rich Text" translator, call Note Markdown
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index 35135497f2..fe1ac8da25 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -1365,6 +1365,7 @@ var ZoteroPane = new function () {
// this.itemSelected()
case 'copySelectedItemCitationsToClipboard':
case 'copySelectedItemsToClipboard':
+ case 'copySelectedItemsToClipboardAsExport':
return;
default:
@@ -2000,27 +2001,24 @@ var ZoteroPane = new function () {
* still caught in handleKeyPress so that we can show an alert about not having references selected.
*/
this.updateQuickCopyCommands = function (selectedItems) {
- let canCopy = false;
+ let canCopyBibliography = false;
+ let canCopyExport = false;
// If all items are notes/attachments and at least one note is not empty
if (selectedItems.every(item => item.isNote() || item.isAttachment())) {
if (selectedItems.some(item => item.note)) {
- canCopy = true;
+ canCopyBibliography = true;
+ canCopyExport = true;
}
}
else {
- let format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
- format = Zotero.QuickCopy.unserializeSetting(format);
- if (format.mode == 'bibliography') {
- canCopy = selectedItems.some(item => item.isRegularItem() || item.isAnnotation());
- }
- else {
- canCopy = true;
- }
+ canCopyBibliography = selectedItems.some(item => item.isRegularItem() || item.isAnnotation());
+ canCopyExport = selectedItems.length > 0;
}
-
- document.getElementById('cmd_zotero_copyCitation').setAttribute('disabled', !canCopy);
- document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', !canCopy);
- document.getElementById('cmd_zotero_copyAnnotation').setAttribute('disabled', !canCopy);
+
+ document.getElementById('cmd_zotero_copyCitation').setAttribute('disabled', !canCopyBibliography);
+ document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', !canCopyBibliography);
+ document.getElementById('cmd_zotero_copyExport').setAttribute('disabled', !canCopyExport);
+ document.getElementById('cmd_zotero_copyAnnotation').setAttribute('disabled', !canCopyBibliography);
};
@@ -2699,28 +2697,29 @@ var ZoteroPane = new function () {
}
- this.copySelectedItemsToClipboard = function (asCitations) {
+ 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);
-
+
if (!items.length) {
return;
}
-
- var format = Zotero.QuickCopy.getFormatFromURL(Zotero.QuickCopy.lastActiveURL);
- if (items.every(item => item.isNote() || item.isAttachment())) {
+
+ 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())) {
- format = Zotero.QuickCopy.getNoteFormat();
items = [Zotero.QuickCopy.annotationsToNote(items)];
}
- format = Zotero.QuickCopy.unserializeSetting(format);
-
+ if (items.every(item => item.isNote())) {
+ items = Zotero.QuickCopy.reformatNoteCitations(items);
+ }
+
// In bibliography mode, remove notes and attachments
if (format.mode == 'bibliography') {
items = items.filter(item => item.isRegularItem());
@@ -2740,10 +2739,8 @@ var ZoteroPane = new function () {
return;
}
- // determine locale preference
- var locale = format.locale ? format.locale : Zotero.Prefs.get('export.quickCopy.locale');
-
if (format.mode == 'bibliography') {
+ var locale = format.locale;
Zotero_File_Interface.copyItemsToClipboard(
items, format.id, locale, format.contentType == 'html', asCitations
);
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index 62de1383fd..111b3fcafc 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -104,10 +104,13 @@
+
+
'
+ + '(lastOne and lastTwo)
'
+ + '(lastTwo and lastThree)
'
+ + 'Done
'
+ + '';
+
+ noteItem = createUnsavedDataObject('item', {
+ itemType: 'note',
+ note: noteHTML
+ });
+ await noteItem.saveTx();
+
+ // Preload note translators
+ let markdownTranslator = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN);
+ markdownTranslator.cacheCode = true;
+ await Zotero.Translators.getCodeForTranslator(markdownTranslator);
+ let htmlTranslator = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_HTML);
+ htmlTranslator.cacheCode = true;
+ await Zotero.Translators.getCodeForTranslator(htmlTranslator);
+ });
+
+ afterEach(function () {
+ Zotero.Prefs.clear('export.quickCopy.bibliographySetting');
+ Zotero.Prefs.clear('export.noteQuickCopy.setting');
+ });
+
+ after(async function () {
+ await Zotero.Items.erase([noteItem.id, itemOne.id, itemTwo.id]);
+ });
+
+ function getClipboardText() {
+ 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.addDataFlavor('text/plain');
+ clipboardService.getData(transferable, Components.interfaces.nsIClipboard.kGlobalClipboard);
+ let str = {};
+ transferable.getTransferData('text/plain', str);
+ return str.value.QueryInterface(Components.interfaces.nsISupportsString).data;
+ }
+
+ it(`should copy CSL-formatted citations to clipboard`, async function () {
+ Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({
+ mode: 'bibliography', id: styleID, contentType: '', locale: ''
+ }));
+ Zotero.Prefs.set('export.noteQuickCopy.setting', JSON.stringify({
+ mode: 'export',
+ id: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT,
+ markdownOptions: { includeAppLinks: false },
+ htmlOptions: { includeAppLinks: false }
+ }));
+
+ await win.ZoteroPane.selectItem(noteItem.id);
+ win.ZoteroPane.copySelectedItemsToClipboard(false, 'bibliography');
+ await Zotero.Promise.delay();
+
+ var text = getClipboardText();
+ var expected = 'Some text\n\n'
+ + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n'
+ + '(firstTwo lastTwo and firstThree lastThree, Item Two (n.d.).)\n\n'
+ + 'Done';
+ assert.equal(text, expected);
+ });
+
+ it("should use embedded item data for deleted items when copying to clipboard", async function () {
+ var deletedItem = createUnsavedDataObject('item', {
+ itemType: 'book',
+ title: 'Deleted Item',
+ creators: [
+ { firstName: 'firstDeleted', lastName: 'lastDeleted', creatorType: 'author' }
+ ]
+ });
+ await deletedItem.saveTx();
+
+ var uriOne = Zotero.URI.getItemURI(itemOne);
+ var uriDeleted = Zotero.URI.getItemURI(deletedItem);
+ var itemDataOne = Zotero.Utilities.Item.itemToCSLJSON(itemOne);
+ var itemDataDeleted = Zotero.Utilities.Item.itemToCSLJSON(deletedItem);
+
+ var citationOne = {
+ citationItems: [{ uris: [uriOne] }],
+ properties: {}
+ };
+ var citationDeleted = {
+ citationItems: [{ uris: [uriDeleted] }],
+ properties: {}
+ };
+
+ var storedCitationItems = [
+ { uris: [uriOne], itemData: itemDataOne },
+ { uris: [uriDeleted], itemData: itemDataDeleted }
+ ];
+ var deletedNoteHTML = ''
+ + '
Before
'
+ + '
(lastOne and lastTwo)
'
+ + '
(lastDeleted)
'
+ + '
After
'
+ + '
';
+
+ var deletedNoteItem = createUnsavedDataObject('item', {
+ itemType: 'note',
+ note: deletedNoteHTML
+ });
+ await deletedNoteItem.saveTx();
+
+ await deletedItem.eraseTx();
+
+ Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({ mode: 'bibliography', id: styleID, contentType: '', locale: '' }));
+
+ let items = Zotero.QuickCopy.reformatNoteCitations([deletedNoteItem]);
+ let format = {
+ mode: 'export',
+ id: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT,
+ markdownOptions: { includeAppLinks: false },
+ htmlOptions: { includeAppLinks: false }
+ };
+ win.Zotero_File_Interface.exportItemsToClipboard(items, format);
+ await Zotero.Promise.delay();
+
+ var text = getClipboardText();
+ var expected = 'Before\n\n'
+ + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n'
+ + '(firstDeleted lastDeleted, Deleted Item (n.d.).)\n\n'
+ + 'After';
+ assert.equal(text, expected);
+
+ await deletedNoteItem.eraseTx();
+ });
+ });
});
diff --git a/test/tests/quickCopyTest.js b/test/tests/quickCopyTest.js
index f2c7473361..ce7b8fba1e 100644
--- a/test/tests/quickCopyTest.js
+++ b/test/tests/quickCopyTest.js
@@ -1,50 +1,290 @@
describe("Zotero.QuickCopy", function () {
- var quickCopyPref;
- var prefName = "export.quickCopy.setting";
-
- before(function* () {
- yield Zotero.QuickCopy.loadSiteSettings();
- Zotero.Prefs.clear(prefName);
- quickCopyPref = Zotero.Prefs.get(prefName);
- quickCopyPref = JSON.stringify(Zotero.QuickCopy.unserializeSetting(quickCopyPref));
+ let domain = "test.org";
+
+ const DEFAULT_BIB = {
+ mode: 'bibliography',
+ id: 'http://www.zotero.org/styles/chicago-shortened-notes-bibliography',
+ contentType: '',
+ locale: ''
+ };
+ const DEFAULT_EXPORT = {
+ mode: 'export',
+ id: '14763d24-8ba0-45df-8f52-b8d1108e7ac9' // Zotero RDF
+ };
+ const DEFAULT_DRAG = 'bibliography';
+ const APA_STYLE_ID = 'http://www.zotero.org/styles/apa';
+ const BIBTEX_TRANSLATOR_ID = '9cb70025-a888-4a29-a210-93ec52da40d4'; // BibTeX
+
+ before(async function () {
+ await Zotero.QuickCopy.loadSiteSettings();
});
-
- afterEach(function () {
- Zotero.Prefs.clear(prefName);
+
+ beforeEach(function () {
+ Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify(DEFAULT_BIB));
+ Zotero.Prefs.set('export.quickCopy.exportSetting', JSON.stringify(DEFAULT_EXPORT));
+ Zotero.Prefs.set('export.quickCopy.preferredFormatOnDrag', DEFAULT_DRAG);
+ Zotero.QuickCopy.lastActiveURL = null;
});
-
- // TODO: These should set site-specific prefs and test the actual response against it,
- // but that will need to wait for 5.0. For now, just make sure they don't fail.
- describe("#getFormatFromURL()", function () {
+
+ describe("#getFormat()", function () {
+ async function setSiteSetting(rawFormat) {
+ await Zotero.DB.queryAsync(
+ "REPLACE INTO settings VALUES ('quickCopySite', ?, ?)",
+ [domain, rawFormat]
+ );
+ await Zotero.QuickCopy.loadSiteSettings();
+ }
+
+ afterEach(async function () {
+ await Zotero.DB.queryAsync("DELETE FROM settings WHERE setting='quickCopySite'");
+ await Zotero.QuickCopy.loadSiteSettings();
+ });
+
it("should handle an HTTP URL", function () {
- assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo.com/'), quickCopyPref);
- })
-
- it("should handle an HTTPS URL", function () {
- assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('https://foo.com/'), quickCopyPref);
- })
-
- it("should handle a domain and path", function () {
- assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo.com/bar'), quickCopyPref);
- })
-
- it("should handle a local host", function () {
- assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo/'), quickCopyPref);
- })
-
- it("should handle a domain with a trailing period", function () {
- assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('http://foo.com.'), quickCopyPref);
- })
-
- it("should handle an about: URL", function () {
- assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('about:blank'), quickCopyPref);
- })
-
- it("should handle a chrome URL", function () {
- assert.deepEqual(Zotero.QuickCopy.getFormatFromURL('chrome://zotero/content/foo.xul'), quickCopyPref);
- })
- })
-
+ let urls = [
+ "http://foo.com/",
+ "https://foo.com/",
+ "http://foo.com/bar",
+ "http://foo/",
+ "http://foo.com",
+ "about:blank",
+ "chrome://zotero/content/foo.xul"
+ ];
+ for (let url of urls) {
+ Zotero.QuickCopy.lastActiveURL = url;
+ assert.deepEqual(Zotero.QuickCopy.getFormat(), JSON.parse(Zotero.Prefs.get("export.quickCopy.bibliographySetting")));
+ }
+ });
+
+ it("should prefer the longer-domain match when multiple site settings apply", async function () {
+ // `test.org` matches anything ending in test.org;
+ // `wiki.test.org` matches only that subdomain
+ await Zotero.DB.queryAsync(
+ "REPLACE INTO settings VALUES ('quickCopySite', ?, ?)",
+ ['test.org', JSON.stringify({
+ bibliography: {
+ id: 'http://www.zotero.org/styles/chicago-author-date',
+ contentType: '',
+ locale: ''
+ },
+ drag: 'bibliography'
+ })]
+ );
+ await Zotero.DB.queryAsync(
+ "REPLACE INTO settings VALUES ('quickCopySite', ?, ?)",
+ ['wiki.test.org', JSON.stringify({
+ bibliography: { id: APA_STYLE_ID, contentType: '', locale: '' },
+ drag: 'bibliography'
+ })]
+ );
+ await Zotero.QuickCopy.loadSiteSettings();
+
+ Zotero.QuickCopy.lastActiveURL = 'https://wiki.test.org/page';
+ let bib = Zotero.QuickCopy.getFormat('bibliography');
+ assert.equal(bib.id, APA_STYLE_ID);
+ });
+
+ it("should prefer the longer-path match when domains are equal", async function () {
+ // Same domain, different paths
+ await Zotero.DB.queryAsync(
+ "REPLACE INTO settings VALUES ('quickCopySite', ?, ?)",
+ ['test.org', JSON.stringify({
+ bibliography: {
+ id: 'http://www.zotero.org/styles/chicago-author-date',
+ contentType: '',
+ locale: ''
+ },
+ drag: 'bibliography'
+ })]
+ );
+ await Zotero.DB.queryAsync(
+ "REPLACE INTO settings VALUES ('quickCopySite', ?, ?)",
+ ['test.org/styles/', JSON.stringify({
+ bibliography: { id: APA_STYLE_ID, contentType: '', locale: '' },
+ drag: 'bibliography'
+ })]
+ );
+ await Zotero.QuickCopy.loadSiteSettings();
+
+ Zotero.QuickCopy.lastActiveURL = 'https://test.org/styles/apa';
+ let bib = Zotero.QuickCopy.getFormat('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 expected = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.bibliographySetting')
+ );
+ assert.equal(result.mode, 'bibliography');
+ assert.equal(result.id, expected.id);
+ });
+
+ it("should return the default exportSetting for mode='export'", function () {
+ Zotero.QuickCopy.lastActiveURL = 'https://no-match.test/';
+ let result = Zotero.QuickCopy.getFormat('export');
+ let expected = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.exportSetting')
+ );
+ assert.equal(result.mode, 'export');
+ assert.equal(result.id, expected.id);
+ });
+
+ it("should follow preferredFormatOnDrag for drag (no mode)", function () {
+ Zotero.QuickCopy.lastActiveURL = 'https://no-match.test/';
+ Zotero.Prefs.set('export.quickCopy.preferredFormatOnDrag', 'export');
+ let result = Zotero.QuickCopy.getFormat();
+ assert.equal(result.mode, 'export');
+
+ Zotero.Prefs.set('export.quickCopy.preferredFormatOnDrag', 'bibliography');
+ result = Zotero.QuickCopy.getFormat();
+ assert.equal(result.mode, 'bibliography');
+ });
+ });
+
+ describe("legacy site setting format", function () {
+ it("should resolve a legacy bibliography site setting", async function () {
+ Zotero.QuickCopy.lastActiveURL = `https://${domain}/`;
+ let legacy = JSON.stringify({
+ mode: 'bibliography',
+ id: APA_STYLE_ID,
+ contentType: 'html',
+ locale: 'en-US'
+ });
+ await setSiteSetting(legacy);
+
+ // bibliography mode → site override
+ let bib = Zotero.QuickCopy.getFormat('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 globalExport = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.exportSetting')
+ );
+ assert.equal(exp.mode, 'export');
+ assert.equal(exp.id, globalExport.id);
+
+ // drag → legacy entry's mode wins (preserves old behavior)
+ let drag = Zotero.QuickCopy.getFormat();
+ assert.equal(drag.mode, 'bibliography');
+ assert.equal(drag.id, APA_STYLE_ID);
+ });
+
+ it("should resolve a legacy export site setting", async function () {
+ Zotero.QuickCopy.lastActiveURL = `https://${domain}/`;
+ let legacy = JSON.stringify({
+ mode: 'export',
+ id: BIBTEX_TRANSLATOR_ID
+ });
+ await setSiteSetting(legacy);
+
+ // export mode → site override
+ let exp = Zotero.QuickCopy.getFormat('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 globalBib = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.bibliographySetting')
+ );
+ assert.equal(bib.mode, 'bibliography');
+ assert.equal(bib.id, globalBib.id);
+
+ // drag → legacy entry's mode wins
+ let drag = Zotero.QuickCopy.getFormat();
+ assert.equal(drag.mode, 'export');
+ assert.equal(drag.id, BIBTEX_TRANSLATOR_ID);
+ });
+
+ it("should support legacy 'bibliography/html=...' string format", async function () {
+ Zotero.QuickCopy.lastActiveURL = `https://${domain}/`;
+ await setSiteSetting(`bibliography/html=${APA_STYLE_ID}`);
+
+ let bib = Zotero.QuickCopy.getFormat('bibliography');
+ assert.equal(bib.mode, 'bibliography');
+ assert.equal(bib.id, APA_STYLE_ID);
+ assert.equal(bib.contentType, 'html');
+ });
+ });
+
+ describe("new site setting format", function () {
+ it("should resolve a new-format site setting with bibliography only", async function () {
+ Zotero.QuickCopy.lastActiveURL = `https://${domain}/`;
+ let siteFormat = JSON.stringify({
+ bibliography: {
+ id: APA_STYLE_ID,
+ contentType: '',
+ locale: 'fr-FR'
+ },
+ drag: 'bibliography'
+ });
+ await setSiteSetting(siteFormat);
+
+ let bib = Zotero.QuickCopy.getFormat('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 globalExport = Zotero.QuickCopy.unserializeSetting(
+ Zotero.Prefs.get('export.quickCopy.exportSetting')
+ );
+ assert.equal(exp.id, globalExport.id);
+
+ // drag → site.drag = 'bibliography' → site bib
+ let drag = Zotero.QuickCopy.getFormat();
+ assert.equal(drag.mode, 'bibliography');
+ assert.equal(drag.id, APA_STYLE_ID);
+ });
+
+ it("should resolve a new-format site setting with both bib and export", async function () {
+ Zotero.QuickCopy.lastActiveURL = `https://${domain}/`;
+ let siteFormat = JSON.stringify({
+ bibliography: { id: APA_STYLE_ID, contentType: '', locale: '' },
+ export: { id: BIBTEX_TRANSLATOR_ID },
+ drag: 'export'
+ });
+ await setSiteSetting(siteFormat);
+
+ let bib = Zotero.QuickCopy.getFormat('bibliography');
+ assert.equal(bib.mode, 'bibliography');
+ assert.equal(bib.id, APA_STYLE_ID);
+
+ let exp = Zotero.QuickCopy.getFormat('export');
+ assert.equal(exp.mode, 'export');
+ assert.equal(exp.id, BIBTEX_TRANSLATOR_ID);
+
+ // drag → site.drag = 'export' → site export
+ let drag = Zotero.QuickCopy.getFormat();
+ assert.equal(drag.mode, 'export');
+ assert.equal(drag.id, BIBTEX_TRANSLATOR_ID);
+ });
+
+ it("should follow site.drag override regardless of preferredFormatOnDrag", async function () {
+ Zotero.QuickCopy.lastActiveURL = `https://${domain}/`;
+ Zotero.Prefs.set('export.quickCopy.preferredFormatOnDrag', 'bibliography');
+ let siteFormat = JSON.stringify({
+ bibliography: { id: APA_STYLE_ID, contentType: '', locale: '' },
+ export: { id: BIBTEX_TRANSLATOR_ID },
+ drag: 'export'
+ });
+ await setSiteSetting(siteFormat);
+
+ let drag = Zotero.QuickCopy.getFormat();
+ assert.equal(drag.mode, 'export');
+ assert.equal(drag.id, BIBTEX_TRANSLATOR_ID);
+ });
+ });
+ });
+
describe("#getContentFromItems()", function () {
it("should generate BibTeX", async function () {
var item = await createDataObject('item');
@@ -55,7 +295,7 @@ describe("Zotero.QuickCopy", function () {
var translatorID = '9cb70025-a888-4a29-a210-93ec52da40d4'; // BibTeX
var format = 'export=' + translatorID;
- Zotero.Prefs.set(prefName, format);
+ Zotero.Prefs.set("export.quickCopy.exportSetting", format);
// Translator code for selected format is loaded automatically, so wait for it
var translator = Zotero.Translators.get(translatorID);
while (!translator.code) {
@@ -82,14 +322,12 @@ describe("Zotero.QuickCopy", function () {
var content = "";
var worked = false;
- // Quick Copy locale not set
- Zotero.Prefs.clear('export.quickCopy.locale');
// 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(prefName, format);
+ Zotero.Prefs.set("export.quickCopy.bibliographySetting", format);
var { text, html } = Zotero.QuickCopy.getContentFromItems([item], format);
Zotero.debug(text);
@@ -109,7 +347,7 @@ describe("Zotero.QuickCopy", function () {
let styleID = 'https://www.zotero-chinese.com/styles/法学引注手册(多语言,重复引用不省略)';
let format = `bibliography=${styleID}`;
- Zotero.Prefs.set(prefName, format);
+ Zotero.Prefs.set("export.quickCopy.bibliographySetting", format);
let item = createUnsavedDataObject('item', {
itemType: 'journalArticle',
diff --git a/test/tests/utilities_internalTest.js b/test/tests/utilities_internalTest.js
index b9b7ddb22b..f21a3b73fd 100644
--- a/test/tests/utilities_internalTest.js
+++ b/test/tests/utilities_internalTest.js
@@ -970,4 +970,190 @@ describe("Zotero.Utilities.Internal", function () {
}
});
});
+
+ describe("#onDragItems()", function () {
+ var noteItem, itemOne, itemTwo;
+ var styleID = 'http://www.zotero.org/styles/chicago-notes-bibliography';
+
+ before(async function () {
+ await Zotero.Styles.init();
+ await Zotero.Translators.init();
+
+ itemOne = createUnsavedDataObject('item', {
+ itemType: 'book',
+ title: 'Item One',
+ creators: [
+ { firstName: 'firstOne', lastName: 'lastOne', creatorType: 'author' },
+ { firstName: 'firstTwo', lastName: 'lastTwo', creatorType: 'author' }
+ ]
+ });
+ await itemOne.saveTx();
+
+ itemTwo = createUnsavedDataObject('item', {
+ itemType: 'book',
+ title: 'Item Two',
+ creators: [
+ { firstName: 'firstTwo', lastName: 'lastTwo', creatorType: 'author' },
+ { firstName: 'firstThree', lastName: 'lastThree', creatorType: 'author' }
+ ]
+ });
+ await itemTwo.saveTx();
+
+ var uriOne = Zotero.URI.getItemURI(itemOne);
+ var uriTwo = Zotero.URI.getItemURI(itemTwo);
+ var citationOne = {
+ citationItems: [{ uris: [uriOne] }],
+ properties: {}
+ };
+ var citationTwo = {
+ citationItems: [{ uris: [uriTwo] }],
+ properties: {}
+ };
+ var noteHTML = ''
+ + '
Some text
'
+ + '
(lastOne and lastTwo)
'
+ + '
(lastTwo and lastThree)
'
+ + '
Done
'
+ + '
';
+
+ noteItem = createUnsavedDataObject('item', {
+ itemType: 'note',
+ note: noteHTML
+ });
+ await noteItem.saveTx();
+
+ // Preload note translators
+ let markdownTranslator = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN);
+ markdownTranslator.cacheCode = true;
+ await Zotero.Translators.getCodeForTranslator(markdownTranslator);
+ let htmlTranslator = Zotero.Translators.get(Zotero.Translators.TRANSLATOR_ID_NOTE_HTML);
+ htmlTranslator.cacheCode = true;
+ await Zotero.Translators.getCodeForTranslator(htmlTranslator);
+ });
+
+ afterEach(function () {
+ Zotero.Prefs.clear('export.quickCopy.preferredFormatOnDrag');
+ Zotero.Prefs.clear('export.quickCopy.bibliographySetting');
+ Zotero.Prefs.clear('export.noteQuickCopy.setting');
+ });
+
+ after(async function () {
+ await Zotero.Items.erase([noteItem.id, itemOne.id, itemTwo.id]);
+ });
+
+ function createFakeEvent() {
+ var data = {};
+ return {
+ dataTransfer: {
+ setData: (type, val) => data[type] = val,
+ getData: type => data[type],
+ setDragImage: () => {},
+ mozSetDataAt: () => {},
+ effectAllowed: 'copy'
+ },
+ shiftKey: false
+ };
+ }
+
+ ['bibliography', 'export'].forEach(function (preferredFormat) {
+ it(`should produce CSL-formatted plain text when dragging a note regardless of preferredFormat=${preferredFormat}`, function () {
+ Zotero.Prefs.set('export.quickCopy.preferredFormatOnDrag', preferredFormat);
+ Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({ mode: 'bibliography', id: styleID, contentType: '', locale: '' }));
+ Zotero.Prefs.set('export.noteQuickCopy.setting', JSON.stringify({
+ mode: 'export',
+ id: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT,
+ markdownOptions: { includeAppLinks: false },
+ htmlOptions: { includeAppLinks: false }
+ }));
+
+ var event = createFakeEvent();
+ Zotero.Utilities.Internal.onDragItems(event, [noteItem.id]);
+
+ var text = event.dataTransfer.getData('text/plain');
+ var expected = 'Some text\n\n'
+ + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n'
+ + '(firstTwo lastTwo and firstThree lastThree, Item Two (n.d.).)\n\n'
+ + 'Done';
+ assert.equal(text, expected);
+ });
+ });
+
+ it("should use embedded item data for deleted items when dragging a note", async function () {
+ // Create items and build a note with embedded citation data
+ var deletedItem = createUnsavedDataObject('item', {
+ itemType: 'book',
+ title: 'Deleted Item',
+ creators: [
+ { firstName: 'firstDeleted', lastName: 'lastDeleted', creatorType: 'author' }
+ ]
+ });
+ await deletedItem.saveTx();
+
+ var uriOne = Zotero.URI.getItemURI(itemOne);
+ var uriDeleted = Zotero.URI.getItemURI(deletedItem);
+ var itemDataOne = Zotero.Utilities.Item.itemToCSLJSON(itemOne);
+ var itemDataDeleted = Zotero.Utilities.Item.itemToCSLJSON(deletedItem);
+
+ var citationOne = {
+ citationItems: [{ uris: [uriOne] }],
+ properties: {}
+ };
+ var citationDeleted = {
+ citationItems: [{ uris: [uriDeleted] }],
+ properties: {}
+ };
+
+ // Build note with data-citation-items containing embedded item data
+ var storedCitationItems = [
+ { uris: [uriOne], itemData: itemDataOne },
+ { uris: [uriDeleted], itemData: itemDataDeleted }
+ ];
+ var deletedNoteHTML = ''
+ + '
Before
'
+ + '
(lastOne and lastTwo)
'
+ + '
(lastDeleted)
'
+ + '
After
'
+ + '
';
+
+ var deletedNoteItem = createUnsavedDataObject('item', {
+ itemType: 'note',
+ note: deletedNoteHTML
+ });
+ await deletedNoteItem.saveTx();
+
+ // Delete the item so its URI no longer resolves
+ await deletedItem.eraseTx();
+
+ Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify({ mode: 'bibliography', id: styleID, contentType: '', locale: '' }));
+ Zotero.Prefs.set('export.noteQuickCopy.setting', JSON.stringify({
+ mode: 'export',
+ id: Zotero.Translators.TRANSLATOR_ID_MARKDOWN_AND_RICH_TEXT,
+ markdownOptions: { includeAppLinks: false },
+ htmlOptions: { includeAppLinks: false }
+ }));
+
+ var event = createFakeEvent();
+ Zotero.Utilities.Internal.onDragItems(event, [deletedNoteItem.id]);
+
+ var text = event.dataTransfer.getData('text/plain');
+ var expected = 'Before\n\n'
+ + '(firstOne lastOne and firstTwo lastTwo, Item One (n.d.).)\n\n'
+ + '(firstDeleted lastDeleted, Deleted Item (n.d.).)\n\n'
+ + 'After';
+ assert.equal(text, expected);
+
+ // Cleanup
+ await deletedNoteItem.eraseTx();
+ });
+ });
});