Implement macOS Sequoia Ctrl-Enter context menu shortcut (#4731)

This commit is contained in:
Abe Jellinek 2024-10-04 17:41:07 -04:00 committed by Dan Stillman
parent fc7307fd84
commit 23f9ebcd17
2 changed files with 126 additions and 0 deletions

View file

@ -40,6 +40,72 @@ window.addEventListener('DOMContentLoaded', () => {
if (fileQuitItemUnix) fileQuitItemUnix.hidden = true;
if (editPreferencesSeparator) editPreferencesSeparator.hidden = true;
if (editPreferencesItem) editPreferencesItem.hidden = true;
// macOS 15 Sequoia has a new system keyboard shortcut, Ctrl-Enter,
// that shows a context menu on the focused control. Firefox currently
// doesn't handle it very well - it shows a context menu on the element
// in the middle of the window, whatever element that may be.
// Prevent/retarget these events (but not Ctrl-clicks).
let lastPreventedContextMenuTime = 0;
document.addEventListener('contextmenu', (event) => {
if (!(event.button === 2 && event.buttons === 0 && !event.ctrlKey)) {
return;
}
event.stopPropagation();
event.stopImmediatePropagation();
event.preventDefault();
// We usually get three of these in a row - only act on the first
if (event.timeStamp - lastPreventedContextMenuTime < 50) {
return;
}
lastPreventedContextMenuTime = event.timeStamp;
let targetElement = document.activeElement;
if (!targetElement) {
return;
}
if (targetElement.hasAttribute('aria-activedescendant')) {
let activeDescendant = targetElement.querySelector(
'#' + CSS.escape(targetElement.getAttribute('aria-activedescendant'))
);
if (activeDescendant) {
targetElement = activeDescendant;
}
}
let [clientX, clientY] = Zotero.Utilities.Internal.getContextMenuPosition(targetElement);
let screenX = window.mozInnerScreenX + clientX;
let screenY = window.mozInnerScreenY + clientY;
// Run in the next tick, because otherwise our rate-limiting above
// prevents this from working on form fields (somehow)
setTimeout(() => {
targetElement.dispatchEvent(new PointerEvent('contextmenu', {
bubbles: true,
cancelable: true,
button: 2,
buttons: 2,
clientX,
clientY,
layerX: clientX, // Wrong, but nobody should ever use these
layerY: clientY,
screenX,
screenY,
}));
});
}, { capture: true });
// Make sure the Ctrl-Enter isn't handled by listeners further down in
// the tree as a regular Enter
document.documentElement.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 'Enter') {
event.stopPropagation();
event.stopImmediatePropagation();
event.preventDefault();
}
}, { capture: true });
}
else {
// Set behavior on all non-macOS platforms

View file

@ -2482,6 +2482,66 @@ Zotero.Utilities.Internal = {
}
return textContent;
},
/**
*
* @param {Element} targetElement
* @returns {[number, number]} clientX and clientY
*/
getContextMenuPosition(targetElement) {
let selection;
if (targetElement.editor?.selection) {
selection = targetElement.editor.selection;
if (!selection.rangeCount) {
selection = null;
}
}
else {
selection = targetElement.ownerDocument.getSelection();
if (!selection.rangeCount || !targetElement.contains(selection.getRangeAt(0).startContainer)) {
selection = null;
}
}
let rect;
let anchorToBottom;
let anchorToEnd;
if (selection) {
let range = selection.getRangeAt(0);
if (range.getClientRects().length) {
rect = range.getBoundingClientRect();
}
// If the selection is between lines in an editor, it'll be
// inside the editor's native anonymous text node and won't
// have any rects for some reason.
// If that's the case, use the text node's bounds.
else if (range.startContainer === range.endContainer && range.startContainer.isNativeAnonymous
&& range.startContainer.firstChild?.nodeType === Node.TEXT_NODE) {
let quads = range.startContainer.firstChild.getBoxQuads();
rect = quads[quads.length - 1].getBounds();
}
else {
rect = range.commonAncestorContainer.getBoundingClientRect();
}
anchorToBottom = !range.collapsed;
anchorToEnd = range.collapsed;
}
else {
rect = targetElement.getBoundingClientRect();
anchorToBottom = true;
anchorToEnd = false;
}
let clientX;
if (Zotero.rtl) {
clientX = rect.x + (anchorToEnd ? 0 : rect.width - 3);
}
else {
clientX = rect.x + (anchorToEnd ? rect.width + 3 : 0);
}
let clientY = rect.y + (anchorToBottom ? rect.height + 8 : 5);
return [clientX, clientY];
}
};