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); target.dispatchEvent(overEvent);
} }
// performAction takes an array of IDs (strings), re-resolves each to a // Find a button in the global OWA toolbar (not inline on a row).
// fresh DOM element before acting, and processes them sequentially. // The toolbar button acts on OWA's currently selected messages.
// This handles OWA's re-renders that invalidate stale element references. function findToolbarButton(label) {
function performAction(ids, actionFn) { var sels = [
if (!ids || ids.length === 0) return; '[role="toolbar"] button[aria-label="' + label + '"]',
var i = 0; '.fui-Toolbar button[aria-label="' + label + '"]',
function processNext() { '[aria-label*="Quick actions" i] button[aria-label="' + label + '"]'
if (i >= ids.length) { ];
clearSelection(); for (var i = 0; i < sels.length; i++) {
return; var btn = document.querySelector(sels[i]);
} if (btn) return btn;
var id = ids[i];
i++;
// 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 () {
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);
}
}, 120);
} }
processNext(); // 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;
} }
function actionDelete(targets) { // Ctrl+click a message row to add it to OWA's native multi-selection.
performAction(targets, function (target) { // Uses MouseEvent with ctrlKey + metaKey to handle both Mac and Win.
var btn = findButton(target, 'Delete'); function ctrlClickRow(target) {
if (btn) { btn.click(); return true; } var rect = target.getBoundingClientRect();
console.warn('[Outcut] Delete button not found'); var opts = {
return false; 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));
} }
function actionArchive(targets) { // Build OWA's native selection by Ctrl+clicking each ID, then run a single
performAction(targets, function (target) { // toolbar action that operates on the entire selection.
var btn = findButton(target, 'Archive'); function performToolbarAction(ids, toolbarLabel, fallbackContextPattern) {
if (btn) { btn.click(); return true; } if (!ids || ids.length === 0) return;
console.warn('[Outcut] Archive button not found');
return false; // 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
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;
}
var t = resolveTargetById(ids[i]);
i++;
if (t) ctrlClickRow(t);
setTimeout(addNext, 60);
}
addNext();
}, 100);
} }
function actionMarkReadUnread(targets) { function actionDelete(ids) {
performAction(targets, function (target) { performToolbarAction(ids, 'Delete', /^delete$/i);
var btn = findButton(target, 'Read / Unread'); }
if (btn) { btn.click(); return true; }
triggerContextMenuAction(target, /mark as read|mark as unread|read \/ unread/i); function actionArchive(ids) {
return true; // context menu handles it performToolbarAction(ids, 'Archive', /^archive$/i);
}); }
function actionMarkReadUnread(ids) {
performToolbarAction(ids, 'Read / Unread', /mark as read|mark as unread|read \/ unread/i);
} }
function actionMove(ids) { function actionMove(ids) {
performToolbarAction(ids, 'Move to', /move to/i);
}
function actionFlag(ids) {
// Try multiple labels
if (!ids || ids.length === 0) return; if (!ids || ids.length === 0) return;
var first = resolveTargetById(ids[0]); var first = resolveTargetById(ids[0]);
if (!first) return; if (!first) return;
triggerHover(first); first.click();
setTimeout(function () { setTimeout(function () {
var btn = findButton(first, 'Move to'); var i = 1;
if (btn) { function addNext() {
btn.click(); if (i >= ids.length) {
} else { setTimeout(function () {
triggerContextMenuAction(first, /move to/i); 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);
} }
}, 150); addNext();
}, 100);
} }
function actionFlag(targets) { function actionPin(ids) {
performAction(targets, function (target) { performToolbarAction(ids, 'Pin / Unpin', /pin/i);
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) { function triggerContextMenuAction(target, pattern) {