fix: use OWA native multi-select via Ctrl+click then single toolbar action

This commit is contained in:
Joel Brock
2026-04-28 07:55:38 -07:00
parent 97ca274070
commit 4d5be13b51

View File

@@ -261,111 +261,128 @@ window.OutlookRelook.Keyboard = (function () {
target.dispatchEvent(overEvent);
}
// 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) {
// 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];
}
}
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;
var i = 0;
function processNext() {
if (i >= ids.length) {
// First: clear any current OWA selection by clicking the first target plain
var first = resolveTargetById(ids[0]);
if (!first) {
clearSelection();
return;
}
var id = ids[i];
i++;
first.click();
// 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();
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
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;
}
// Hover to make OWA's inline action buttons appear
triggerHover(target);
setTimeout(function () {
var inlineHandled = actionFn(target);
if (inlineHandled === false) {
// Fallback: click the row + use toolbar
target.click();
setTimeout(function () {
// Re-resolve once more in case clicking caused a re-render
var freshTarget = resolveTargetById(id) || target;
actionFn(freshTarget);
setTimeout(processNext, 250);
}, 150);
} else {
// Wait long enough for OWA to finish re-rendering before next action
setTimeout(processNext, 300);
var t = resolveTargetById(ids[i]);
i++;
if (t) ctrlClickRow(t);
setTimeout(addNext, 60);
}
}, 120);
}
processNext();
addNext();
}, 100);
}
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 actionDelete(ids) {
performToolbarAction(ids, 'Delete', /^delete$/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 actionArchive(ids) {
performToolbarAction(ids, 'Archive', /^archive$/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 actionMarkReadUnread(ids) {
performToolbarAction(ids, 'Read / Unread', /mark as read|mark as unread|read \/ unread/i);
}
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;
triggerHover(first);
first.click();
setTimeout(function () {
var btn = findButton(first, 'Move to');
if (btn) {
btn.click();
} else {
triggerContextMenuAction(first, /move to/i);
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;
}
}, 150);
var t = resolveTargetById(ids[i]);
i++;
if (t) ctrlClickRow(t);
setTimeout(addNext, 60);
}
addNext();
}, 100);
}
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 actionPin(ids) {
performToolbarAction(ids, 'Pin / Unpin', /pin/i);
}
function triggerContextMenuAction(target, pattern) {