From 97ca274070587b30199c1aa437d651cdac50a6cd Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Tue, 28 Apr 2026 07:51:45 -0700 Subject: [PATCH] fix: track action targets by ID, re-resolve before each action to survive re-renders --- content/keyboard.js | 97 +++++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/content/keyboard.js b/content/keyboard.js index e287ed6..26f6029 100644 --- a/content/keyboard.js +++ b/content/keyboard.js @@ -176,18 +176,24 @@ window.OutlookRelook.Keyboard = (function () { updateCountBadge(); } - function getActionTargets(items) { - var targets = []; + // 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 = []; if (selectedIds.size > 0) { - selectedIds.forEach(function (id) { - var idx = findItemById(items, id); - if (idx >= 0) targets.push(items[idx]); - }); + selectedIds.forEach(function (id) { ids.push(id); }); } else { - var fi = getFocusedIndex(items); - if (fi >= 0) targets.push(items[fi]); + if (focusedId) ids.push(focusedId); } - return targets; + 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; } // --- Selection count badge --- @@ -255,36 +261,48 @@ window.OutlookRelook.Keyboard = (function () { target.dispatchEvent(overEvent); } - function performAction(targets, actionFn) { - if (targets.length === 0) return; + // performAction takes an array of IDs (strings), re-resolves each to a + // fresh DOM element before acting, and processes them sequentially. + // This handles OWA's re-renders that invalidate stale element references. + function performAction(ids, actionFn) { + if (!ids || ids.length === 0) return; var i = 0; function processNext() { - if (i >= targets.length) { + if (i >= ids.length) { clearSelection(); return; } - var target = targets[i]; + var id = ids[i]; i++; - // First, try to find the inline button without clicking the row. - // Hover to make OWA's inline action buttons appear. + // Re-resolve the ID to the current DOM element each iteration + var target = resolveTargetById(id); + if (!target) { + // Element no longer exists (e.g., already deleted) — skip to next + processNext(); + return; + } + + // Hover to make OWA's inline action buttons appear triggerHover(target); setTimeout(function () { - // Try inline button first (avoids opening the email in fill-screen mode) var inlineHandled = actionFn(target); - // If inline button wasn't found, fall back to clicking the row + toolbar if (inlineHandled === false) { + // Fallback: click the row + use toolbar target.click(); setTimeout(function () { - actionFn(target); - setTimeout(processNext, 200); + // Re-resolve once more in case clicking caused a re-render + var freshTarget = resolveTargetById(id) || target; + actionFn(freshTarget); + setTimeout(processNext, 250); }, 150); } else { - setTimeout(processNext, 200); + // Wait long enough for OWA to finish re-rendering before next action + setTimeout(processNext, 300); } - }, 100); + }, 120); } processNext(); } @@ -316,18 +334,19 @@ window.OutlookRelook.Keyboard = (function () { }); } - 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); - } - }, 150); - } + function actionMove(ids) { + if (!ids || ids.length === 0) return; + var first = resolveTargetById(ids[0]); + if (!first) return; + triggerHover(first); + setTimeout(function () { + var btn = findButton(first, 'Move to'); + if (btn) { + btn.click(); + } else { + triggerContextMenuAction(first, /move to/i); + } + }, 150); } function actionFlag(targets) { @@ -423,22 +442,22 @@ window.OutlookRelook.Keyboard = (function () { e.preventDefault(); toggleSelect(items, currentIdx); } else if (matchesAction(e, preset.delete)) { - targets = getActionTargets(items); + targets = getActionTargetIds(items); actionDelete(targets); } else if (matchesAction(e, preset.archive)) { - targets = getActionTargets(items); + targets = getActionTargetIds(items); actionArchive(targets); } else if (matchesAction(e, preset.readUnread)) { - targets = getActionTargets(items); + targets = getActionTargetIds(items); actionMarkReadUnread(targets); } else if (matchesAction(e, preset.move)) { - targets = getActionTargets(items); + targets = getActionTargetIds(items); actionMove(targets); } else if (matchesAction(e, preset.flag)) { - targets = getActionTargets(items); + targets = getActionTargetIds(items); actionFlag(targets); } else if (matchesAction(e, preset.pin)) { - targets = getActionTargets(items); + targets = getActionTargetIds(items); actionPin(targets); } else if (matchesAction(e, preset.deselect)) { clearSelection();