feat: DOM injector — mark-all-read button and folder jump dialog
This commit is contained in:
@@ -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 };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user