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.
This commit is contained in:
Bogdan Abaev 2026-05-04 16:58:33 -07:00
parent a692ca3493
commit b2a884bf5f
16 changed files with 1518 additions and 515 deletions

View file

@ -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) => {

View file

@ -35,36 +35,50 @@
<separator/>
<label value="&zotero.preferences.quickCopy.itemFormat;" control="zotero-quickCopy-menu"/>
<menulist id="zotero-quickCopy-menu" label="&zotero.general.loading;" native="true"/>
<label data-l10n-id="preferences-quickCopy-preferredFormatOnDrag" control="zotero-quickCopy-preferredFormatOnDrag"/>
<radiogroup orient="horizontal" id="zotero-quickCopy-preferredFormatOnDrag"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()">
<radio data-l10n-id="preferences-quickCopy-preferredFormatOnDrag-bibliography-radio" value="bibliography"/>
<radio data-l10n-id="preferences-quickCopy-preferredFormatOnDrag-export-radio" value="export"/>
</radiogroup>
<label data-l10n-id="preferences-quickCopy-bibliographyFormat" control="zotero-quickCopy-bibliography-menu"/>
<menulist id="zotero-quickCopy-bibliography-menu" label="&zotero.general.loading;"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()" native="true"/>
<hbox align="center">
<label id="zotero-quickCopy-locale-menu-label" value="&zotero.bibliography.locale.label;" control="zotero-quickCopy-locale-menu"/>
<menulist id="zotero-quickCopy-locale-menu" oncommand="Zotero_Preferences.Export._lastSelectedLocale = this.value" native="true"/>
<menulist id="zotero-quickCopy-locale-menu"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()" native="true"/>
<separator orient="vertical" width="15px"/>
<checkbox id="zotero-quickCopy-copyAsHTML" label="&zotero.preferences.quickCopy.copyAsHTML;"
oncommand="Zotero_Preferences.Export.onCopyAsHTMLChange(this.checked)"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()"
native="true"/>
</hbox>
<label data-l10n-id="preferences-quickCopy-exportFormat" control="zotero-quickCopy-export-menu"/>
<menulist id="zotero-quickCopy-export-menu" label="&zotero.general.loading;"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()" native="true"/>
<separator/>
<label value="&zotero.preferences.quickCopy.noteFormat;" control="zotero-noteQuickCopy-menu"/>
<menulist id="zotero-noteQuickCopy-menu" label="&zotero.general.loading;" native="true"/>
<menulist id="zotero-noteQuickCopy-menu" label="&zotero.general.loading;"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()" native="true"/>
<box id="noteQuickCopy-wrapper">
<vbox id="noteQuickCopy-format-options" hidden="true" align="center">
<hbox id="noteQuickCopy-markdown-options" align="center">
<label id="noteQuickCopy-markdown-includeAppLinks-label"></label>
<checkbox id="noteQuickCopy-markdown-includeAppLinks"
oncommand="Zotero_Preferences.Export.onUpdateNoteExportOptions()"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()"
native="true" aria-describedby="noteQuickCopy-markdown-includeAppLinks-label"/>
</hbox>
<hbox id="noteQuickCopy-html-options" align="center">
<label id="noteQuickCopy-html-includeAppLinks-label"></label>
<checkbox id="noteQuickCopy-html-includeAppLinks"
oncommand="Zotero_Preferences.Export.onUpdateNoteExportOptions()"
oncommand="Zotero_Preferences.Export.updateQuickCopyPrefs()"
native="true" aria-describedby="noteQuickCopy-html-includeAppLinks-label"/>
</hbox>
</vbox>

View file

@ -38,96 +38,157 @@
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
title=""
onload="Zotero_QuickCopySiteEditor.onLoad(); Zotero_Preferences.Export.updateQuickCopyUI(); sizeToContent()">
<dialog
onload="Zotero_QuickCopySiteEditor.onLoad(); sizeToContent()">
<dialog
buttons="cancel,accept"
id="zotero-quickCopySiteEditor">
<linkset>
<html:link rel="localization" href="branding/brand.ftl"/>
<html:link rel="localization" href="zotero.ftl"/>
<html:link rel="localization" href="preferences.ftl"/>
</linkset>
<script src="chrome://zotero/content/include.js"/>
<script src="preferences.js"/>
<script src="preferences_export.js"/>
<vbox id="zotero-preferences-quickCopySiteEditor">
<label value="&zotero.preferences.quickCopy.siteEditor.domainPath; &zotero.preferences.quickCopy.siteEditor.domainPath.example;" control="zotero-quickCopy-domain"/>
<html:input type="text" id="zotero-quickCopy-domain" oninput="Zotero_QuickCopySiteEditor.onDomainInput(event)"/>
<separator class="thin"/>
<label value="&zotero.preferences.quickCopy.siteEditor.format;" control="zotero-quickCopy-menu"/>
<menulist id="zotero-quickCopy-menu" native="true"/>
<separator class="thin"/>
<label id="zotero-quickCopy-locale-menu-label" value="&zotero.preferences.quickCopy.siteEditor.locale;" control="zotero-quickCopy-locale-menu"/>
<hbox align="center">
<menulist id="zotero-quickCopy-locale-menu" oncommand="Zotero_Preferences.Export._lastSelectedLocale = this.value" native="true"/>
</hbox>
<separator class="thin"/>
<checkbox id="zotero-quickCopy-copyAsHTML" label="&zotero.preferences.quickCopy.copyAsHTML;"
oncommand="Zotero_Preferences.Export.buildQuickCopyFormatDropDown(
document.getElementById('zotero-quickCopy-menu'),
this.checked ? 'html' : '',
null,
io.translators
)"
<label data-l10n-id="preferences-quickCopy-bibliographyFormat" control="zotero-quickCopy-bibliography-menu"/>
<menulist id="zotero-quickCopy-bibliography-menu"
oncommand="Zotero_QuickCopySiteEditor.onBibliographyChange()"
native="true"/>
<hbox align="center">
<label id="zotero-quickCopy-locale-menu-label" value="&zotero.preferences.quickCopy.siteEditor.locale;" control="zotero-quickCopy-locale-menu"/>
<menulist id="zotero-quickCopy-locale-menu" native="true"/>
</hbox>
<checkbox id="zotero-quickCopy-copyAsHTML" label="&zotero.preferences.quickCopy.copyAsHTML;"
native="true"/>
<separator class="thin"/>
<label data-l10n-id="preferences-quickCopy-exportFormat" control="zotero-quickCopy-export-menu"/>
<menulist id="zotero-quickCopy-export-menu" native="true"/>
<separator class="thin"/>
<label data-l10n-id="preferences-quickCopy-preferredFormatOnDrag"/>
<radiogroup orient="horizontal" id="zotero-quickCopy-preferredFormatOnDrag">
<radio data-l10n-id="preferences-quickCopy-preferredFormatOnDrag-bibliography-radio" value="bibliography"/>
<radio data-l10n-id="preferences-quickCopy-preferredFormatOnDrag-export-radio" value="export"/>
</radiogroup>
</vbox>
<script>
<![CDATA[
document.addEventListener('dialogaccept', () => Zotero_QuickCopySiteEditor.onAccept());
document.addEventListener('dialogaccept', (event) => {
if (!Zotero_QuickCopySiteEditor.onAccept()) {
event.preventDefault();
}
});
var Zotero_QuickCopySiteEditor = new function () {
var menu = document.getElementById('zotero-quickCopy-menu');
var bibMenu, exportMenu, dragRadio, copyAsHTML, localeMenu, domainInput;
this.onLoad = function () {
// Hide OK button initially if domain field is empty
document.getElementById('zotero-quickCopySiteEditor').getButton('accept').disabled
= document.getElementById('zotero-quickCopy-domain').value.trim() === '';
var io = window.arguments[0];
var existing = io.siteSetting || {};
// Pre-select with the existing site override if there is one,
// otherwise the user's current global preference
var bibSub = existing.bibliography
|| Zotero.QuickCopy.unserializeSetting(
Zotero.Prefs.get('export.quickCopy.bibliographySetting')
);
var expSub = existing.export
|| Zotero.QuickCopy.unserializeSetting(
Zotero.Prefs.get('export.quickCopy.exportSetting')
);
bibMenu = document.getElementById('zotero-quickCopy-bibliography-menu');
exportMenu = document.getElementById('zotero-quickCopy-export-menu');
dragRadio = document.getElementById('zotero-quickCopy-preferredFormatOnDrag');
copyAsHTML = document.getElementById('zotero-quickCopy-copyAsHTML');
localeMenu = document.getElementById('zotero-quickCopy-locale-menu');
domainInput = document.getElementById('zotero-quickCopy-domain');
domainInput.value = io.domain || '';
// Build dropdowns; selection is applied below
Zotero_Preferences.Export.buildBibliographyQuickCopyDropDown(bibMenu);
Zotero_Preferences.Export.buildExportQuickCopyDropDown(exportMenu, io.translators);
Zotero_Preferences.Export._selectMenuItemByValue(bibMenu, bibSub.id);
Zotero_Preferences.Export._selectMenuItemByValue(exportMenu, expSub.id);
copyAsHTML.checked = bibSub.contentType === 'html';
// Locale list: populate full options, then filter by the selected style
Zotero.Styles.populateLocaleList(localeMenu);
Zotero.Styles.updateLocaleList(
localeMenu,
bibSub.id ? Zotero.Styles.get(bibSub.id) : null,
bibSub.locale || ''
);
dragRadio.value = existing.drag
|| Zotero.Prefs.get('export.quickCopy.preferredFormatOnDrag');
this.updateAcceptButton();
};
this.updateAcceptButton = function () {
// Domain is the only required field; both menus always have a pre-selected value
var hasDomain = domainInput.value.trim() !== '';
document.getElementById('zotero-quickCopySiteEditor')
.getButton('accept').disabled = !hasDomain;
};
// Re-filter the locale list when the bibliography style changes
this.onBibliographyChange = function () {
let styleID = bibMenu.value;
Zotero.Styles.updateLocaleList(
localeMenu,
styleID ? Zotero.Styles.get(styleID) : null,
localeMenu.value || ''
);
};
this.onAccept = function () {
var io = window.arguments[0];
io.domain = document.getElementById('zotero-quickCopy-domain').value;
io.format = menu.value;
io.locale = '';
if (!document.getElementById('zotero-quickCopy-locale-menu').disabled) {
io.locale = document.getElementById('zotero-quickCopy-locale-menu').value;
}
io.domain = domainInput.value;
io.siteSetting = {
bibliography: {
id: bibMenu.value || '',
contentType: copyAsHTML.checked ? 'html' : '',
locale: localeMenu.value || ''
},
export: { id: exportMenu.value || '' },
drag: dragRadio.value
|| Zotero.Prefs.get('export.quickCopy.preferredFormatOnDrag')
};
io.ok = true;
}
return true;
};
this.onDomainInput = function (event) {
// Hide OK button if domain field is empty
document.getElementById('zotero-quickCopySiteEditor').getButton('accept').disabled = event.target.value.trim() === '';
// Auto-select Wikipedia Citation Templates
this.updateAcceptButton();
// Auto-select Wikipedia Citation Templates as the export format
if (event.target.value == 'wikipedia.org') {
menu.value = 'export=3f50aaac-7acc-4350-acd0-59cb77faf620';
Zotero_Preferences.Export.updateQuickCopyUI();
Zotero_Preferences.Export._selectMenuItemByValue(
exportMenu,
'3f50aaac-7acc-4350-acd0-59cb77faf620'
);
}
};
}
var io = window.arguments[0];
var contentType = io.asHTML ? 'html' : '';
document.getElementById('zotero-quickCopy-domain').value = io.domain ? io.domain : '';
Zotero_Preferences.Export.buildQuickCopyFormatDropDown(
document.getElementById('zotero-quickCopy-menu'),
contentType,
io.format,
io.translators
);
Zotero.Styles.populateLocaleList(
document.getElementById('zotero-quickCopy-locale-menu')
);
document.getElementById('zotero-quickCopy-copyAsHTML').checked = io.asHTML;
Zotero_Preferences.Export._lastSelectedLocale = io.locale;
]]>
</script>
</dialog>
</window>

