fix: track action targets by ID, re-resolve before each action to survive re-renders

This commit is contained in:
Joel Brock
2026-04-28 07:51:45 -07:00
parent e75bcfa478
commit 97ca274070

View File

@@ -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();