fix: revert keyboard.js to working hover-based actions, add minimal Outlook preset key remapping

This commit is contained in:
Joel Brock
2026-04-28 08:00:12 -07:00
parent 4d5be13b51
commit d4d392962a

View File

@@ -1,4 +1,4 @@
// Outcut — Keyboard Navigation & Multi-Select // Outlook Relook — Gmail-Style Keyboard Navigation & Multi-Select
// Adds keyboard focus cursor and multi-select to OWA's message list. // Adds keyboard focus cursor and multi-select to OWA's message list.
// Gated by the keyboardMultiSelect setting. // Gated by the keyboardMultiSelect setting.
// //
@@ -7,53 +7,6 @@
window.OutlookRelook = window.OutlookRelook || {}; window.OutlookRelook = window.OutlookRelook || {};
// Keyboard shortcut presets
var KEY_PRESETS = {
gmail: {
nextMessage: [{key: 'j'}, {key: 'ArrowDown'}],
prevMessage: [{key: 'k'}, {key: 'ArrowUp'}],
selectExtendDown: [{key: 'j', shift: true}, {key: 'ArrowDown', shift: true}],
selectExtendUp: [{key: 'k', shift: true}, {key: 'ArrowUp', shift: true}],
toggleSelect: [{key: 'x'}, {key: ' '}],
delete: [{key: '#'}],
archive: [{key: 'e'}],
readUnread: [{key: 'I', shift: true}, {key: 'U', shift: true}],
move: [{key: 'v'}],
flag: [{key: 'f'}],
pin: [{key: 'p'}],
deselect: [{key: 'Escape'}],
open: [{key: 'Enter'}, {key: 'o'}],
label: 'Gmail-style: j/k nav, x select, # del, e archive'
},
outlook: {
nextMessage: [{key: 'ArrowDown'}, {key: 'j'}],
prevMessage: [{key: 'ArrowUp'}, {key: 'k'}],
selectExtendDown: [{key: 'ArrowDown', shift: true}],
selectExtendUp: [{key: 'ArrowUp', shift: true}],
toggleSelect: [{key: ' '}],
delete: [{key: 'Delete'}, {key: 'Backspace'}],
archive: [{key: 'e'}],
readUnread: [{key: 'q'}, {key: 'I', shift: true}],
move: [{key: 'v'}],
flag: [{key: 'Insert'}, {key: 'f'}],
pin: [{key: 'p'}],
deselect: [{key: 'Escape'}],
open: [{key: 'Enter'}],
label: 'Outlook-style: arrows nav, Space select, Del delete'
}
};
function matchesAction(e, actionBindings) {
if (!actionBindings) return false;
for (var i = 0; i < actionBindings.length; i++) {
var b = actionBindings[i];
if (e.key === b.key && !!e.shiftKey === !!b.shift && !!e.ctrlKey === !!b.ctrl) {
return true;
}
}
return false;
}
window.OutlookRelook.Keyboard = (function () { window.OutlookRelook.Keyboard = (function () {
'use strict'; 'use strict';
@@ -134,6 +87,9 @@ window.OutlookRelook.Keyboard = (function () {
var idx = findItemById(items, id); var idx = findItemById(items, id);
if (idx >= 0) { if (idx >= 0) {
items[idx].classList.add('or-kb-selected'); items[idx].classList.add('or-kb-selected');
console.log('[Outcut] Applied or-kb-selected to', id.substring(0, 20), 'hasClass:', items[idx].classList.contains('or-kb-selected'));
} else {
console.log('[Outcut] Could not find item for selected id:', id.substring(0, 20));
} }
}); });
@@ -176,24 +132,18 @@ window.OutlookRelook.Keyboard = (function () {
updateCountBadge(); updateCountBadge();
} }
// Returns an array of message IDs to act on. function getActionTargets(items) {
// We return IDs (not DOM elements) because OWA re-renders rows after each var targets = [];
// action, invalidating direct element references.
function getActionTargetIds(items) {
var ids = [];
if (selectedIds.size > 0) { if (selectedIds.size > 0) {
selectedIds.forEach(function (id) { ids.push(id); }); selectedIds.forEach(function (id) {
} else {
if (focusedId) ids.push(focusedId);
}
return ids;
}
// Re-resolve an ID to the current DOM element (or null if it's gone).
function resolveTargetById(id) {
var items = getMessageItems();
var idx = findItemById(items, id); var idx = findItemById(items, id);
return idx >= 0 ? items[idx] : null; if (idx >= 0) targets.push(items[idx]);
});
} else {
var fi = getFocusedIndex(items);
if (fi >= 0) targets.push(items[fi]);
}
return targets;
} }
// --- Selection count badge --- // --- Selection count badge ---
@@ -261,128 +211,98 @@ window.OutlookRelook.Keyboard = (function () {
target.dispatchEvent(overEvent); target.dispatchEvent(overEvent);
} }
// Find a button in the global OWA toolbar (not inline on a row). function performAction(targets, actionFn) {
// The toolbar button acts on OWA's currently selected messages. if (targets.length === 0) return;
function findToolbarButton(label) { var i = 0;
var sels = [ function processNext() {
'[role="toolbar"] button[aria-label="' + label + '"]', if (i >= targets.length) {
'.fui-Toolbar button[aria-label="' + label + '"]',
'[aria-label*="Quick actions" i] button[aria-label="' + label + '"]'
];
for (var i = 0; i < sels.length; i++) {
var btn = document.querySelector(sels[i]);
if (btn) return btn;
}
// Fallback: any button with that label that's NOT inside a message row
var all = document.querySelectorAll('button[aria-label="' + label + '"]');
for (var j = 0; j < all.length; j++) {
if (!all[j].closest('[role="option"], [role="listitem"]')) {
return all[j];
}
}
return null;
}
// Ctrl+click a message row to add it to OWA's native multi-selection.
// Uses MouseEvent with ctrlKey + metaKey to handle both Mac and Win.
function ctrlClickRow(target) {
var rect = target.getBoundingClientRect();
var opts = {
bubbles: true, cancelable: true, view: window,
clientX: rect.x + 30, clientY: rect.y + rect.height / 2,
ctrlKey: true, metaKey: true, button: 0
};
target.dispatchEvent(new MouseEvent('mousedown', opts));
target.dispatchEvent(new MouseEvent('mouseup', opts));
target.dispatchEvent(new MouseEvent('click', opts));
}
// Build OWA's native selection by Ctrl+clicking each ID, then run a single
// toolbar action that operates on the entire selection.
function performToolbarAction(ids, toolbarLabel, fallbackContextPattern) {
if (!ids || ids.length === 0) return;
// First: clear any current OWA selection by clicking the first target plain
var first = resolveTargetById(ids[0]);
if (!first) {
clearSelection(); clearSelection();
return; return;
} }
first.click(); var target = targets[i];
i++;
// First, try to find the inline button without clicking the row.
// Hover to make OWA's inline action buttons appear.
triggerHover(target);
setTimeout(function () { setTimeout(function () {
// Now Ctrl+click the rest to add to selection // Try inline button first (avoids opening the email in fill-screen mode)
var i = 1; var inlineHandled = actionFn(target);
function addNext() {
if (i >= ids.length) { // If inline button wasn't found, fall back to clicking the row + toolbar
// All selected in OWA, now click the toolbar action button if (inlineHandled === false) {
target.click();
setTimeout(function () { setTimeout(function () {
var btn = findToolbarButton(toolbarLabel); actionFn(target);
setTimeout(processNext, 200);
}, 150);
} else {
setTimeout(processNext, 200);
}
}, 100);
}
processNext();
}
function actionDelete(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Delete');
if (btn) { btn.click(); return true; }
console.warn('[Outcut] Delete button not found');
return false;
});
}
function actionArchive(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Archive');
if (btn) { btn.click(); return true; }
console.warn('[Outcut] Archive button not found');
return false;
});
}
function actionMarkReadUnread(targets) {
performAction(targets, function (target) {
var btn = findButton(target, 'Read / Unread');
if (btn) { btn.click(); return true; }
triggerContextMenuAction(target, /mark as read|mark as unread|read \/ unread/i);
return true; // context menu handles it
});
}
function actionMove(targets) {
if (targets.length > 0) {
triggerHover(targets[0]);
setTimeout(function () {
var btn = findButton(targets[0], 'Move to');
if (btn) { if (btn) {
btn.click(); btn.click();
} else if (fallbackContextPattern) {
var fresh = resolveTargetById(ids[0]);
if (fresh) triggerContextMenuAction(fresh, fallbackContextPattern);
} else { } else {
console.warn('[Outcut] Toolbar button not found: ' + toolbarLabel); triggerContextMenuAction(targets[0], /move to/i);
} }
setTimeout(clearSelection, 200); }, 150);
}, 100);
return;
} }
var t = resolveTargetById(ids[i]);
i++;
if (t) ctrlClickRow(t);
setTimeout(addNext, 60);
}
addNext();
}, 100);
} }
function actionDelete(ids) { function actionFlag(targets) {
performToolbarAction(ids, 'Delete', /^delete$/i); performAction(targets, function (target) {
var btn = findButton(target, 'Flag this message')
|| findButton(target, 'Flag / Unflag');
if (btn) { btn.click(); return true; }
console.warn('[Outcut] Flag button not found');
return false;
});
} }
function actionArchive(ids) { function actionPin(targets) {
performToolbarAction(ids, 'Archive', /^archive$/i); performAction(targets, function (target) {
} var btn = findButton(target, 'Pin / Unpin');
if (btn) { btn.click(); return true; }
function actionMarkReadUnread(ids) { console.warn('[Outcut] Pin button not found');
performToolbarAction(ids, 'Read / Unread', /mark as read|mark as unread|read \/ unread/i); return false;
} });
function actionMove(ids) {
performToolbarAction(ids, 'Move to', /move to/i);
}
function actionFlag(ids) {
// Try multiple labels
if (!ids || ids.length === 0) return;
var first = resolveTargetById(ids[0]);
if (!first) return;
first.click();
setTimeout(function () {
var i = 1;
function addNext() {
if (i >= ids.length) {
setTimeout(function () {
var btn = findToolbarButton('Flag / Unflag') || findToolbarButton('Flag this message');
if (btn) btn.click();
setTimeout(clearSelection, 200);
}, 100);
return;
}
var t = resolveTargetById(ids[i]);
i++;
if (t) ctrlClickRow(t);
setTimeout(addNext, 60);
}
addNext();
}, 100);
}
function actionPin(ids) {
performToolbarAction(ids, 'Pin / Unpin', /pin/i);
} }
function triggerContextMenuAction(target, pattern) { function triggerContextMenuAction(target, pattern) {
@@ -408,6 +328,18 @@ window.OutlookRelook.Keyboard = (function () {
// --- Key handler --- // --- Key handler ---
// Outlook preset: map Outlook-native keys to the Gmail-style internal keys
// used by the switch below. This way the action code stays untouched.
function remapForOutlookPreset(key, shift) {
// Delete/Backspace → '#' (delete)
if (key === 'Delete' || key === 'Backspace') return { key: '#', shift: shift };
// q → mark read/unread (Outlook native shortcut for read/unread)
if (key === 'q' && !shift) return { key: 'I', shift: true };
// Insert → flag
if (key === 'Insert') return { key: 'f', shift: shift };
return { key: key, shift: shift };
}
function handleKeydown(e) { function handleKeydown(e) {
if (!currentSettings.keyboardMultiSelect) return; if (!currentSettings.keyboardMultiSelect) return;
if (isComposeOrDialogActive()) return; if (isComposeOrDialogActive()) return;
@@ -415,8 +347,15 @@ window.OutlookRelook.Keyboard = (function () {
var items = getMessageItems(); var items = getMessageItems();
if (items.length === 0) return; if (items.length === 0) return;
var presetName = currentSettings.keyboardPreset || 'gmail'; var key = e.key;
var preset = KEY_PRESETS[presetName] || KEY_PRESETS.gmail; var shift = e.shiftKey;
// Apply Outlook preset key remapping if active
if (currentSettings.keyboardPreset === 'outlook') {
var remapped = remapForOutlookPreset(key, shift);
key = remapped.key;
shift = remapped.shift;
}
// Initialize focus if not set // Initialize focus if not set
var currentIdx = getFocusedIndex(items); var currentIdx = getFocusedIndex(items);
@@ -439,54 +378,101 @@ window.OutlookRelook.Keyboard = (function () {
var handled = true; var handled = true;
var targets; var targets;
if (matchesAction(e, preset.selectExtendDown)) { switch (key) {
case 'j':
case 'ArrowDown':
if (shift) {
toggleSelect(items, currentIdx); toggleSelect(items, currentIdx);
if (currentIdx < items.length - 1) { if (currentIdx < items.length - 1) {
setFocus(items, currentIdx + 1); setFocus(items, currentIdx + 1);
toggleSelect(items, currentIdx + 1); toggleSelect(items, currentIdx + 1);
} }
} else if (matchesAction(e, preset.selectExtendUp)) { } else {
if (currentIdx < items.length - 1) {
setFocus(items, currentIdx + 1);
}
}
break;
case 'k':
case 'ArrowUp':
if (shift) {
toggleSelect(items, currentIdx); toggleSelect(items, currentIdx);
if (currentIdx > 0) { if (currentIdx > 0) {
setFocus(items, currentIdx - 1); setFocus(items, currentIdx - 1);
toggleSelect(items, currentIdx - 1); toggleSelect(items, currentIdx - 1);
} }
} else if (matchesAction(e, preset.nextMessage)) { } else {
if (currentIdx < items.length - 1) setFocus(items, currentIdx + 1); if (currentIdx > 0) {
} else if (matchesAction(e, preset.prevMessage)) { setFocus(items, currentIdx - 1);
if (currentIdx > 0) setFocus(items, currentIdx - 1); }
} else if (matchesAction(e, preset.toggleSelect)) { }
break;
case 'x':
case ' ':
e.preventDefault(); e.preventDefault();
toggleSelect(items, currentIdx); toggleSelect(items, currentIdx);
} else if (matchesAction(e, preset.delete)) { break;
targets = getActionTargetIds(items);
case '#':
targets = getActionTargets(items);
actionDelete(targets); actionDelete(targets);
} else if (matchesAction(e, preset.archive)) { break;
targets = getActionTargetIds(items);
case 'e':
targets = getActionTargets(items);
actionArchive(targets); actionArchive(targets);
} else if (matchesAction(e, preset.readUnread)) { break;
targets = getActionTargetIds(items);
case 'I':
case 'U':
// Shift+i or Shift+u — OWA uses a single "Read / Unread" toggle
if (shift) {
targets = getActionTargets(items);
actionMarkReadUnread(targets); actionMarkReadUnread(targets);
} else if (matchesAction(e, preset.move)) {
targets = getActionTargetIds(items);
actionMove(targets);
} else if (matchesAction(e, preset.flag)) {
targets = getActionTargetIds(items);
actionFlag(targets);
} else if (matchesAction(e, preset.pin)) {
targets = getActionTargetIds(items);
actionPin(targets);
} else if (matchesAction(e, preset.deselect)) {
clearSelection();
} else if (matchesAction(e, preset.open)) {
if (currentIdx >= 0 && currentIdx < items.length) items[currentIdx].click();
} else { } else {
handled = false; handled = false;
} }
break;
case 'v':
targets = getActionTargets(items);
actionMove(targets);
break;
case 'f':
// Flag/unflag
targets = getActionTargets(items);
actionFlag(targets);
break;
case 'p':
// Pin/unpin
targets = getActionTargets(items);
actionPin(targets);
break;
case 'Escape':
clearSelection();
break;
case 'Enter':
case 'o':
if (currentIdx >= 0 && currentIdx < items.length) {
items[currentIdx].click();
}
break;
default:
handled = false;
}
if (handled) { if (handled) {
e.stopPropagation(); e.stopPropagation();
if (e.key !== 'Enter') e.preventDefault(); if (key !== 'Enter' && key !== 'o') {
e.preventDefault();
}
} }
} }
@@ -542,11 +528,9 @@ window.OutlookRelook.Keyboard = (function () {
function updateSettings(settings) { function updateSettings(settings) {
var wasEnabled = currentSettings.keyboardMultiSelect; var wasEnabled = currentSettings.keyboardMultiSelect;
var isEnabled = settings.keyboardMultiSelect; var isEnabled = settings.keyboardMultiSelect;
var oldPreset = currentSettings.keyboardPreset;
var newPreset = settings.keyboardPreset;
currentSettings = settings; currentSettings = settings;
if (wasEnabled !== isEnabled || oldPreset !== newPreset) { if (wasEnabled !== isEnabled) {
stop(); stop();
start(settings); start(settings);
} }