From 62157e67bccb7f6c7a395ac90bf23aeedc019d36 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 23 Apr 2026 08:58:51 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20DOM=20injector=20=E2=80=94=20mark-all-r?= =?UTF-8?q?ead=20button=20and=20folder=20jump=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/injector.js | 265 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 1 deletion(-) diff --git a/content/injector.js b/content/injector.js index e611edf..9d8bc86 100644 --- a/content/injector.js +++ b/content/injector.js @@ -1 +1,264 @@ -// Outlook Relook — injector.js (stub, implemented in later task) +// Outlook Relook — DOM Injector +// Injects custom UI elements: mark-all-read button, folder jump dialog. + +window.OutlookRelook = window.OutlookRelook || {}; + +window.OutlookRelook.Injector = (function () { + 'use strict'; + + var OR = window.OutlookRelook; + var currentSettings = {}; + var cleanupFns = []; + + // --- "Mark all as read" button --- + function setupMarkAllRead() { + if (!currentSettings.markAllReadButton) return; + + function injectButton() { + // Find folder headers that don't already have our button + var headers = OR.resolveSelector('folder-header'); + for (var i = 0; i < headers.length; i++) { + var header = headers[i]; + if (header.querySelector('.or-mark-all-read')) continue; + + var btn = document.createElement('button'); + btn.className = 'or-mark-all-read'; + btn.textContent = '\u2713\u2713'; // double checkmark + btn.title = 'Mark all as read'; + btn.style.cssText = [ + 'background: none;', + 'border: 1px solid var(--or-border, #ccc);', + 'color: var(--or-text-secondary, #666);', + 'cursor: pointer;', + 'font-size: 11px;', + 'padding: 2px 6px;', + 'margin-left: 8px;', + 'border-radius: 3px;', + 'line-height: 1;', + 'vertical-align: middle;' + ].join(' '); + + btn.addEventListener('click', (function (hdr) { + return function (e) { + e.stopPropagation(); + // Find the context menu "Mark all as read" option + var folder = hdr.closest('[role="treeitem"]'); + if (folder) { + // Dispatch a contextmenu event to open OWA's context menu + var rect = folder.getBoundingClientRect(); + var contextEvent = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + clientX: rect.x + 10, + clientY: rect.y + 10, + }); + folder.dispatchEvent(contextEvent); + + // Wait for context menu to render, then find and click "Mark all as read" + setTimeout(function () { + var menuItems = document.querySelectorAll('[role="menuitem"]'); + for (var j = 0; j < menuItems.length; j++) { + if (/mark all as read/i.test(menuItems[j].textContent)) { + menuItems[j].click(); + console.log('[Outlook Relook] Marked all as read'); + return; + } + } + // Close context menu if option not found + document.body.click(); + console.warn('[Outlook Relook] "Mark all as read" menu item not found'); + }, 300); + } + }; + })(header)); + + header.appendChild(btn); + } + } + + // Inject on load and watch for new headers + var timer = setTimeout(injectButton, 3000); + var injectObserver = new MutationObserver(function () { injectButton(); }); + injectObserver.observe(document.body, { childList: true, subtree: true }); + + cleanupFns.push(function () { + clearTimeout(timer); + injectObserver.disconnect(); + var btns = document.querySelectorAll('.or-mark-all-read'); + for (var k = 0; k < btns.length; k++) btns[k].remove(); + }); + } + + // --- Quick folder jump (Ctrl+Shift+K) --- + function setupFolderJump() { + if (!currentSettings.quickFolderJump) return; + + var dialog = null; + + function createDialog() { + var overlay = document.createElement('div'); + overlay.id = 'or-folder-jump'; + overlay.style.cssText = [ + 'position: fixed;', + 'top: 0; left: 0; right: 0; bottom: 0;', + 'background: rgba(0,0,0,0.4);', + 'z-index: 999999;', + 'display: flex;', + 'align-items: flex-start;', + 'justify-content: center;', + 'padding-top: 20vh;' + ].join(' '); + + var box = document.createElement('div'); + box.style.cssText = [ + 'background: var(--or-bg-primary, #fff);', + 'border: 1px solid var(--or-border, #ccc);', + 'border-radius: 8px;', + 'padding: 12px;', + 'width: 400px;', + 'max-height: 400px;', + 'box-shadow: 0 8px 32px rgba(0,0,0,0.2);', + 'font-family: inherit;' + ].join(' '); + + var input = document.createElement('input'); + input.type = 'text'; + input.placeholder = 'Jump to folder...'; + input.style.cssText = [ + 'width: 100%;', + 'padding: 8px 12px;', + 'border: 1px solid var(--or-border, #ccc);', + 'border-radius: 4px;', + 'font-size: 14px;', + 'outline: none;', + 'box-sizing: border-box;', + 'background: var(--or-bg-secondary, #f5f5f5);', + 'color: var(--or-text-primary, #000);' + ].join(' '); + + var results = document.createElement('div'); + results.style.cssText = 'margin-top: 8px; max-height: 300px; overflow-y: auto;'; + + input.addEventListener('input', function () { + var query = input.value.toLowerCase().trim(); + // Clear results using safe DOM methods + while (results.firstChild) { + results.removeChild(results.firstChild); + } + + if (!query) return; + + // Find all folder tree items + var folders = document.querySelectorAll('[role="treeitem"]'); + var matches = []; + for (var i = 0; i < folders.length; i++) { + var name = (folders[i].textContent || '').trim(); + if (name.toLowerCase().indexOf(query) !== -1) { + matches.push({ name: name, element: folders[i] }); + } + } + + var limit = Math.min(matches.length, 10); + for (var j = 0; j < limit; j++) { + (function (match) { + var item = document.createElement('div'); + item.textContent = match.name; + item.style.cssText = [ + 'padding: 6px 12px;', + 'cursor: pointer;', + 'border-radius: 4px;', + 'color: var(--or-text-primary, #000);' + ].join(' '); + item.addEventListener('mouseenter', function () { + item.style.backgroundColor = 'var(--or-bg-hover, #e0e0e0)'; + }); + item.addEventListener('mouseleave', function () { + item.style.backgroundColor = ''; + }); + item.addEventListener('click', function () { + match.element.click(); + closeDialog(); + }); + results.appendChild(item); + })(matches[j]); + } + }); + + box.appendChild(input); + box.appendChild(results); + overlay.appendChild(box); + + overlay.addEventListener('click', function (e) { + if (e.target === overlay) closeDialog(); + }); + + input.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeDialog(); + if (e.key === 'Enter') { + var firstResult = results.querySelector('div'); + if (firstResult) firstResult.click(); + } + }); + + return { overlay: overlay, input: input }; + } + + function openDialog() { + if (dialog) return; + var created = createDialog(); + document.body.appendChild(created.overlay); + dialog = created.overlay; + setTimeout(function () { created.input.focus(); }, 50); + } + + function closeDialog() { + if (dialog) { + dialog.remove(); + dialog = null; + } + } + + var handler = function (e) { + // Ctrl+Shift+K (or Cmd+Shift+K on Mac) + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'K') { + e.preventDefault(); + e.stopPropagation(); + if (dialog) { + closeDialog(); + } else { + openDialog(); + } + } + }; + + document.addEventListener('keydown', handler, true); + cleanupFns.push(function () { + document.removeEventListener('keydown', handler, true); + closeDialog(); + }); + } + + // --- Public API --- + + function start(settings) { + currentSettings = settings; + setupMarkAllRead(); + setupFolderJump(); + console.log('[Outlook Relook] Injector started'); + } + + function updateSettings(settings) { + 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 }; +})();