View file

@ -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;

View file

@ -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);

View file

@ -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 <span class="citation"> 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");

View file

@ -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

View file

@ -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");
}

View file

@ -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

View file

@ -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
);

View file

@ -104,10 +104,13 @@
<command id="cmd_zotero_exportLibrary" oncommand="Zotero_File_Interface.exportFile();"/>
<command id="cmd_zotero_advancedSearch" oncommand="ZoteroPane_Local.openAdvancedSearchWindow();"/>
<command id="cmd_zotero_copyCitation"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(true);"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(true, 'bibliography');"
disabled="true"/>
<command id="cmd_zotero_copyBibliography"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard();"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(false, 'bibliography');"
disabled="true"/>
<command id="cmd_zotero_copyExport"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard(false, 'export');"
disabled="true"/>
<command id="cmd_zotero_copyAnnotation"
oncommand="ZoteroPane_Local.copySelectedItemsToClipboard();"
@ -172,6 +175,9 @@
<key id="key_copyBibliography"
command="cmd_zotero_copyBibliography"
modifiers="accel shift"/>
<key id="key_copyExport"
command="cmd_zotero_copyExport"
modifiers="accel shift"/>
<key id="key_advancedSearch"
command="cmd_zotero_advancedSearch"
key="&findCmd.key;"
@ -384,8 +390,8 @@
hidden="true"/>
<menuitem id="menu_copyExport"
class="menu-type-library"
key="key_copyBibliography"
command="cmd_zotero_copyBibliography"
key="key_copyExport"
command="cmd_zotero_copyExport"
hidden="true"/>
<menuitem id="menu_copyNote"
label="&copyNoteCmd.label;"

