From d4d392962a12e4affd45a73eb60de5fd7c603090 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Tue, 28 Apr 2026 08:00:12 -0700 Subject: [PATCH] fix: revert keyboard.js to working hover-based actions, add minimal Outlook preset key remapping --- content/keyboard.js | 418 +++++++++++++++++++++----------------------- 1 file changed, 201 insertions(+), 217 deletions(-) diff --git a/content/keyboard.js b/content/keyboard.js index 0be58d1..4d492d0 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -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. // Gated by the keyboardMultiSelect setting. // @@ -7,53 +7,6 @@ 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 () { 'use strict'; @@ -134,6 +87,9 @@ window.OutlookRelook.Keyboard = (function () { var idx = findItemById(items, id); if (idx >= 0) { 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(); } - // Returns an array of message IDs to act on. - // We return IDs (not DOM elements) because OWA re-renders rows after each - // action, invalidating direct element references. - function getActionTargetIds(items) { - var ids = []; + function getActionTargets(items) { + var targets = []; if (selectedIds.size > 0) { - selectedIds.forEach(function (id) { ids.push(id); }); + selectedIds.forEach(function (id) { + var idx = findItemById(items, id); + if (idx >= 0) targets.push(items[idx]); + }); } else { - if (focusedId) ids.push(focusedId); + var fi = getFocusedIndex(items); + if (fi >= 0) targets.push(items[fi]); } - 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); - return idx >= 0 ? items[idx] : null; + return targets; } // --- Selection count badge --- @@ -261,128 +211,98 @@ window.OutlookRelook.Keyboard = (function () { target.dispatchEvent(overEvent); } - // Find a button in the global OWA toolbar (not inline on a row). - // The toolbar button acts on OWA's currently selected messages. - function findToolbarButton(label) { - var sels = [ - '[role="toolbar"] button[aria-label="' + label + '"]', - '.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]; + function performAction(targets, actionFn) { + if (targets.length === 0) return; + var i = 0; + function processNext() { + if (i >= targets.length) { + clearSelection(); + return; } - } - return null; - } + var target = targets[i]; + i++; - // 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)); - } + // First, try to find the inline button without clicking the row. + // Hover to make OWA's inline action buttons appear. + triggerHover(target); - // 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; + setTimeout(function () { + // Try inline button first (avoids opening the email in fill-screen mode) + var inlineHandled = actionFn(target); - // First: clear any current OWA selection by clicking the first target plain - var first = resolveTargetById(ids[0]); - if (!first) { - clearSelection(); - return; - } - first.click(); - - setTimeout(function () { - // Now Ctrl+click the rest to add to selection - var i = 1; - function addNext() { - if (i >= ids.length) { - // All selected in OWA, now click the toolbar action button + // If inline button wasn't found, fall back to clicking the row + toolbar + if (inlineHandled === false) { + target.click(); setTimeout(function () { - var btn = findToolbarButton(toolbarLabel); - if (btn) { - btn.click(); - } else if (fallbackContextPattern) { - var fresh = resolveTargetById(ids[0]); - if (fresh) triggerContextMenuAction(fresh, fallbackContextPattern); - } else { - console.warn('[Outcut] Toolbar button not found: ' + toolbarLabel); - } - setTimeout(clearSelection, 200); - }, 100); - return; + actionFn(target); + setTimeout(processNext, 200); + }, 150); + } else { + setTimeout(processNext, 200); } - var t = resolveTargetById(ids[i]); - i++; - if (t) ctrlClickRow(t); - setTimeout(addNext, 60); - } - addNext(); - }, 100); + }, 100); + } + processNext(); } - function actionDelete(ids) { - performToolbarAction(ids, 'Delete', /^delete$/i); + 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(ids) { - performToolbarAction(ids, 'Archive', /^archive$/i); + 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(ids) { - performToolbarAction(ids, 'Read / Unread', /mark as read|mark as unread|read \/ unread/i); + 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(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; + function actionMove(targets) { + if (targets.length > 0) { + triggerHover(targets[0]); + setTimeout(function () { + var btn = findButton(targets[0], 'Move to'); + if (btn) { + btn.click(); + } else { + triggerContextMenuAction(targets[0], /move to/i); } - var t = resolveTargetById(ids[i]); - i++; - if (t) ctrlClickRow(t); - setTimeout(addNext, 60); - } - addNext(); - }, 100); + }, 150); + } } - function actionPin(ids) { - performToolbarAction(ids, 'Pin / Unpin', /pin/i); + function actionFlag(targets) { + 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 actionPin(targets) { + performAction(targets, function (target) { + var btn = findButton(target, 'Pin / Unpin'); + if (btn) { btn.click(); return true; } + console.warn('[Outcut] Pin button not found'); + return false; + }); } function triggerContextMenuAction(target, pattern) { @@ -408,6 +328,18 @@ window.OutlookRelook.Keyboard = (function () { // --- 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) { if (!currentSettings.keyboardMultiSelect) return; if (isComposeOrDialogActive()) return; @@ -415,8 +347,15 @@ window.OutlookRelook.Keyboard = (function () { var items = getMessageItems(); if (items.length === 0) return; - var presetName = currentSettings.keyboardPreset || 'gmail'; - var preset = KEY_PRESETS[presetName] || KEY_PRESETS.gmail; + var key = e.key; + 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 var currentIdx = getFocusedIndex(items); @@ -439,54 +378,101 @@ window.OutlookRelook.Keyboard = (function () { var handled = true; var targets; - if (matchesAction(e, preset.selectExtendDown)) { - toggleSelect(items, currentIdx); - if (currentIdx < items.length - 1) { - setFocus(items, currentIdx + 1); - toggleSelect(items, currentIdx + 1); - } - } else if (matchesAction(e, preset.selectExtendUp)) { - toggleSelect(items, currentIdx); - if (currentIdx > 0) { - setFocus(items, currentIdx - 1); - toggleSelect(items, currentIdx - 1); - } - } else if (matchesAction(e, preset.nextMessage)) { - if (currentIdx < items.length - 1) setFocus(items, currentIdx + 1); - } else if (matchesAction(e, preset.prevMessage)) { - if (currentIdx > 0) setFocus(items, currentIdx - 1); - } else if (matchesAction(e, preset.toggleSelect)) { - e.preventDefault(); - toggleSelect(items, currentIdx); - } else if (matchesAction(e, preset.delete)) { - targets = getActionTargetIds(items); - actionDelete(targets); - } else if (matchesAction(e, preset.archive)) { - targets = getActionTargetIds(items); - actionArchive(targets); - } else if (matchesAction(e, preset.readUnread)) { - targets = getActionTargetIds(items); - 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 { - handled = false; + switch (key) { + case 'j': + case 'ArrowDown': + if (shift) { + toggleSelect(items, currentIdx); + if (currentIdx < items.length - 1) { + setFocus(items, currentIdx + 1); + toggleSelect(items, currentIdx + 1); + } + } else { + if (currentIdx < items.length - 1) { + setFocus(items, currentIdx + 1); + } + } + break; + + case 'k': + case 'ArrowUp': + if (shift) { + toggleSelect(items, currentIdx); + if (currentIdx > 0) { + setFocus(items, currentIdx - 1); + toggleSelect(items, currentIdx - 1); + } + } else { + if (currentIdx > 0) { + setFocus(items, currentIdx - 1); + } + } + break; + + case 'x': + case ' ': + e.preventDefault(); + toggleSelect(items, currentIdx); + break; + + case '#': + targets = getActionTargets(items); + actionDelete(targets); + break; + + case 'e': + targets = getActionTargets(items); + actionArchive(targets); + break; + + case 'I': + case 'U': + // Shift+i or Shift+u — OWA uses a single "Read / Unread" toggle + if (shift) { + targets = getActionTargets(items); + actionMarkReadUnread(targets); + } else { + 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) { 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) { var wasEnabled = currentSettings.keyboardMultiSelect; var isEnabled = settings.keyboardMultiSelect; - var oldPreset = currentSettings.keyboardPreset; - var newPreset = settings.keyboardPreset; currentSettings = settings; - if (wasEnabled !== isEnabled || oldPreset !== newPreset) { + if (wasEnabled !== isEnabled) { stop(); start(settings); }