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