Files
Outcut/content/injector.js
Joel Brock 3de2db7d89 rename: Outlook Relook → Outcut, add keyboard presets, remove design UI from popup
- Rename extension to "Outcut" throughout all source files and console logs
- Add KEY_PRESETS object (gmail/outlook) and matchesAction() helper to keyboard.js
- Rewrite handleKeydown to use preset-based dispatch instead of hardcoded switch
- Update updateSettings to re-init when keyboardPreset changes
- Add keyboardPreset: 'gmail' default to settings-defaults.js
- Replace popup Design Tweaks UI with preset dropdown + dynamic help text
- Keep all design tweak content script logic intact (activatable via storage)
- Update manifest: version 1.0.0, remove activeTab, add homepage_url
2026-04-27 14:11:45 -07:00

265 lines
8.3 KiB
JavaScript

// 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('[Outcut] Marked all as read');
return;
}
}
// Close context menu if option not found
document.body.click();
console.warn('[Outcut] "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('[Outcut] 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 };
})();