feat: behavior patches — auto-collapse, hover suppress, toast dismiss, auto-advance
This commit is contained in:
@@ -1 +1,291 @@
|
|||||||
// Outlook Relook — behavior.js (stub, implemented in later task)
|
// Outlook Relook — Behavior Patches
|
||||||
|
// JS-based UX improvements, each gated by a setting key.
|
||||||
|
|
||||||
|
window.OutlookRelook = window.OutlookRelook || {};
|
||||||
|
|
||||||
|
window.OutlookRelook.Behavior = (function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var OR = window.OutlookRelook;
|
||||||
|
var currentSettings = {};
|
||||||
|
var cleanupFns = [];
|
||||||
|
|
||||||
|
// --- Auto-collapse ribbon on page load ---
|
||||||
|
function setupAutoCollapseRibbon() {
|
||||||
|
if (!currentSettings.autoCollapseRibbon) return;
|
||||||
|
|
||||||
|
// Wait for OWA to finish rendering, then collapse the ribbon
|
||||||
|
var timer = setTimeout(function () {
|
||||||
|
var elements = OR.resolveSelector('ribbon-collapse-button');
|
||||||
|
for (var i = 0; i < elements.length; i++) {
|
||||||
|
if (elements[i].getAttribute('aria-expanded') === 'true') {
|
||||||
|
elements[i].click();
|
||||||
|
console.log('[Outlook Relook] Auto-collapsed ribbon');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
cleanupFns.push(function () { clearTimeout(timer); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Remember sidebar collapsed/expanded state ---
|
||||||
|
function setupRememberSidebar() {
|
||||||
|
if (!currentSettings.rememberSidebarState) return;
|
||||||
|
|
||||||
|
var savedState = localStorage.getItem('or-sidebar-collapsed');
|
||||||
|
if (savedState === 'true') {
|
||||||
|
var timer = setTimeout(function () {
|
||||||
|
var pane = OR.resolveSelector('folder-pane');
|
||||||
|
for (var i = 0; i < pane.length; i++) {
|
||||||
|
var toggle = pane[i].closest('[aria-expanded]') || pane[i].querySelector('[aria-expanded]');
|
||||||
|
if (toggle && toggle.getAttribute('aria-expanded') === 'true') {
|
||||||
|
toggle.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
cleanupFns.push(function () { clearTimeout(timer); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for sidebar toggle changes
|
||||||
|
var sidebarObserver = new MutationObserver(function () {
|
||||||
|
var pane = OR.resolveSelector('folder-pane');
|
||||||
|
if (pane.length > 0) {
|
||||||
|
var isVisible = pane[0].offsetWidth > 50;
|
||||||
|
localStorage.setItem('or-sidebar-collapsed', String(!isVisible));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var timer2 = setTimeout(function () {
|
||||||
|
var pane = OR.resolveSelector('folder-pane');
|
||||||
|
if (pane.length > 0 && pane[0].parentElement) {
|
||||||
|
sidebarObserver.observe(pane[0].parentElement, { attributes: true, subtree: true });
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
cleanupFns.push(function () {
|
||||||
|
clearTimeout(timer2);
|
||||||
|
sidebarObserver.disconnect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Suppress contact card hover popups ---
|
||||||
|
function setupSuppressContactHover() {
|
||||||
|
if (!currentSettings.suppressContactHover) return;
|
||||||
|
|
||||||
|
var handler = function (e) {
|
||||||
|
// Check if the hovered element or its ancestors trigger a contact card
|
||||||
|
var target = e.target.closest('[data-lpc-hover-target], [aria-haspopup="dialog"]');
|
||||||
|
if (target) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseenter', handler, true);
|
||||||
|
cleanupFns.push(function () { document.removeEventListener('mouseenter', handler, true); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auto-advance to next email after delete ---
|
||||||
|
function setupAutoAdvance() {
|
||||||
|
if (!currentSettings.autoAdvanceAfterDelete) return;
|
||||||
|
|
||||||
|
var handler = function (e) {
|
||||||
|
// Detect delete key press
|
||||||
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
var selected = document.querySelector(
|
||||||
|
'[role="option"][aria-selected="true"], [role="listitem"][aria-selected="true"]'
|
||||||
|
);
|
||||||
|
if (selected) {
|
||||||
|
var next = selected.nextElementSibling;
|
||||||
|
if (next) {
|
||||||
|
// Wait for OWA to process the delete, then focus next
|
||||||
|
setTimeout(function () {
|
||||||
|
next.click();
|
||||||
|
next.focus();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handler, true);
|
||||||
|
cleanupFns.push(function () { document.removeEventListener('keydown', handler, true); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auto-dismiss notification toasts ---
|
||||||
|
function setupAutoDismissToasts() {
|
||||||
|
if (currentSettings.autoDismissToasts === 'off') return;
|
||||||
|
var delay = parseInt(currentSettings.autoDismissToasts, 10) * 1000;
|
||||||
|
if (isNaN(delay) || delay <= 0) return;
|
||||||
|
|
||||||
|
var toastObserver = new MutationObserver(function (mutations) {
|
||||||
|
for (var m = 0; m < mutations.length; m++) {
|
||||||
|
var addedNodes = mutations[m].addedNodes;
|
||||||
|
for (var n = 0; n < addedNodes.length; n++) {
|
||||||
|
var node = addedNodes[n];
|
||||||
|
if (node.nodeType !== 1) continue;
|
||||||
|
var toasts = [];
|
||||||
|
if (node.matches && node.matches('[role="alert"], [role="status"][aria-live]')) {
|
||||||
|
toasts.push(node);
|
||||||
|
}
|
||||||
|
if (node.querySelectorAll) {
|
||||||
|
var found = node.querySelectorAll('[role="alert"], [role="status"][aria-live]');
|
||||||
|
for (var t = 0; t < found.length; t++) toasts.push(found[t]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < toasts.length; i++) {
|
||||||
|
(function (toast) {
|
||||||
|
setTimeout(function () {
|
||||||
|
// Try to find a dismiss button
|
||||||
|
var dismiss = toast.querySelector('[aria-label*="Close" i], [aria-label*="Dismiss" i]');
|
||||||
|
if (dismiss) {
|
||||||
|
dismiss.click();
|
||||||
|
} else {
|
||||||
|
toast.style.display = 'none';
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
})(toasts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toastObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
|
cleanupFns.push(function () { toastObserver.disconnect(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reposition toast notifications ---
|
||||||
|
function setupToastPosition() {
|
||||||
|
if (currentSettings.toastPosition === 'bottom-left') return; // OWA default
|
||||||
|
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'or-toast-position';
|
||||||
|
style.textContent = [
|
||||||
|
'[role="alert"], [role="status"][aria-live] {',
|
||||||
|
' position: fixed !important;',
|
||||||
|
' top: 8px !important;',
|
||||||
|
' right: 8px !important;',
|
||||||
|
' bottom: auto !important;',
|
||||||
|
' left: auto !important;',
|
||||||
|
' z-index: 999999 !important;',
|
||||||
|
'}'
|
||||||
|
].join('\n');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
cleanupFns.push(function () { style.remove(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sticky Reply/Forward bar ---
|
||||||
|
function setupStickyReplyBar() {
|
||||||
|
if (!currentSettings.stickyReplyBar) return;
|
||||||
|
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'or-sticky-reply';
|
||||||
|
style.textContent = [
|
||||||
|
'[aria-label*="Reply all" i][role="button"],',
|
||||||
|
'[aria-label*="Reply" i][role="button"],',
|
||||||
|
'[aria-label*="Forward" i][role="button"] {',
|
||||||
|
' position: sticky !important;',
|
||||||
|
' bottom: 0 !important;',
|
||||||
|
' z-index: 10 !important;',
|
||||||
|
' background-color: var(--or-bg-primary, #fff) !important;',
|
||||||
|
'}'
|
||||||
|
].join('\n');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
cleanupFns.push(function () { style.remove(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auto-resize compose window ---
|
||||||
|
function setupAutoResizeCompose() {
|
||||||
|
if (!currentSettings.autoResizeCompose) return;
|
||||||
|
|
||||||
|
var composeObserver = new MutationObserver(function (mutations) {
|
||||||
|
for (var m = 0; m < mutations.length; m++) {
|
||||||
|
var addedNodes = mutations[m].addedNodes;
|
||||||
|
for (var n = 0; n < addedNodes.length; n++) {
|
||||||
|
var node = addedNodes[n];
|
||||||
|
if (node.nodeType !== 1) continue;
|
||||||
|
var composeWindows = [];
|
||||||
|
if (node.matches && node.matches('[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]')) {
|
||||||
|
composeWindows.push(node);
|
||||||
|
}
|
||||||
|
if (node.querySelectorAll) {
|
||||||
|
var found = node.querySelectorAll('[aria-label*="compose" i][role="dialog"], [aria-label*="New message" i]');
|
||||||
|
for (var i = 0; i < found.length; i++) composeWindows.push(found[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var w = 0; w < composeWindows.length; w++) {
|
||||||
|
composeWindows[w].style.minHeight = '60vh';
|
||||||
|
composeWindows[w].style.height = '60vh';
|
||||||
|
console.log('[Outlook Relook] Auto-resized compose window');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
composeObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
|
cleanupFns.push(function () { composeObserver.disconnect(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Throttle desktop notifications ---
|
||||||
|
function setupThrottleNotifications() {
|
||||||
|
if (!currentSettings.throttleNotifications) return;
|
||||||
|
|
||||||
|
var OrigNotification = window.Notification;
|
||||||
|
|
||||||
|
// Only patch if Notification API exists
|
||||||
|
if (!OrigNotification) return;
|
||||||
|
|
||||||
|
var lastNotificationTime = 0;
|
||||||
|
var MIN_INTERVAL = 30000; // 30 seconds between notifications
|
||||||
|
|
||||||
|
window.Notification = function (title, options) {
|
||||||
|
var now = Date.now();
|
||||||
|
if (now - lastNotificationTime < MIN_INTERVAL) {
|
||||||
|
console.log('[Outlook Relook] Throttled notification: "' + title + '"');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
lastNotificationTime = now;
|
||||||
|
return new OrigNotification(title, options);
|
||||||
|
};
|
||||||
|
window.Notification.permission = OrigNotification.permission;
|
||||||
|
window.Notification.requestPermission = OrigNotification.requestPermission.bind(OrigNotification);
|
||||||
|
|
||||||
|
cleanupFns.push(function () {
|
||||||
|
window.Notification = OrigNotification;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public API ---
|
||||||
|
|
||||||
|
function start(settings) {
|
||||||
|
currentSettings = settings;
|
||||||
|
|
||||||
|
setupAutoCollapseRibbon();
|
||||||
|
setupRememberSidebar();
|
||||||
|
setupSuppressContactHover();
|
||||||
|
setupAutoAdvance();
|
||||||
|
setupAutoDismissToasts();
|
||||||
|
setupToastPosition();
|
||||||
|
setupStickyReplyBar();
|
||||||
|
setupAutoResizeCompose();
|
||||||
|
setupThrottleNotifications();
|
||||||
|
|
||||||
|
console.log('[Outlook Relook] Behavior patches applied');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSettings(settings) {
|
||||||
|
// Tear down existing behaviors and re-apply
|
||||||
|
stop();
|
||||||
|
currentSettings = settings;
|
||||||
|
start(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
for (var i = 0; i < cleanupFns.length; i++) {
|
||||||
|
try { cleanupFns[i](); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
cleanupFns = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start: start, updateSettings: updateSettings, stop: stop };
|
||||||
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user