View file

@ -49,6 +49,18 @@ preferences-locate-library-lookup-intro = Library Lookup can find a resource onl
preferences-locate-resolver = Resolver:
preferences-locate-base-url = Base URL:
preferences-quickCopy-bibliographyFormat = Bibliography Style:
preferences-quickCopy-exportFormat = Export Format:
preferences-quickCopy-preferredFormatOnDrag = Format to use on drag:
preferences-quickCopy-preferredFormatOnDrag-bibliography = Bibliography
preferences-quickCopy-preferredFormatOnDrag-export = Export
preferences-quickCopy-preferredFormatOnDrag-bibliography-radio =
.label = { preferences-quickCopy-preferredFormatOnDrag-bibliography }
preferences-quickCopy-preferredFormatOnDrag-export-radio =
.label = { preferences-quickCopy-preferredFormatOnDrag-export }
preferences-quickCopy-siteEditor-export-format = Export Format
preferences-quickCopy-siteEditor-bibliography-format = Bibliography Format
preferences-quickCopy-siteEditor-use-on-drag = On Drag
preferences-quickCopy-minus =
.aria-label = { general-remove }
.label = { $label }

View file

@ -101,6 +101,7 @@ pref("extensions.zotero.keys.library", "L");
pref("extensions.zotero.keys.quicksearch", "K");
pref("extensions.zotero.keys.copySelectedItemCitationsToClipboard", "A");
pref("extensions.zotero.keys.copySelectedItemsToClipboard", "C");
pref("extensions.zotero.keys.copySelectedItemsToClipboardAsExport", "E");
pref("extensions.zotero.keys.sync", "Y");
pref("extensions.zotero.keys.toggleAllRead", "R");
pref("extensions.zotero.keys.toggleRead", "`");
@ -139,7 +140,9 @@ pref("extensions.zotero.import.createNewCollection.fromFileOpenHandler", true);
pref("extensions.zotero.rtfScan.lastInputFile", "");
pref("extensions.zotero.rtfScan.lastOutputFile", "");
pref("extensions.zotero.export.quickCopy.setting", "bibliography=http://www.zotero.org/styles/chicago-shortened-notes-bibliography");
pref("extensions.zotero.export.quickCopy.bibliographySetting", '{"mode":"bibliography","id":"http://www.zotero.org/styles/chicago-shortened-notes-bibliography","contentType":"","locale":""}');
pref("extensions.zotero.export.quickCopy.exportSetting", '{"mode":"export","id":"9cb70025-a888-4a29-a210-93ec52da40d4"}');
pref("extensions.zotero.export.quickCopy.preferredFormatOnDrag", "bibliography");
pref("extensions.zotero.export.quickCopy.dragLimit", 50);
pref("extensions.zotero.export.noteQuickCopy.setting", '{"mode":"export","id":"a45eca67-1ee8-45e5-b4c6-23fb8a852873","markdownOptions":{"includeAppLinks":true},"htmlOptions":{"includeAppLinks":false}}');

