Implement macOS Sequoia Ctrl-Enter context menu shortcut (#4731)
This commit is contained in:
parent
fc7307fd84
commit
23f9ebcd17
2 changed files with 126 additions and 0 deletions
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue