|
|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
// Outcut — Keyboard Navigation & Multi-Select
|
|
|
|
|
// Outlook Relook — Gmail-Style Keyboard Navigation & Multi-Select
|
|
|
|
|
// Adds keyboard focus cursor and multi-select to OWA's message list.
|
|
|
|
|
// Gated by the keyboardMultiSelect setting.
|
|
|
|
|
//
|
|
|
|
|
@@ -7,53 +7,6 @@
|
|
|
|
|
|
|
|
|
|
window.OutlookRelook = window.OutlookRelook || {};
|
|
|
|
|
|
|
|
|
|
// Keyboard shortcut presets
|
|
|
|
|
var KEY_PRESETS = {
|
|
|
|
|
gmail: {
|
|
|
|
|
nextMessage: [{key: 'j'}, {key: 'ArrowDown'}],
|
|
|
|
|
prevMessage: [{key: 'k'}, {key: 'ArrowUp'}],
|
|
|
|
|
selectExtendDown: [{key: 'j', shift: true}, {key: 'ArrowDown', shift: true}],
|
|
|
|
|
selectExtendUp: [{key: 'k', shift: true}, {key: 'ArrowUp', shift: true}],
|
|
|
|
|
toggleSelect: [{key: 'x'}, {key: ' '}],
|
|
|
|
|
delete: [{key: '#'}],
|
|
|
|
|
archive: [{key: 'e'}],
|
|
|
|
|
readUnread: [{key: 'I', shift: true}, {key: 'U', shift: true}],
|
|
|
|
|
move: [{key: 'v'}],
|
|
|
|
|
flag: [{key: 'f'}],
|
|
|
|
|
pin: [{key: 'p'}],
|
|
|
|
|
deselect: [{key: 'Escape'}],
|
|
|
|
|
open: [{key: 'Enter'}, {key: 'o'}],
|
|
|
|
|
label: 'Gmail-style: j/k nav, x select, # del, e archive'
|
|
|
|
|
},
|
|
|
|
|
outlook: {
|
|
|
|
|
nextMessage: [{key: 'ArrowDown'}, {key: 'j'}],
|
|
|
|
|
prevMessage: [{key: 'ArrowUp'}, {key: 'k'}],
|
|
|
|
|
selectExtendDown: [{key: 'ArrowDown', shift: true}],
|
|
|
|
|
selectExtendUp: [{key: 'ArrowUp', shift: true}],
|
|
|
|
|
toggleSelect: [{key: ' '}],
|
|
|
|
|
delete: [{key: 'Delete'}, {key: 'Backspace'}],
|
|
|
|
|
archive: [{key: 'e'}],
|
|
|
|
|
readUnread: [{key: 'q'}, {key: 'I', shift: true}],
|
|
|
|
|
move: [{key: 'v'}],
|
|
|
|
|
flag: [{key: 'Insert'}, {key: 'f'}],
|
|
|
|
|
pin: [{key: 'p'}],
|
|
|
|
|
deselect: [{key: 'Escape'}],
|
|
|
|
|
open: [{key: 'Enter'}],
|
|
|
|
|
label: 'Outlook-style: arrows nav, Space select, Del delete'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function matchesAction(e, actionBindings) {
|
|
|
|
|
if (!actionBindings) return false;
|
|
|
|
|
for (var i = 0; i < actionBindings.length; i++) {
|
|
|
|
|
var b = actionBindings[i];
|
|
|
|
|
if (e.key === b.key && !!e.shiftKey === !!b.shift && !!e.ctrlKey === !!b.ctrl) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.OutlookRelook.Keyboard = (function () {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
@@ -134,6 +87,9 @@ window.OutlookRelook.Keyboard = (function () {
|
|
|
|
|
var idx = findItemById(items, id);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
items[idx].classList.add('or-kb-selected');
|
|
|
|
|
console.log('[Outcut] Applied or-kb-selected to', id.substring(0, 20), 'hasClass:', items[idx].classList.contains('or-kb-selected'));
|
|
|
|
|
} else {
|
|
|
|
|
console.log('[Outcut] Could not find item for selected id:', id.substring(0, 20));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -372,6 +328,18 @@ window.OutlookRelook.Keyboard = (function () {
|
|
|
|
|
|
|
|
|
|
// --- Key handler ---
|
|
|
|
|
|
|
|
|
|
// Outlook preset: map Outlook-native keys to the Gmail-style internal keys
|
|
|
|
|
// used by the switch below. This way the action code stays untouched.
|
|
|
|
|
function remapForOutlookPreset(key, shift) {
|
|
|
|
|
// Delete/Backspace → '#' (delete)
|
|
|
|
|
if (key === 'Delete' || key === 'Backspace') return { key: '#', shift: shift };
|
|
|
|
|
// q → mark read/unread (Outlook native shortcut for read/unread)
|
|
|
|
|
if (key === 'q' && !shift) return { key: 'I', shift: true };
|
|
|
|
|
// Insert → flag
|
|
|
|
|
if (key === 'Insert') return { key: 'f', shift: shift };
|
|
|
|
|
return { key: key, shift: shift };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleKeydown(e) {
|
|
|
|
|
if (!currentSettings.keyboardMultiSelect) return;
|
|
|
|
|
if (isComposeOrDialogActive()) return;
|
|
|
|
|
@@ -379,8 +347,15 @@ window.OutlookRelook.Keyboard = (function () {
|
|
|
|
|
var items = getMessageItems();
|
|
|
|
|
if (items.length === 0) return;
|
|
|
|
|
|
|
|
|
|
var presetName = currentSettings.keyboardPreset || 'gmail';
|
|
|
|
|
var preset = KEY_PRESETS[presetName] || KEY_PRESETS.gmail;
|
|
|
|
|
var key = e.key;
|
|
|
|
|
var shift = e.shiftKey;
|
|
|
|
|
|
|
|
|
|
// Apply Outlook preset key remapping if active
|
|
|
|
|
if (currentSettings.keyboardPreset === 'outlook') {
|
|
|
|
|
var remapped = remapForOutlookPreset(key, shift);
|
|
|
|
|
key = remapped.key;
|
|
|
|
|
shift = remapped.shift;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize focus if not set
|
|
|
|
|
var currentIdx = getFocusedIndex(items);
|
|
|
|
|
@@ -403,54 +378,101 @@ window.OutlookRelook.Keyboard = (function () {
|
|
|
|
|
var handled = true;
|
|
|
|
|
var targets;
|
|
|
|
|
|
|
|
|
|
if (matchesAction(e, preset.selectExtendDown)) {
|
|
|
|
|
toggleSelect(items, currentIdx);
|
|
|
|
|
if (currentIdx < items.length - 1) {
|
|
|
|
|
setFocus(items, currentIdx + 1);
|
|
|
|
|
toggleSelect(items, currentIdx + 1);
|
|
|
|
|
}
|
|
|
|
|
} else if (matchesAction(e, preset.selectExtendUp)) {
|
|
|
|
|
toggleSelect(items, currentIdx);
|
|
|
|
|
if (currentIdx > 0) {
|
|
|
|
|
setFocus(items, currentIdx - 1);
|
|
|
|
|
toggleSelect(items, currentIdx - 1);
|
|
|
|
|
}
|
|
|
|
|
} else if (matchesAction(e, preset.nextMessage)) {
|
|
|
|
|
if (currentIdx < items.length - 1) setFocus(items, currentIdx + 1);
|
|
|
|
|
} else if (matchesAction(e, preset.prevMessage)) {
|
|
|
|
|
if (currentIdx > 0) setFocus(items, currentIdx - 1);
|
|
|
|
|
} else if (matchesAction(e, preset.toggleSelect)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleSelect(items, currentIdx);
|
|
|
|
|
} else if (matchesAction(e, preset.delete)) {
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionDelete(targets);
|
|
|
|
|
} else if (matchesAction(e, preset.archive)) {
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionArchive(targets);
|
|
|
|
|
} else if (matchesAction(e, preset.readUnread)) {
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionMarkReadUnread(targets);
|
|
|
|
|
} else if (matchesAction(e, preset.move)) {
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionMove(targets);
|
|
|
|
|
} else if (matchesAction(e, preset.flag)) {
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionFlag(targets);
|
|
|
|
|
} else if (matchesAction(e, preset.pin)) {
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionPin(targets);
|
|
|
|
|
} else if (matchesAction(e, preset.deselect)) {
|
|
|
|
|
clearSelection();
|
|
|
|
|
} else if (matchesAction(e, preset.open)) {
|
|
|
|
|
if (currentIdx >= 0 && currentIdx < items.length) items[currentIdx].click();
|
|
|
|
|
} else {
|
|
|
|
|
handled = false;
|
|
|
|
|
switch (key) {
|
|
|
|
|
case 'j':
|
|
|
|
|
case 'ArrowDown':
|
|
|
|
|
if (shift) {
|
|
|
|
|
toggleSelect(items, currentIdx);
|
|
|
|
|
if (currentIdx < items.length - 1) {
|
|
|
|
|
setFocus(items, currentIdx + 1);
|
|
|
|
|
toggleSelect(items, currentIdx + 1);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (currentIdx < items.length - 1) {
|
|
|
|
|
setFocus(items, currentIdx + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'k':
|
|
|
|
|
case 'ArrowUp':
|
|
|
|
|
if (shift) {
|
|
|
|
|
toggleSelect(items, currentIdx);
|
|
|
|
|
if (currentIdx > 0) {
|
|
|
|
|
setFocus(items, currentIdx - 1);
|
|
|
|
|
toggleSelect(items, currentIdx - 1);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (currentIdx > 0) {
|
|
|
|
|
setFocus(items, currentIdx - 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'x':
|
|
|
|
|
case ' ':
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
toggleSelect(items, currentIdx);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case '#':
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionDelete(targets);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'e':
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionArchive(targets);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'I':
|
|
|
|
|
case 'U':
|
|
|
|
|
// Shift+i or Shift+u — OWA uses a single "Read / Unread" toggle
|
|
|
|
|
if (shift) {
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionMarkReadUnread(targets);
|
|
|
|
|
} else {
|
|
|
|
|
handled = false;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'v':
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionMove(targets);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'f':
|
|
|
|
|
// Flag/unflag
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionFlag(targets);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'p':
|
|
|
|
|
// Pin/unpin
|
|
|
|
|
targets = getActionTargets(items);
|
|
|
|
|
actionPin(targets);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'Escape':
|
|
|
|
|
clearSelection();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'Enter':
|
|
|
|
|
case 'o':
|
|
|
|
|
if (currentIdx >= 0 && currentIdx < items.length) {
|
|
|
|
|
items[currentIdx].click();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
handled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (handled) {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
if (e.key !== 'Enter') e.preventDefault();
|
|
|
|
|
if (key !== 'Enter' && key !== 'o') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -506,11 +528,9 @@ window.OutlookRelook.Keyboard = (function () {
|
|
|
|
|
function updateSettings(settings) {
|
|
|
|
|
var wasEnabled = currentSettings.keyboardMultiSelect;
|
|
|
|
|
var isEnabled = settings.keyboardMultiSelect;
|
|
|
|
|
var oldPreset = currentSettings.keyboardPreset;
|
|
|
|
|
var newPreset = settings.keyboardPreset;
|
|
|
|
|
currentSettings = settings;
|
|
|
|
|
|
|
|
|
|
if (wasEnabled !== isEnabled || oldPreset !== newPreset) {
|
|
|
|
|
if (wasEnabled !== isEnabled) {
|
|
|
|
|
stop();
|
|
|
|
|
start(settings);
|
|
|
|
|
}
|
|
|
|
|
|