mirror of
https://github.com/zotero/zotero.git
synced 2026-10-04 02:31:47 +00:00
context-aware consolidated quick-copy command
Menu > Copy and Cmd/Ctrl + C are now one entry point for context-aware copying of selected items. Updated behavior: - when a regular item is selected, copy the citation - when a note or annotation is selected, copy them via quickCopy in note format - when a reader tab is open and nothing is selected, copy the citation of the top-level item - when a reader tab is open and an annotation is selected, copy the annotation - when a reader tab is open and text is selected, select the text - no changes here. Depending on which items are selected, Menu > Copy command labels will be updated to Menu > Copy Citation or Menu > Copy Note, or Menu > Copy Annotations. Added helpers to the reader and ZoteroPane to indicate what Cmd/Ctrl+C is supposed to do based on the selection (classifyCopyContext), so we can determine what label to use. Removed old Cmd-Shift-A "Copy Citation" shortcut. Updated preferences Export panel text, renamed preferences to "copyAsBibliography" and "copyAsExport" for clarity.
This commit is contained in:
parent
ac6c26096e
commit
99dbdfa9ac
16 changed files with 458 additions and 149 deletions
|
|
@ -89,6 +89,33 @@
|
|||
'zotero/item': event.dataTransfer.getData('zotero/item')
|
||||
}, this._iframe.contentWindow);
|
||||
}, true);
|
||||
// Same workaround for paste: the iframe's clipboardData.getData()
|
||||
// returns empty for non-standard MIME types (zotero/annotation,
|
||||
// zotero/item), so read them from the system clipboard here and
|
||||
// proxy into the iframe scope.
|
||||
this._iframe.contentWindow.addEventListener('paste', (_event) => {
|
||||
let read = (flavor) => {
|
||||
try {
|
||||
let xfer = Components.classes['@mozilla.org/widget/transferable;1']
|
||||
.createInstance(Components.interfaces.nsITransferable);
|
||||
xfer.init(null);
|
||||
xfer.addDataFlavor(flavor);
|
||||
Components.classes['@mozilla.org/widget/clipboard;1']
|
||||
.getService(Components.interfaces.nsIClipboard)
|
||||
.getData(xfer, Components.interfaces.nsIClipboard.kGlobalClipboard);
|
||||
let str = {};
|
||||
xfer.getTransferData(flavor, str, {});
|
||||
return str.value.QueryInterface(Components.interfaces.nsISupportsString).data;
|
||||
}
|
||||
catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
this._iframe.contentWindow.wrappedJSObject.pastedData = Components.utils.cloneInto({
|
||||
'zotero/annotation': read('zotero/annotation'),
|
||||
'zotero/item': read('zotero/item')
|
||||
}, this._iframe.contentWindow);
|
||||
}, true);
|
||||
this._iframe.docShell.windowDraggingAllowed = true;
|
||||
this._initialized = true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ var Zotero_File_Interface = new function () {
|
|||
*/
|
||||
this.writeToClipboard = function (content) {
|
||||
if (!content) return;
|
||||
let { text, html } = content;
|
||||
let { text, html, annotations } = content;
|
||||
|
||||
let transferable = Components.classes['@mozilla.org/widget/transferable;1']
|
||||
.createInstance(Components.interfaces.nsITransferable);
|
||||
|
|
@ -270,6 +270,16 @@ var Zotero_File_Interface = new function () {
|
|||
transferable.addDataFlavor('text/plain');
|
||||
transferable.setTransferData('text/plain', str, text.length * 2);
|
||||
}
|
||||
// Rich annotation payload for the note editor's paste handler; without
|
||||
// it, pasted annotations land as plain text with unresolved {citation}
|
||||
// placeholders.
|
||||
if (annotations) {
|
||||
let str = Components.classes['@mozilla.org/supports-string;1']
|
||||
.createInstance(Components.interfaces.nsISupportsString);
|
||||
str.data = annotations;
|
||||
transferable.addDataFlavor('zotero/annotation');
|
||||
transferable.setTransferData('zotero/annotation', str, annotations.length * 2);
|
||||
}
|
||||
|
||||
clipboardService.setData(transferable, null, Components.interfaces.nsIClipboard.kGlobalClipboard);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -168,16 +168,16 @@
|
|||
<html:label class="modifier"/>
|
||||
<html:input type="text" id="textbox-quicksearch" maxlength="1" size="1" preference="extensions.zotero.keys.quicksearch"/>
|
||||
|
||||
<html:label for="textbox-copySelectedItemCitationsToClipboard">&zotero.preferences.keys.copySelectedItemCitationsToClipboard;</html:label>
|
||||
<html:label for="textbox-copyAsBibliography" data-l10n-id="preferences-keys-copy-as-bibliography"/>
|
||||
<html:label class="modifier"/>
|
||||
<html:input type="text" id="textbox-copySelectedItemCitationsToClipboard" maxlength="1" size="1"
|
||||
preference="extensions.zotero.keys.copySelectedItemCitationsToClipboard"
|
||||
<html:input type="text" id="textbox-copyAsBibliography" maxlength="1" size="1"
|
||||
preference="extensions.zotero.keys.copyAsBibliography"
|
||||
oninput="if (Zotero_Preferences.Export) { Zotero_Preferences.Export.updateQuickCopyInstructions(); }"/>
|
||||
|
||||
<html:label for="textbox-copySelectedItemsToClipboard">&zotero.preferences.keys.copySelectedItemsToClipboard;</html:label>
|
||||
<html:label for="textbox-copyAsExport" data-l10n-id="preferences-keys-copy-as-export"/>
|
||||
<html:label class="modifier"/>
|
||||
<html:input type="text" id="textbox-copySelectedItemsToClipboard" maxlength="1" size="1"
|
||||
preference="extensions.zotero.keys.copySelectedItemsToClipboard"
|
||||
<html:input type="text" id="textbox-copyAsExport" maxlength="1" size="1"
|
||||
preference="extensions.zotero.keys.copyAsExport"
|
||||
oninput="if (Zotero_Preferences.Export) { Zotero_Preferences.Export.updateQuickCopyInstructions(); }"/>
|
||||
|
||||
<html:label for="textbox-toggleAllRead">&zotero.preferences.keys.toggleAllRead;</html:label>
|
||||
|
|
|
|||
|
|
@ -505,22 +505,20 @@ Zotero_Preferences.Export = {
|
|||
|
||||
|
||||
updateQuickCopyInstructions: function () {
|
||||
var prefix = Zotero.isMac ? Zotero.getString('general.keys.cmdShift') : Zotero.getString('general.keys.ctrlShift');
|
||||
|
||||
var key = Zotero.Prefs.get('keys.copySelectedItemsToClipboard');
|
||||
var str = Zotero.getString('zotero.preferences.export.quickCopy.instructions', prefix + key);
|
||||
var instr = document.getElementById('quickCopy-instructions');
|
||||
while (instr.hasChildNodes()) {
|
||||
instr.removeChild(instr.firstChild);
|
||||
}
|
||||
instr.appendChild(document.createTextNode(str));
|
||||
|
||||
key = Zotero.Prefs.get('keys.copySelectedItemCitationsToClipboard');
|
||||
str = Zotero.getString('zotero.preferences.export.quickCopy.citationInstructions', prefix + key);
|
||||
instr = document.getElementById('quickCopy-citationInstructions');
|
||||
while (instr.hasChildNodes()) {
|
||||
instr.removeChild(instr.firstChild);
|
||||
}
|
||||
instr.appendChild(document.createTextNode(str));
|
||||
var shiftPrefix = Zotero.getString("command-or-control") + "+" + Zotero.getString("general-key-shift") + "+";
|
||||
|
||||
document.l10n.setAttributes(
|
||||
document.getElementById('quickCopy-instructions'),
|
||||
'preferences-quickCopy-instructions',
|
||||
{
|
||||
bibShortcut: shiftPrefix + Zotero.Prefs.get('keys.copyAsBibliography'),
|
||||
exportShortcut: shiftPrefix + Zotero.Prefs.get('keys.copyAsExport')
|
||||
}
|
||||
);
|
||||
document.l10n.setAttributes(
|
||||
document.getElementById('quickCopy-citationInstructions'),
|
||||
'preferences-quickCopy-citationInstructions',
|
||||
{ citationShortcut: Zotero.getString("command-or-control") + "+C" }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const ZoteroStandalone = new function () {
|
|||
setTimeout(async () => {
|
||||
// Item and other things might not be loaded yet when reopening tabs
|
||||
await Zotero.Schema.schemaUpdatePromise;
|
||||
this.updateQuickCopyOptions();
|
||||
ZoteroPane.updateQuickCopyMenu();
|
||||
}, 0);
|
||||
// "library", "reader" or "note"
|
||||
let type = extraData[ids[0]].type;
|
||||
|
|
@ -95,12 +95,10 @@ const ZoteroStandalone = new function () {
|
|||
return Zotero.initializationPromise;
|
||||
})
|
||||
.then(async function () {
|
||||
document.getElementById('key_copyCitation')
|
||||
.setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemCitationsToClipboard'));
|
||||
document.getElementById('key_copyBibliography')
|
||||
.setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemsToClipboard'));
|
||||
.setAttribute('key', Zotero.Keys.getKeyForCommand('copyAsBibliography'));
|
||||
document.getElementById('key_copyExport')
|
||||
.setAttribute('key', Zotero.Keys.getKeyForCommand('copySelectedItemsToClipboardAsExport'));
|
||||
.setAttribute('key', Zotero.Keys.getKeyForCommand('copyAsExport'));
|
||||
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
|
||||
|
|
@ -236,9 +234,10 @@ const ZoteroStandalone = new function () {
|
|||
};
|
||||
|
||||
this.onEditMenuOpen = function (event) {
|
||||
this.updateQuickCopyOptions();
|
||||
// goUpdateGlobalEditMenuItems(true) is necessary to update Edit menu when contenteditable is focused
|
||||
// must happen before updateQuickCopyMenu so it knows the state of standard copy command
|
||||
window.goUpdateGlobalEditMenuItems(true);
|
||||
ZoteroPane.updateQuickCopyMenu();
|
||||
|
||||
this.onUpdateCustomMenus(event, 'edit');
|
||||
};
|
||||
|
|
@ -337,53 +336,18 @@ const ZoteroStandalone = new function () {
|
|||
};
|
||||
|
||||
|
||||
this.updateQuickCopyOptions = function () {
|
||||
var selected = [];
|
||||
|
||||
let win = Zotero.getMainWindow();
|
||||
if (win) {
|
||||
try {
|
||||
selected = win.ZoteroPane.getSelectedItems();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
win.ZoteroPane.updateQuickCopyCommands(selected);
|
||||
/**
|
||||
* Handle consolidated Edit > Copy menu clicked.
|
||||
* Native cmd_copy when text is selected, otherwise delegate to copySelectedItemsToClipboard
|
||||
* that handles copying of items/notes/annotation in library and reader tabs.
|
||||
*/
|
||||
this.handleCopyMenuCommand = function () {
|
||||
if (ZoteroPane.classifyCopyContext() === 'text') {
|
||||
// eslint-disable-next-line no-undef
|
||||
goDoCommand('cmd_copy');
|
||||
}
|
||||
|
||||
var exportingNotes = selected.every(item => item.isNote() || item.isAttachment());
|
||||
var exportingAnnotations = selected.every(item => item.isAnnotation());
|
||||
|
||||
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 || 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 });
|
||||
|
||||
// Label the export menu with the selected translator name
|
||||
var exportFormat = Zotero.QuickCopy.getFormat({ mode: 'export' });
|
||||
if (exportFormat.id) {
|
||||
try {
|
||||
let translator = Zotero.Translators.get(exportFormat.id);
|
||||
if (translator) {
|
||||
copyExport.label = Zotero.getString('quickCopy.copyAs', translator.label);
|
||||
}
|
||||
else {
|
||||
copyExport.hidden = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (!(e instanceof Zotero.Exception.UnloadedDataException && e.dataType == 'translators')) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
copyExport.hidden = true;
|
||||
}
|
||||
else {
|
||||
ZoteroPane.copySelectedItemsToClipboard(true, 'bibliography');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Zotero.Prefs = new function () {
|
|||
|
||||
// Process pref version updates
|
||||
var fromVersion = this.get('prefVersion');
|
||||
var toVersion = 23;
|
||||
var toVersion = 24;
|
||||
if (!fromVersion) {
|
||||
this.set('prefVersion', toVersion);
|
||||
}
|
||||
|
|
@ -261,6 +261,21 @@ Zotero.Prefs = new function () {
|
|||
this.clear('export.quickCopy.locale');
|
||||
}
|
||||
break;
|
||||
|
||||
case 24:
|
||||
// Separate Cmd+Shift+A shortcut to copy citation is removed - now handled with Cmd/Ctrl + C
|
||||
this.clear('keys.copySelectedItemCitationsToClipboard');
|
||||
// Rename copySelectedItemsToClipboard to copyAsBibliography for clarity
|
||||
if (this.prefHasUserValue("keys.copySelectedItemsToClipboard")) {
|
||||
this.set("keys.copyAsBibliography", this.get("keys.copySelectedItemsToClipboard"));
|
||||
this.clear("keys.copySelectedItemsToClipboard");
|
||||
}
|
||||
// Rename copySelectedItemsToClipboardAsExport to copyAsExport for consistency with the above
|
||||
if (this.prefHasUserValue("keys.copySelectedItemsToClipboardAsExport")) {
|
||||
this.set("keys.copyAsExport", this.get("keys.copySelectedItemsToClipboardAsExport"));
|
||||
this.clear("keys.copySelectedItemsToClipboardAsExport");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.set('prefVersion', toVersion);
|
||||
|
|
|
|||
|
|
@ -110,6 +110,40 @@ class ReaderInstance {
|
|||
return state ? JSON.parse(JSON.stringify(state)) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys of the annotations currently selected in the reader.
|
||||
*
|
||||
* @returns {String[]}
|
||||
*/
|
||||
getSelectedAnnotationIDs() {
|
||||
let ids = this._internalReader?._state?.selectedAnnotationIDs;
|
||||
return ids ? Array.from(ids) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* What is currently selected in the reader's view(s), used to predict what
|
||||
* a copy action will produce so the Edit menu and Cmd+C handler can label
|
||||
* and behave accordingly.
|
||||
*
|
||||
* @returns {'annotation' | 'text' | null}
|
||||
*/
|
||||
getSelectionType() {
|
||||
if (!this._internalReader) return null;
|
||||
|
||||
if (this.getSelectedAnnotationIDs().length > 0) {
|
||||
return 'annotation';
|
||||
}
|
||||
|
||||
let view = this._internalReader._lastViewPrimary ? this._internalReader._primaryView : this._internalReader._secondaryView;
|
||||
if (!view || !view._iframeWindow) return null;
|
||||
let sel = view._iframeWindow.getSelection();
|
||||
if (sel && !sel.isCollapsed && String(sel).length > 0) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async migrateMendeleyColors(libraryID, annotations) {
|
||||
let colorMap = new Map();
|
||||
colorMap.set('#fff5ad', '#ffd400');
|
||||
|
|
|
|||
|
|
@ -3425,7 +3425,11 @@ Zotero.Utilities.Internal.onDragItems = function (event, itemIDs, dragImage = ev
|
|||
// Get Quick Copy format for current URL (set via /ping from connector)
|
||||
// and the items being dragged. getContentFromItems handles the rest
|
||||
// (annotation wrapping, note-citation reformatting, regular-item filter).
|
||||
let format = Zotero.QuickCopy.getFormat({ items });
|
||||
// Shift-Drag means the user wants a citation cluster, so use bibliography
|
||||
// mode then regardless of the pref
|
||||
let format = event.shiftKey
|
||||
? Zotero.QuickCopy.getFormat({ mode: 'bibliography', items })
|
||||
: Zotero.QuickCopy.getFormat({ items });
|
||||
|
||||
Zotero.debug("Dragging with format " + JSON.stringify(format));
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1361,11 +1361,10 @@ var ZoteroPane = new function () {
|
|||
break;
|
||||
|
||||
// Handled by <key>s in standalone.js, pointing to <command>s in zoteroPane.xul,
|
||||
// which are enabled or disabled by this.updateQuickCopyCommands(), called by
|
||||
// which are enabled or disabled by this.updateQuickCopyMenu(), called by
|
||||
// this.itemSelected()
|
||||
case 'copySelectedItemCitationsToClipboard':
|
||||
case 'copySelectedItemsToClipboard':
|
||||
case 'copySelectedItemsToClipboardAsExport':
|
||||
case 'copyAsBibliography':
|
||||
case 'copyAsExport':
|
||||
return;
|
||||
|
||||
default:
|
||||
|
|
@ -1901,10 +1900,9 @@ var ZoteroPane = new function () {
|
|||
this.itemPane.editable = this.collectionsView.editable;
|
||||
this.itemPane.updateItemPaneButtons(selectedItems);
|
||||
|
||||
// Tab selection observer in standalone.js makes sure that
|
||||
// updateQuickCopyCommands is called
|
||||
// Keep the copy command + menu state current as the library selection changes
|
||||
if (Zotero_Tabs.selectedType == 'library') {
|
||||
this.updateQuickCopyCommands(selectedItems);
|
||||
this.updateQuickCopyMenu();
|
||||
}
|
||||
|
||||
// Check if selection has actually changed. The onselect event that calls this
|
||||
|
|
@ -1996,29 +1994,65 @@ var ZoteroPane = new function () {
|
|||
};
|
||||
|
||||
/**
|
||||
* Update the <command> elements that control the shortcut keys and the enabled state of the
|
||||
* "Copy Citation"/"Copy Bibliography"/"Copy as"/"Copy Note" menu options. When disabled, the shortcuts are
|
||||
* still caught in handleKeyPress so that we can show an alert about not having references selected.
|
||||
* Refresh the Edit-menu copy entries to match the current context.
|
||||
* Sets `menu_copy` item's label and disabled state (Cmd/Ctrl+C), as well as
|
||||
* Copy Bibliography and Export as ... disabled state.
|
||||
*/
|
||||
this.updateQuickCopyCommands = function (selectedItems) {
|
||||
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)) {
|
||||
canCopyBibliography = true;
|
||||
canCopyExport = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
canCopyBibliography = selectedItems.some(item => item.isRegularItem() || item.isAnnotation());
|
||||
canCopyExport = selectedItems.length > 0;
|
||||
this.updateQuickCopyMenu = function () {
|
||||
let selected = this.getSelectedItems();
|
||||
|
||||
// Consolidated Copy entry: reflect what Cmd+C would do in this context
|
||||
let copyItem = document.getElementById('menu_copy');
|
||||
let action = this.classifyCopyContext();
|
||||
copyItem.disabled = false;
|
||||
let count = selected.length;
|
||||
document.l10n.setAttributes(copyItem, `menu-edit-copy-${action}`, { count });
|
||||
// Default Copy -- mirror cmd_copy's enabled state
|
||||
if (action === "text") {
|
||||
copyItem.disabled = document.getElementById('cmd_copy').hasAttribute('disabled');
|
||||
}
|
||||
|
||||
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);
|
||||
// Copy Bibliography / Copy as ...:
|
||||
// - Library tab: only available when the selection contains regular items.
|
||||
// - Reader tab: available when the open item has a parent regular item
|
||||
// to act on.
|
||||
let hideBibAndExport;
|
||||
if (Zotero_Tabs.selectedType === 'reader') {
|
||||
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
||||
let hasMetadata = Zotero.Items.get(reader.itemID).parentItem;
|
||||
hideBibAndExport = !hasMetadata;
|
||||
}
|
||||
else {
|
||||
let exportingNotes = selected.every(item => item.isNote() || item.isAttachment());
|
||||
let exportingAnnotations = selected.every(item => item.isAnnotation());
|
||||
hideBibAndExport = !selected.length || exportingNotes || exportingAnnotations;
|
||||
}
|
||||
let copyBibliography = document.getElementById('menu_copyBibliography');
|
||||
let copyExport = document.getElementById('menu_copyExport');
|
||||
copyBibliography.hidden = hideBibAndExport;
|
||||
copyExport.hidden = hideBibAndExport;
|
||||
document.getElementById('cmd_zotero_copyBibliography').setAttribute('disabled', hideBibAndExport);
|
||||
document.getElementById('cmd_zotero_copyExport').setAttribute('disabled', hideBibAndExport);
|
||||
|
||||
// Label "Copy as ..." with the selected export translator's name
|
||||
let exportFormat = Zotero.QuickCopy.getFormat({ mode: 'export' });
|
||||
if (exportFormat.id) {
|
||||
try {
|
||||
let translator = Zotero.Translators.get(exportFormat.id);
|
||||
if (translator) {
|
||||
copyExport.label = Zotero.getString('quickCopy.copyAs', translator.label);
|
||||
}
|
||||
else {
|
||||
copyExport.hidden = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (!(e instanceof Zotero.Exception.UnloadedDataException && e.dataType == 'translators')) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
copyExport.hidden = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -2698,26 +2732,95 @@ var ZoteroPane = new function () {
|
|||
|
||||
|
||||
this.copySelectedItemsToClipboard = function (asCitations, mode = 'bibliography') {
|
||||
let itemIDs = this.getSelectedItems(true);
|
||||
// Get selected item IDs in the item tree order
|
||||
itemIDs = this.getSortedItems(true).filter(id => itemIDs.includes(id));
|
||||
let items = Zotero.Items.get(itemIDs);
|
||||
let items;
|
||||
// "Copy Bibliography" / "Copy as ..." in the reader tab always act
|
||||
// on the parent item being read -- annotation selection is for the
|
||||
// smart-copy (Cmd+C / asCitations=true) path only.
|
||||
if (!asCitations && Zotero_Tabs.selectedType === 'reader') {
|
||||
let reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID);
|
||||
let item = Zotero.Items.get(reader.itemID);
|
||||
if (item.parentItem) item = item.parentItem;
|
||||
items = [item];
|
||||
}
|
||||
else {
|
||||
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;
|
||||
|
||||
let format = Zotero.QuickCopy.getFormat({ mode, items });
|
||||
|
||||
// asCitations only applies to bibliography mode
|
||||
if (asCitations && format.mode !== 'bibliography') return;
|
||||
|
||||
let content = Zotero.QuickCopy.getContentFromItems(items, format, { asCitations });
|
||||
// asCitations only applies to bibliography mode; for notes/annotations
|
||||
// the note format is used and asCitations is ignored
|
||||
let content = Zotero.QuickCopy.getContentFromItems(items, format, {
|
||||
asCitations: asCitations && format.mode === 'bibliography'
|
||||
});
|
||||
if (!content) {
|
||||
Services.prompt.alert(null, "", Zotero.getString("fileInterface.noReferencesError"));
|
||||
return;
|
||||
}
|
||||
// When copying annotations, also set the zotero/annotation flavor so
|
||||
// the note editor's paste handler can render them with linked
|
||||
// citations (matches the rich path used by drag-and-drop from the
|
||||
// reader). Shape mirrors reader.js _getAnnotation -- the note
|
||||
// editor's paste plugin expects `id` (not `key`) and normalized tags.
|
||||
if (items.every(item => item.isAnnotation())) {
|
||||
content.annotations = JSON.stringify(items.map((annotation) => {
|
||||
let json = Zotero.Annotations.toJSONSync(annotation);
|
||||
json.id = annotation.key;
|
||||
delete json.key;
|
||||
json.attachmentItemID = annotation.parentItemID;
|
||||
json.tags = json.tags || [];
|
||||
return json;
|
||||
}));
|
||||
}
|
||||
Zotero_File_Interface.writeToClipboard(content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Classify what Cmd/Ctrl+C in the current context should do
|
||||
*
|
||||
* @returns {'text' | 'citation' | 'note' | 'annotation'}
|
||||
* - 'citation' if we should quick-copy as an in-text citation
|
||||
* - 'note'/'annotation' if we should quick-copy via the note format
|
||||
* - 'text' - default, copy should copy text
|
||||
*/
|
||||
this.classifyCopyContext = function () {
|
||||
let tab = Zotero_Tabs.getTabInfo();
|
||||
|
||||
if (tab.type === 'reader') {
|
||||
let reader = Zotero.Reader.getByTabID(tab.id);
|
||||
// Quick-copy only applies when the reader is focused
|
||||
if (!reader?._iframeWindow?.document.hasFocus()) return 'text';
|
||||
let hasMetadata = Zotero.Items.get(reader.itemID).parentItem;
|
||||
let defaultMode = hasMetadata ? "citation" : "text";
|
||||
// if nothing is selected in the reader, Copy should copy the citation if top level item exists
|
||||
return reader.getSelectionType() || defaultMode;
|
||||
}
|
||||
|
||||
if (tab.type === 'library') {
|
||||
// Quick-copy only applies when the items tree owns focus
|
||||
if (!this.itemsView.domEl.contains(document.activeElement)) {
|
||||
return 'text';
|
||||
}
|
||||
let items = this.getSelectedItems();
|
||||
if (!items.length) return 'text';
|
||||
if (items.every(item => item.isAnnotation())) return 'annotation';
|
||||
if (items.every(item => item.isNote() || item.isAttachment())) {
|
||||
return items.some(item => item.note) ? 'note' : 'text';
|
||||
}
|
||||
if (items.some(item => item.isRegularItem() || item.isAnnotation())) {
|
||||
return 'citation';
|
||||
}
|
||||
}
|
||||
|
||||
return 'text';
|
||||
};
|
||||
|
||||
|
||||
this.clearQuicksearch = async function (skipSearchRun) {
|
||||
var search = document.getElementById('zotero-tb-search');
|
||||
if (search.searchTextbox.value !== '') {
|
||||
|
|
@ -3089,6 +3192,8 @@ var ZoteroPane = new function () {
|
|||
* Return an array of Item objects for selected items
|
||||
*
|
||||
* If asIDs is true, return an array of itemIDs instead
|
||||
*
|
||||
* For the reader tab, returns the selected annotation item(s) in sidebar, if any.
|
||||
*/
|
||||
this.getSelectedItems = function (asIDs) {
|
||||
switch (Zotero_Tabs.selectedType) {
|
||||
|
|
@ -3104,6 +3209,13 @@ var ZoteroPane = new function () {
|
|||
if (item.parentItem) {
|
||||
item = item.parentItem;
|
||||
}
|
||||
// Return annotations selected in reader's sidebar if any
|
||||
let selectedAnnotations = reader.getSelectedAnnotationIDs()
|
||||
.map(id => Zotero.Items.getByLibraryAndKey(item.libraryID, id))
|
||||
.filter(Boolean);
|
||||
if (selectedAnnotations.length) {
|
||||
return asIDs ? selectedAnnotations.map(item => item.id) : selectedAnnotations;
|
||||
}
|
||||
return asIDs ? [item.id] : [item];
|
||||
}
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -103,18 +103,12 @@
|
|||
<command id="cmd_zotero_importFromClipboard" oncommand="Zotero_File_Interface.importFromClipboard();"/>
|
||||
<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, 'bibliography');"
|
||||
disabled="true"/>
|
||||
<command id="cmd_zotero_copyBibliography"
|
||||
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();"
|
||||
disabled="true"/>
|
||||
<command id="cmd_zotero_createTimeline" oncommand="Zotero_Timeline_Interface.loadTimeline();"/>
|
||||
<command id="cmd_zotero_rtfScan" oncommand="window.openDialog('chrome://zotero/content/rtfScan.xhtml', 'rtfScan', 'chrome,centerscreen')"/>
|
||||
<command id="cmd_zotero_newCollection" oncommand="ZoteroPane_Local.newCollection(ZoteroPane_Local.getSelectedCollection()?.key)"/>
|
||||
|
|
@ -169,9 +163,13 @@
|
|||
key="&importCmd.key;"
|
||||
command="cmd_zotero_importFromClipboard"
|
||||
modifiers="accel shift alt"/>
|
||||
<key id="key_copyCitation"
|
||||
command="cmd_zotero_copyCitation"
|
||||
modifiers="accel shift"/>
|
||||
<!-- Use inline `oncommand`, not a shared <command>: routing through a
|
||||
<command> consumes the keypress so the reader iframe never gets
|
||||
its native copy event. menu_copy duplicates the handler call. -->
|
||||
<key id="key_smartCopy"
|
||||
key="C"
|
||||
modifiers="accel"
|
||||
oncommand="ZoteroStandalone.handleCopyMenuCommand();"/>
|
||||
<key id="key_copyBibliography"
|
||||
command="cmd_zotero_copyBibliography"
|
||||
modifiers="accel shift"/>
|
||||
|
|
@ -376,13 +374,9 @@
|
|||
key="key_cut"
|
||||
command="cmd_cut" data-l10n-id="text-action-cut"/>
|
||||
<menuitem id="menu_copy"
|
||||
key="key_copy"
|
||||
command="cmd_copy" data-l10n-id="text-action-copy"/>
|
||||
<menuitem id="menu_copyCitation"
|
||||
label="©CitationCmd.label;"
|
||||
command="cmd_zotero_copyCitation"
|
||||
key="key_copyCitation"
|
||||
hidden="true"/>
|
||||
key="key_smartCopy"
|
||||
oncommand="ZoteroStandalone.handleCopyMenuCommand();"
|
||||
data-l10n-id="menu-edit-copy-text"/>
|
||||
<menuitem id="menu_copyBibliography"
|
||||
label="©BibliographyCmd.label;"
|
||||
command="cmd_zotero_copyBibliography"
|
||||
|
|
@ -393,16 +387,6 @@
|
|||
key="key_copyExport"
|
||||
command="cmd_zotero_copyExport"
|
||||
hidden="true"/>
|
||||
<menuitem id="menu_copyNote"
|
||||
label="©NoteCmd.label;"
|
||||
command="cmd_zotero_copyBibliography"
|
||||
key="key_copyBibliography"
|
||||
hidden="true"/>
|
||||
<menuitem id="menu_copyAnnotation"
|
||||
data-l10n-id="menu-edit-copy-annotation"
|
||||
command="cmd_zotero_copyAnnotation"
|
||||
key="key_copyBibliography"
|
||||
hidden="true"/>
|
||||
<menuitem id="menu_paste"
|
||||
key="key_paste"
|
||||
command="cmd_paste" data-l10n-id="text-action-paste"/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
preferences-window =
|
||||
.title = { -app-name } Settings
|
||||
|
||||
preferences-keys-copy-as-bibliography = Copy as Bibliography
|
||||
preferences-keys-copy-as-export = Copy in Export Format
|
||||
|
||||
preferences-appearance-title = Appearance and Language
|
||||
|
||||
preferences-auto-recognize-files =
|
||||
|
|
@ -49,6 +52,9 @@ preferences-locate-library-lookup-intro = Library Lookup can find a resource onl
|
|||
preferences-locate-resolver = Resolver:
|
||||
preferences-locate-base-url = Base URL:
|
||||
|
||||
preferences-quickCopy-instructions = Quick Copy allows you to quickly export items in a given format. You can copy selected items to the clipboard in the selected bibliography style or export format by pressing { $bibShortcut } or { $exportShortcut } respectively. You can also drag items directly into a text box in another program.
|
||||
preferences-quickCopy-citationInstructions = You can also copy citations or footnotes in selected citation style by pressing { $citationShortcut } or holding down Shift before dragging items.
|
||||
|
||||
preferences-quickCopy-bibliographyFormat = Bibliography Style:
|
||||
preferences-quickCopy-exportFormat = Export Format:
|
||||
preferences-quickCopy-preferredFormatOnDrag = Format to use on drag:
|
||||
|
|
|
|||
|
|
@ -150,11 +150,20 @@ menu-view-note-tab-font-size =
|
|||
menu-show-tabs-menu =
|
||||
.label = Show Tabs Menu
|
||||
|
||||
menu-edit-copy-citation =
|
||||
.label = Copy Citation
|
||||
menu-edit-copy-note =
|
||||
.label = { $count ->
|
||||
[one] Copy Note
|
||||
*[other] Copy { $count } Notes
|
||||
}
|
||||
menu-edit-copy-annotation =
|
||||
.label = { $count ->
|
||||
[one] Copy Annotation
|
||||
*[other] Copy { $count } Annotations
|
||||
*[other] Copy { $count } Annotations
|
||||
}
|
||||
menu-edit-copy-text =
|
||||
.label = Copy
|
||||
|
||||
main-window-command =
|
||||
.label = Library
|
||||
|
|
|
|||
|
|
@ -729,8 +729,6 @@ zotero.preferences.search.clearNonLinkedURLs = Clear All Except Web Links
|
|||
zotero.preferences.search.indexUnindexed = Index Unindexed Items
|
||||
zotero.preferences.export.quickCopy.citationStyles = Citation Styles
|
||||
zotero.preferences.export.quickCopy.exportFormats = Export Formats
|
||||
zotero.preferences.export.quickCopy.instructions = Quick Copy allows you to quickly export items in a given format. You can copy selected items to the clipboard by pressing %S or drag items directly into a text box in another program.
|
||||
zotero.preferences.export.quickCopy.citationInstructions = For citation styles, you can copy citations or footnotes by pressing %S or holding down Shift before dragging items.
|
||||
zotero.preferences.export.quickCopy.note.htmlOptions.label = Rich Text/HTML
|
||||
|
||||
zotero.preferences.wordProcessors.installationSuccess = Installation was successful.
|
||||
|
|
|
|||
|
|
@ -99,9 +99,8 @@ pref("extensions.zotero.keys.newItem", "N");
|
|||
pref("extensions.zotero.keys.newNote", "O");
|
||||
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.copyAsBibliography", "C");
|
||||
pref("extensions.zotero.keys.copyAsExport", "E");
|
||||
pref("extensions.zotero.keys.sync", "Y");
|
||||
pref("extensions.zotero.keys.toggleAllRead", "R");
|
||||
pref("extensions.zotero.keys.toggleRead", "`");
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ describe("Zotero_File_Interface", function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#exportItemsToClipboard()", function () {
|
||||
describe("Copy citations to clipboard", function () {
|
||||
var noteItem, itemOne, itemTwo;
|
||||
var styleID = 'http://www.zotero.org/styles/chicago-notes-bibliography';
|
||||
|
||||
|
|
|
|||
|
|
@ -349,4 +349,153 @@ describe("Zotero.QuickCopy", function () {
|
|||
let content = Zotero.QuickCopy.getContentFromItems([item], format, { asCitations: true });
|
||||
assert.equal(content.text, '《新型数据财产的行为主义保护:基于财产权理论的分析》。');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
describe("Smart copy", function () {
|
||||
var win, zp, doc;
|
||||
var regularItem, attachment, annotation, note;
|
||||
var clipboardService;
|
||||
const SMART_COPY_BIB = {
|
||||
mode: 'bibliography',
|
||||
id: 'http://www.zotero.org/styles/apa',
|
||||
contentType: '',
|
||||
locale: ''
|
||||
};
|
||||
|
||||
before(async function () {
|
||||
win = await loadZoteroPane();
|
||||
zp = win.ZoteroPane;
|
||||
doc = win.document;
|
||||
clipboardService = Components.classes["@mozilla.org/widget/clipboard;1"]
|
||||
.getService(Components.interfaces.nsIClipboard);
|
||||
|
||||
await Zotero.Styles.init();
|
||||
await Zotero.Translators.init();
|
||||
|
||||
// Preload Note Markdown / Note HTML translators
|
||||
for (let id of [
|
||||
Zotero.Translators.TRANSLATOR_ID_NOTE_MARKDOWN,
|
||||
Zotero.Translators.TRANSLATOR_ID_NOTE_HTML,
|
||||
]) {
|
||||
let translator = Zotero.Translators.get(id);
|
||||
translator.cacheCode = true;
|
||||
await Zotero.Translators.getCodeForTranslator(translator);
|
||||
}
|
||||
|
||||
regularItem = createUnsavedDataObject('item', {
|
||||
itemType: 'journalArticle',
|
||||
title: 'My Smart Copy Paper'
|
||||
});
|
||||
regularItem.setField('date', '2020');
|
||||
regularItem.setCreators([
|
||||
{ firstName: 'Jane', lastName: 'Smartcopy', creatorType: 'author' }
|
||||
]);
|
||||
await regularItem.saveTx();
|
||||
|
||||
attachment = await importFileAttachment('test.pdf', { parentItemID: regularItem.id });
|
||||
annotation = await createAnnotation('highlight', attachment);
|
||||
|
||||
note = createUnsavedDataObject('item', { itemType: 'note' });
|
||||
note.setNote('<p>Test note content</p>');
|
||||
await note.saveTx();
|
||||
});
|
||||
|
||||
// The test runner has a root-level afterEach (test/content/runtests.js)
|
||||
// that clears any user-set pref after every test. Re-pin the bibliography
|
||||
// style here before each test.
|
||||
beforeEach(function () {
|
||||
Zotero.Prefs.set('export.quickCopy.bibliographySetting', JSON.stringify(SMART_COPY_BIB));
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
win.Zotero_Tabs.closeAll();
|
||||
win.close();
|
||||
});
|
||||
|
||||
function getClipboardText() {
|
||||
let transferable = Components.classes["@mozilla.org/widget/transferable;1"]
|
||||
.createInstance(Components.interfaces.nsITransferable);
|
||||
transferable.init(null);
|
||||
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;
|
||||
}
|
||||
|
||||
function clearClipboard() {
|
||||
clipboardService.emptyClipboard(Components.interfaces.nsIClipboard.kGlobalClipboard);
|
||||
}
|
||||
|
||||
describe("Library tab", function () {
|
||||
beforeEach(async function () {
|
||||
await selectLibrary(win);
|
||||
doc.getElementById('item-tree-main').focus();
|
||||
});
|
||||
|
||||
it("should copy citation for a regular item", async function () {
|
||||
await zp.itemsView.selectItem(regularItem.id);
|
||||
clearClipboard();
|
||||
doc.getElementById('key_smartCopy').doCommand();
|
||||
assert.equal(getClipboardText(), '(Smartcopy, 2020)');
|
||||
});
|
||||
|
||||
it("should copy annotation content when an annotation is selected", async function () {
|
||||
await zp.itemsView.selectItem(annotation.id);
|
||||
clearClipboard();
|
||||
doc.getElementById('key_smartCopy').doCommand();
|
||||
assert.include(getClipboardText(), annotation.annotationText);
|
||||
});
|
||||
|
||||
it("should copy note content when a note is selected", async function () {
|
||||
await zp.itemsView.selectItem(note.id);
|
||||
clearClipboard();
|
||||
doc.getElementById('key_smartCopy').doCommand();
|
||||
assert.include(getClipboardText(), 'Test note content');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Reader tab", function () {
|
||||
var reader;
|
||||
var hasFocusStub;
|
||||
|
||||
before(async function () {
|
||||
reader = await Zotero.Reader.open(attachment.id);
|
||||
await reader._initPromise;
|
||||
await reader._internalReader._primaryView.initializedPromise;
|
||||
while (!reader._iframeWindow) {
|
||||
await Zotero.Promise.delay(50);
|
||||
}
|
||||
// Pretend that the reader is focused
|
||||
hasFocusStub = sinon.stub(reader._iframeWindow.document, 'hasFocus').returns(true);
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await win.Zotero_Tabs.select(reader.tabID);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
hasFocusStub.restore();
|
||||
});
|
||||
|
||||
it("should copy parent citation when nothing is selected in the reader", function () {
|
||||
clearClipboard();
|
||||
doc.getElementById('key_smartCopy').doCommand();
|
||||
assert.equal(getClipboardText(), '(Smartcopy, 2020)');
|
||||
});
|
||||
|
||||
it("should copy selected annotation", function () {
|
||||
let stub = sinon.stub(reader, 'getSelectedAnnotationIDs').returns([annotation.key]);
|
||||
try {
|
||||
clearClipboard();
|
||||
doc.getElementById('key_smartCopy').doCommand();
|
||||
assert.include(getClipboardText(), annotation.annotationText);
|
||||
}
|
||||
finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue