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();
+ });
+ });
});