View file

@ -308,4 +308,184 @@ describe("Zotero_File_Interface", function () {
assert.equal(annotation5.annotationPageLabel, '3');
});
});
describe("#exportItemsToClipboard()", 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 = '<div data-schema-version="9">'
+ '<p>Some text</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationTwo))
+ '">(<span class="citation-item">lastTwo and lastThree</span>)</span></p>'
+ '<p>Done</p>'
+ '</div>';
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 = '<div data-citation-items="'
+ encodeURIComponent(JSON.stringify(storedCitationItems))
+ '" data-schema-version="9">'
+ '<p>Before</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationDeleted))
+ '">(<span class="citation-item">lastDeleted</span>)</span></p>'
+ '<p>After</p>'
+ '</div>';
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();
});
});
});

View file

@ -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',

View file

@ -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 = '<div data-schema-version="9">'
+ '<p>Some text</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationTwo))
+ '">(<span class="citation-item">lastTwo and lastThree</span>)</span></p>'
+ '<p>Done</p>'
+ '</div>';
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 = '<div data-citation-items="'
+ encodeURIComponent(JSON.stringify(storedCitationItems))
+ '" data-schema-version="9">'
+ '<p>Before</p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationOne))
+ '">(<span class="citation-item">lastOne and lastTwo</span>)</span></p>'
+ '<p><span class="citation" data-citation="'
+ encodeURIComponent(JSON.stringify(citationDeleted))
+ '">(<span class="citation-item">lastDeleted</span>)</span></p>'
+ '<p>After</p>'
+ '</div>';
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();
});
});
});