Skip to content

Instantly share code, notes, and snippets.

@marktaiwan
Last active November 11, 2022 22:45
Show Gist options
  • Save marktaiwan/344ed3fedaa6a767f4adfecf7ead7c86 to your computer and use it in GitHub Desktop.
Save marktaiwan/344ed3fedaa6a767f4adfecf7ead7c86 to your computer and use it in GitHub Desktop.
Configurable shortcuts and enhanced keyboard navigations. "Ctrl+Shift+/" to open settings.
// ==UserScript==
// @name E(x)Hentai Enhanced Keyboard Navigation
// @description Configurable shortcuts and enhanced keyboard navigations. "Ctrl+Shift+/" to open settings.
// @version 0.0.1
// @author Marker
// @license WTFPL
// @namespace https://github.com/marktaiwan/
// @match https://e-hentai.org/*
// @match https://exhentai.org/*
// @grant GM_addStyle
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
let lastSelected = null;
const SCRIPT_ID = 'custom_shortcuts';
const CSS = `/* Generated by Custom Shortcuts */
#${SCRIPT_ID}--panelWrapper {
position: fixed;
top: 0px;
left: 0px;
display: flex;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
}
#${SCRIPT_ID}--panel {
font-size: 1.2em;
min-width: unset;
max-width: unset;
margin: unset;
position: unset;
}
#${SCRIPT_ID}--close-button {
color: inherit;
background-color: transparent;
border: 1px solid;
cursor: pointer;
}
#${SCRIPT_ID}--preset-selector {
padding: 1px 2px;
}
.${SCRIPT_ID}--header {
text-align: center;
border-bottom: 1px solid;
padding-bottom: 5px;
}
.${SCRIPT_ID}--body {
padding-top: 5px;
max-height: calc(100vh - 80px);
overflow: auto;
}
.${SCRIPT_ID}--table {
display: grid;
width: 600px;
grid-template-columns: 1fr 150px 150px;
grid-column-gap: 5px;
grid-row-gap: 5px;
}
.${SCRIPT_ID}--table input {
margin: 2px;
padding: 2px 0px;
font-size: 12px;
align-self: center;
text-align: center;
}
.highlighted {
box-shadow: 0px 0px 0px 4px coral;
}
.highlighted a {
outline: none;
}
`;
/*
* - 'key' uses KeyboardEvent.code to represent keypress.
* For instance, 's' would be 'KeyS' and '5' would be either 'Digit5' or
* 'Numpad5'.
* - 'ctrl', 'alt', 'shift' are Booleans and defaults to false if not
* present.
*/
const presets = {
preset_1: {
scrollUp: [{key: 'KeyW'}, {key: 'ArrowUp'}],
scrollDown: [{key: 'KeyS'}, {key: 'ArrowDown'}],
scrollLeft: [{key: 'KeyA'}, {key: 'ArrowLeft'}],
scrollRight: [{key: 'KeyD'}, {key: 'ArrowRight'}],
toggleKeyboardNav: [{key: 'KeyQ'}],
toggleTagSelect: [{key: 'KeyT'}],
openSelected: [{key: 'KeyE'}],
openInNewTab: [{key: 'KeyE', shift: true}],
prev: [{key: 'KeyZ'}],
next: [{key: 'KeyX'}],
reloadImage: [{key: 'KeyR'}],
toGallery: [{key: 'KeyG'}],
focusSearch: [{key: 'KeyS', shift: true}],
historyBack: [{key: 'KeyA', shift: true}],
historyForward: [{key: 'KeyD', shift: true}],
showTagDef: [],
tagUpvote: [],
tagDownvote: [],
},
preset_2: {},
preset_3: {},
/* Keybinds that are applied globally */
global: {
usePreset_1: [{key: 'Digit1', alt: true}],
usePreset_2: [{key: 'Digit2', alt: true}],
usePreset_3: [{key: 'Digit3', alt: true}]
},
/* Special non-configurable keybinds */
reserved: {
unfocus: [{key: 'Escape'}],
toggleSettings: [{key: 'Slash', ctrl: true, shift: true}],
}
};
const reservedKeys = [
'Escape',
'Backspace',
'Delete',
'Meta',
'ContextMenu',
// 'Enter',
// 'Tab',
// 'CapsLock',
// 'ScrollLock',
// 'NumLock',
];
/*
* 'constant' executes the command twice, on keydown and keyup.
*
* 'repeat' indicates whether the command should act on
* subsequent events generated by the key being held down.
* Defaults to false.
*
* 'input' indicates whether the command should execute when an
* input field has focus.
* Defaults to false.
*
* 'global' indicates whether the keybind applies to all presets.
* Defaults to false.
*/
const actions = {
scrollUp: {
name: 'Scroll up',
fn: event => scroll('up', event),
constant: true,
repeat: true
},
scrollDown: {
name: 'Scroll down',
fn: event => scroll('down', event),
constant: true,
repeat: true
},
scrollLeft: {
name: 'Scroll left',
fn: event => scroll('left', event),
constant: true,
repeat: true
},
scrollRight: {
name: 'Scroll right',
fn: event => scroll('right', event),
constant: true,
repeat: true
},
toggleKeyboardNav: {
name: 'Toggle selection mode',
fn: () => {
let selector;
switch (getPageType()) {
case 'index':
selector = '.gl3m, .gl3c, .gl1e, .gl3t';
break;
case 'gallery':
selector = '.gdtm, .gdtl';
break;
default:
return;
}
const highlightedElement = $('.highlighted');
if (highlightedElement) {
unhighlight(highlightedElement);
} else {
if (lastSelected && !lastSelected.matches(TAG_SELECTOR) && isVisible(lastSelected)) {
highlight(lastSelected);
} else {
highlight(getFirstVisibleOrClosest(selector));
}
}
}
},
toggleTagSelect: {
name: 'Toggle tag selection',
fn: () => {
if (unsafeWindow.selected_tag) {
unsafeWindow.toggle_tagmenu(undefined, undefined);
} else {
unhighlight($('.highlighted'));
document.activeElement.blur();
if (lastSelected && lastSelected.matches(TAG_SELECTOR) && isVisible(lastSelected)) {
highlight(lastSelected);
} else {
highlight(getFirstVisibleOrClosest(TAG_SELECTOR));
}
}
}
},
openSelected: {
name: 'Open selected',
fn: () => {
if (unsafeWindow.selected_link) {
unsafeWindow.tag_show_galleries();
} else {
const selection = $('.highlighted');
if (selection) click('a', selection);
}
}
},
openInNewTab: {
name: 'Open selected in new tab',
fn: () => {
if (unsafeWindow.selected_link) {
window.open(unsafeWindow.selected_link.href, '_blank');
} else {
const selection = $('.highlighted');
if (selection) {
const anchor = $('a', selection);
window.open(anchor.href, '_blank');
}
}
}
},
prev: {
name: 'Previous page',
fn: () => {
if (getPageType() !== 'slide') {
click('.ptt td:first-child');
} else {
click('#prev');
}
}
},
next: {
name: 'Next page',
fn: () => {
if (getPageType() !== 'slide') {
click('.ptt td:last-child');
} else {
click('#next');
}
}
},
reloadImage: {
name: 'Reload image',
fn: () => click('#loadfail')
},
toGallery: {
name: 'Return to image gallery',
fn: () => {
switch (getPageType()) {
case 'slide':
click('#i5 .sb a');
break;
case 'gallery':
if (sessionStorage.lastIndex) {
sessionStorage.scrollAfterLoad = 1;
window.location.assign(sessionStorage.lastIndex);
} else {
window.location.assign(window.location.origin);
}
break;
case 'index':
window.location.assign(window.location.origin);
}
}
},
focusSearch: {
name: 'Focus on search field',
fn: () => {
const searchField = $('#f_search');
if (searchField) {
searchField.focus();
searchField.select();
return {preventDefault: true};
}
}
},
historyBack: {
name: 'Go back in browser history',
fn: () => window.history.back()
},
historyForward: {
name: 'Go forward in browser history',
fn: () => window.history.forward()
},
showTagDef: {
name: 'Show tag definition',
fn: () => {
if (unsafeWindow.selected_tag) unsafeWindow.tag_define();
}
},
tagUpvote: {
name: 'Tag: Vote up',
fn: () => {
if (unsafeWindow.selected_tag) unsafeWindow.tag_vote_up();
}
},
tagDownvote: {
name: 'Tag: Vote down',
fn: () => {
if (unsafeWindow.selected_tag) unsafeWindow.tag_vote_down();
}
},
usePreset_1: {
name: 'Global: Switch to preset 1',
fn: () => switchPreset('preset_1'),
global: true
},
usePreset_2: {
name: 'Global: Switch to preset 2',
fn: () => switchPreset('preset_2'),
global: true
},
usePreset_3: {
name: 'Global: Switch to preset 3',
fn: () => switchPreset('preset_3'),
global: true
},
unfocus: {
fn: (e) => {
e.target.blur();
return {stopPropagation: true};
},
input: true
},
toggleSettings: {
fn: () => {
const panel = $(`#${SCRIPT_ID}--panelWrapper`);
if (panel) {
panel.remove();
} else {
openSettings();
}
}
}
};
const TAG_SELECTOR = '.gtw a, .gtl a, .gt a';
const smoothscroll = (function () {
let startTime = null;
let prevFrame = 0;
let keydown = {up: false, down: false, left: false, right: false};
function reset() {
startTime = null;
keydown = {up: false, down: false, left: false, right: false};
}
function noKeyDown() {
return !(keydown.up || keydown.down || keydown.left || keydown.right);
}
function step(timestamp) {
// Only run step() once per animation frame. Discard any subsequent runs
// with interval greatly shorter than 16ms without resetting.
const interval = timestamp - prevFrame;
prevFrame = timestamp;
if (interval < 10) return;
if (noKeyDown() || !document.hasFocus()) {
reset();
return;
}
startTime = startTime || timestamp;
const elapsed = timestamp - startTime;
const maxVelocity = 40; // px/frame
const easeDuration = 250; // ms
const velocity = (elapsed > easeDuration)
? maxVelocity
: maxVelocity * (elapsed / easeDuration);
let x = 0;
let y = 0;
if (keydown.up) y += 1;
if (keydown.down) y += -1;
if (keydown.left) x += -1;
if (keydown.right) x += 1;
const rad = Math.atan2(y, x);
x = (x != 0) ? Math.cos(rad) : 0;
y = Math.sin(rad) * -1;
window.scrollBy(Math.round(x * velocity), Math.round(y * velocity));
window.requestAnimationFrame(step);
}
return function (direction, type) {
switch (type) {
case 'keydown':
if (noKeyDown()) window.requestAnimationFrame(step);
keydown[direction] = true;
break;
case 'keyup':
keydown[direction] = false;
break;
}
};
})();
const dispatchMouseover = (function () {
const interval = 100;
let timeout;
return function (ele, delay) {
if (delay) {
window.clearTimeout(timeout);
timeout = window.setTimeout(() => {
ele.dispatchEvent(new Event('mouseover'));
}, interval);
} else {
ele.dispatchEvent(new Event('mouseover'));
}
};
})();
function $(selector, parent = document) {
return parent.querySelector(selector);
}
function $$(selector, parent = document) {
return parent.querySelectorAll(selector);
}
function click(selector, parent = document) {
const el = $(selector, parent);
if (el) el.click();
}
function getStorage(key) {
const store = JSON.parse(localStorage.getItem(SCRIPT_ID));
return store[key];
}
function setStorage(key, val) {
const store = JSON.parse(localStorage.getItem(SCRIPT_ID));
store[key] = val;
localStorage.setItem(SCRIPT_ID, JSON.stringify(store));
}
function getRect(ele) {
// Relative to viewport
const {top, bottom, left, height, width} = ele.getBoundingClientRect();
const mid = (top + bottom) / 2;
// Relative to document
const x = left + window.pageXOffset + (width / 2);
const y = top + window.pageYOffset + (height / 2);
return {top, bottom, left, height, width, mid, x, y};
}
function isVisible(ele) {
const clientHeight = document.documentElement.clientHeight;
const {top, bottom, height, mid} = getRect(ele);
const margin = Math.min(Math.max(50, height / 4), clientHeight / 4);
const eleInViewport = (mid > 0 + margin && mid < clientHeight - margin
|| top < 0 + margin && bottom > clientHeight - margin);
let tagVisibleInContainer = true;
if (ele.matches(TAG_SELECTOR)) {
const {top: tagTop, bottom: tagBottom} = getRect(ele.parentElement);
const {top: listTop, bottom: listBottom} = getRect($('#taglist'));
tagVisibleInContainer = (tagTop - listTop > 0 && tagBottom - listBottom < 0);
}
return (eleInViewport && tagVisibleInContainer);
}
function getFirstVisibleOrClosest(selector) {
const nodeList = $$(selector);
const listLength = nodeList.length;
const viewportMid = document.documentElement.clientHeight / 2;
if (listLength < 1) return;
let closest = nodeList[0];
let closest_delta = Math.abs(getRect(closest).mid - viewportMid);
for (let i = 0; i < listLength; i++) {
const ele = nodeList[i];
if (isVisible(ele)) return ele;
const ele_y = getRect(ele).mid;
const ele_delta = Math.abs(ele_y - viewportMid);
if (ele_delta < closest_delta) {
[closest, closest_delta] = [ele, ele_delta];
}
}
return closest;
}
function getPageType() {
// Determine if the current page is index, gallery, or image slide
const indexReg = new RegExp('^https?://(exhentai|e-hentai)\\.org(/(tag/[\\w\\+:-]+(/\\d+)?/?|(popular|watched|favorites\\.php)?(\\?.*)?))?$');
const galleryReg = new RegExp('^https?://(exhentai|e-hentai)\\.org/g/\\d+/\\w+/(\\?p=\\d+)?$');
const slideReg = new RegExp('^https?://(exhentai|e-hentai)\\.org/s/\\w+/\\w+-\\d+(\\?nl=.+)?$');
const href = window.location.href;
if (indexReg.test(href)) return 'index';
if (galleryReg.test(href)) return 'gallery';
if (slideReg.test(href)) return 'slide';
}
function getIndexLayout() {
// Determine the index layout mode
const option = $('#dms option[selected]');
if (option) return option.value;
}
function highlight(selection, setSmooth = true) {
if (!selection) return;
// Undo existing selection
unhighlight($('.highlighted'));
if (unsafeWindow.selected_tag) unsafeWindow.toggle_tagmenu(undefined, undefined);
if (selection.matches(TAG_SELECTOR)) {
// Tag selection
selection.click();
} else {
// Thumb selection
// Special case for index thumbnail layout
if (selection.matches('.gl1t')) selection = $('.gl3t', selection);
$('a', selection).focus({preventScroll: true});
selection.classList.add('highlighted');
if (getPageType() == 'index' && ['m', 'p', 'l'].includes(getIndexLayout())) {
dispatchMouseover(selection, !setSmooth);
}
}
if (!isVisible(selection)) {
if (setSmooth) {
selection.scrollIntoView({behavior: 'smooth', block: 'center'});
} else {
selection.scrollIntoView({behavior: 'auto', block: 'nearest'});
}
}
lastSelected = selection;
}
function unhighlight(ele) {
if (!ele) return;
ele.classList.remove('highlighted');
document.activeElement.blur();
if (getPageType() == 'index' && ['m', 'p', 'l'].includes(getIndexLayout())) {
ele.dispatchEvent(new Event('mouseout'));
}
}
function scroll(direction, event) {
const type = event.type;
const highlighted = $('.highlighted') || unsafeWindow.selected_link;
if (highlighted && type == 'keydown') {
keyboardNav(direction, highlighted, !event.repeat);
} else if (!event.repeat){
smoothscroll(direction, type);
}
}
function keyboardNav(direction, highlighted, setSmooth) {
function similar(val1, val2, margin) {
return (val1 < val2 + margin && val1 > val2 - margin);
}
function distance(a, b) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
// debugger;
// Special case for index thumbnail layout
if (highlighted.matches('.gl3t')) highlighted = highlighted.parentElement;
const rect = getRect(highlighted);
const originalPos = {x: rect.x, y: rect.y};
const margin = 4; // px
const pageType = getPageType();
const pageLayout = getIndexLayout();
let selector;
if (highlighted.matches(TAG_SELECTOR)) {
// selecting tags
selector = TAG_SELECTOR;
} else if (pageType == 'gallery') {
// gallery: normal/large thumbnail
selector = '.gdtm, .gdtl';
} else if (pageLayout !== 't') {
// index: minimal, compact, extended layout
selector = '.gl3m, .gl3c, .gl1e';
} else {
// index: thumbnail layout
selector = '.gl1t';
}
const nodeList = $$(selector);
let ele = highlighted;
let index = [...nodeList].indexOf(ele);
if (pageType == 'index' && pageLayout !== 't') {
switch (direction) {
case 'up': case 'left': {
if (index > 0) ele = nodeList.item(--index);
break;
}
case 'down': case 'right': {
if (index < nodeList.length - 1) ele = nodeList.item(++index);
break;
}
}
} else {
switch (direction) {
case 'left': {
if (index > 0) ele = nodeList.item(--index);
break;
}
case 'right': {
if (index < nodeList.length - 1) ele = nodeList.item(++index);
break;
}
case 'up': case 'down': {
let closest = highlighted;
let closestDistance, closestYDistance;
while ((direction == 'up' && index > 0) || (direction == 'down' && index < nodeList.length - 1)) {
if (direction == 'up') index--;
if (direction == 'down') index++;
const current = nodeList.item(index);
const currentPos = getRect(current);
const currentDistance = distance(originalPos, currentPos);
const currentYDistance = Math.abs(currentPos.y - originalPos.y);
// Skip same row, and only iterate over elements one row up/down.
if (similar(currentPos.y, originalPos.y, margin)) continue;
if (!closestYDistance) closestYDistance = currentYDistance;
if (currentYDistance > closestYDistance) break;
if (!closestDistance || currentDistance <= closestDistance) {
closest = current;
closestDistance = currentDistance;
}
}
ele = closest;
break;
}
}
}
highlight(ele, setSmooth);
}
function switchPreset(id) {
const selector = $(`#${SCRIPT_ID}--preset-selector`);
if (selector) {
selector.value = id;
selector.dispatchEvent(new Event('input'));
} else {
setStorage('usePreset', id);
}
}
function getActiveKeybinds() {
const keybinds = getStorage('keybinds');
const id = getStorage('usePreset');
return keybinds[id];
}
function getGlobalKeybinds() {
const keybinds = getStorage('keybinds');
return keybinds['global'];
}
/*
* Returns false if no match found, otherwise returns the bind settings
*/
function matchKeybind(key, ctrl, alt, shift) {
const keybinds = {...getActiveKeybinds(), ...getGlobalKeybinds(), ...presets.reserved};
for (const name in keybinds) {
for (const slot of keybinds[name]) {
if (slot === null || slot === undefined) continue;
const {
key: bindKey,
ctrl: bindCtrl = false,
alt: bindAlt = false,
shift: bindShift = false
} = slot;
if (key == bindKey
&& ctrl == bindCtrl
&& alt == bindAlt
&& shift == bindShift
&& actions.hasOwnProperty(name)) {
return name;
}
}
}
return false;
}
function openSettings() {
function rowTemplate(name, id) {
return `
<span>${name}</span>
<input data-command="${id}" data-slot="0" data-key="" data-ctrl="0" data-alt="0" data-shift="0" type="text">
<input data-command="${id}" data-slot="1" data-key="" data-ctrl="0" data-alt="0" data-shift="0" type="text">
`;
}
function printRows() {
const arr = [];
for (const id in actions) {
if (actions[id].name) arr.push(rowTemplate(actions[id].name, id));
}
return arr.join('');
}
function clear(input) {
input.value = '';
input.dataset.key = '';
input.ctrl = false;
input.alt = false;
input.shift = false;
}
function renderSingleKeybind(input) {
function simplify(str) {
return str.replace(/^(Key|Digit)/, '');
}
const keyCombinations = [];
if (input.ctrl) keyCombinations.push('Ctrl');
if (input.alt) keyCombinations.push('Alt');
if (input.shift) keyCombinations.push('Shift');
if (input.dataset.key !== '') keyCombinations.push(simplify(input.dataset.key));
input.value = keyCombinations.join('+');
}
function renderAllKeybinds(wrapper) {
const panelWrapper = wrapper || document.getElementById(`${SCRIPT_ID}--panelWrapper`);
const keybinds = {...getActiveKeybinds(), ...getGlobalKeybinds()};
if (!panelWrapper) return;
// Reset input fields
for (const input of $$('[data-command]', panelWrapper)) clear(input);
// Populate input from storage
for (const name in keybinds) {
const slots = keybinds[name];
for (let i = 0; i < slots.length; i++) {
const input = $(` [data-command="${name}"][data-slot="${i}"]`, panelWrapper);
if (!slots[i] || !input || !slots[i].key) continue;
const {key, ctrl = false, alt = false, shift = false} = slots[i];
input.dataset.key = key;
input.ctrl = ctrl;
input.alt = alt;
input.shift = shift;
renderSingleKeybind(input);
}
}
}
function modifierLookup(which) {
return ({16: 'shift', 17: 'ctrl', 18: 'alt'}[which]);
}
function saveKeybind(input) {
const key = input.dataset.key;
const ctrl = input.ctrl;
const alt = input.alt;
const shift = input.shift;
const command = input.dataset.command;
const slot = parseInt(input.dataset.slot);
if (matchKeybind(key, ctrl, alt, shift)) {
// existing keybind
clear(input);
input.blur();
input.value = 'Keybind already in use';
return;
}
if (reservedKeys.includes(key)) {
// reserved key
clear(input);
input.blur();
input.value = 'Key is reserved';
return;
}
const presets = getStorage('keybinds');
const keybinds = (actions[command].global)
? presets['global']
: presets[getStorage('usePreset')];
if (!keybinds[command]) {
keybinds[command] = [];
}
if (key !== '') {
// set
keybinds[command][slot] = {key, ctrl, alt, shift};
input.blur();
} else {
// delete
delete keybinds[command][slot];
if (keybinds[command].every(val => val === null)) delete keybinds[command];
}
setStorage('keybinds', presets);
renderSingleKeybind(input);
}
function keydownHandler(e) {
e.preventDefault();
e.stopPropagation();
const input = e.target;
if (e.code == 'Escape' || e.code == 'Backspace' || e.code == 'Delete') {
clear(input);
saveKeybind(input);
return;
}
if (e.repeat || input.dataset.key !== '') {
return;
}
if (e.which >= 16 && e.which <= 18) {
input[modifierLookup(e.which)] = true;
renderSingleKeybind(input);
return;
}
input.dataset.key = e.code;
saveKeybind(input);
}
function keyupHandler(e) {
e.preventDefault();
e.stopPropagation();
const input = e.target;
if (e.which >= 16 && e.which <= 18 && !e.repeat && input.dataset.key == '') {
input[modifierLookup(e.which)] = false;
renderSingleKeybind(input);
}
}
const panelWrapper = document.createElement('div');
panelWrapper.id = `${SCRIPT_ID}--panelWrapper`;
panelWrapper.innerHTML = `
<div id="${SCRIPT_ID}--panel" class="ido">
<div class="${SCRIPT_ID}--header">
<b>Custom Shortcuts Settings</b>
<select id="${SCRIPT_ID}--preset-selector">
<option value="preset_1">Preset 1</option>
<option value="preset_2">Preset 2</option>
<option value="preset_3">Preset 3</option>
</select>
<button id="${SCRIPT_ID}--close-button" class="button">🗙</button>
</div>
<div class="${SCRIPT_ID}--body">
Esc/Backspace/Del to clear setting
<br>
<br>
<div class="${SCRIPT_ID}--table">
<span><b>Action</b></span>
<span><b>Slot 1</b></span>
<span><b>Slot 2</b></span>
${printRows()}
</div>
</div>
</div>
`;
for (const input of $$('[data-command]', panelWrapper)) {
// event handlers
input.addEventListener('keydown', keydownHandler);
input.addEventListener('keyup', keyupHandler);
// define getter and setters
for (const modifier of ['ctrl', 'alt', 'shift']) {
Object.defineProperty(input, modifier, {
set: function (val) {
this.dataset[modifier] = val ? '1' : '0';
},
get: function () {
return (this.dataset[modifier] == '1');
}
});
}
}
// selector
const selector = $(`#${SCRIPT_ID}--preset-selector`, panelWrapper);
selector.value = getStorage('usePreset');
selector.addEventListener('input', () => {
setStorage('usePreset', selector.value);
selector.blur();
renderAllKeybinds();
});
// close panel
panelWrapper.addEventListener('click', e => {
if (e.target == e.currentTarget ||
e.target.matches(`#${SCRIPT_ID}--close-button`)) {
panelWrapper.remove();
}
});
renderAllKeybinds(panelWrapper);
// fighting z-index with the ads banners
panelWrapper.style.zIndex = document.getElementsByTagName('*').length + 10;
document.body.appendChild(panelWrapper);
}
function keyHandler(e) {
const command = matchKeybind(e.code, e.ctrlKey, e.altKey, e.shiftKey);
const ownSettingsSelector = `.${SCRIPT_ID}--table input, #${SCRIPT_ID}--preset-selector`;
let stopPropagation = true;
let preventDefault = true;
if (!command) {
// keep things like ctrl + f working if combination is not rebound
preventDefault = false;
}
// By default not to run on site inputs
if (e.target.matches('input, textarea') || e.target.matches(ownSettingsSelector)) {
stopPropagation = false;
preventDefault = false;
}
if (command
&& (actions[command].constant || (e.type == 'keydown'))
&& (actions[command].repeat || !e.repeat)
&& (actions[command].input || !e.target.matches('input, textarea'))
&& !e.target.matches(ownSettingsSelector)) {
const o = actions[command].fn(e) || {};
if (o.hasOwnProperty('stopPropagation')) stopPropagation = o.stopPropagation;
if (o.hasOwnProperty('preventDefault')) preventDefault = o.preventDefault;
}
if (stopPropagation) e.stopPropagation();
if (preventDefault) e.preventDefault();
}
function init() {
GM_addStyle(CSS);
// Initialize localStorage on first run
if (localStorage.getItem(SCRIPT_ID) == null) localStorage.setItem(SCRIPT_ID, '{}');
if (getStorage('keybinds') == null) setStorage('keybinds', {
preset_1: presets.preset_1,
preset_2: presets.preset_2,
preset_3: presets.preset_3,
global: presets.global
});
if (getStorage('usePreset') == null) setStorage('usePreset', 'preset_1');
// 'capture' is set to true so that the event is dispatched to the handler
// before the native ones, so that the site shortcuts can be disabled
// by stopPropagation();
document.addEventListener('keydown', keyHandler, {capture: true});
document.addEventListener('keyup', keyHandler, {capture: true});
window.addEventListener('pagehide', function () {
// Disable highlight when navigating away from current page.
// Workaround for Firefox preserving page state when moving forward
// and back in history.
unhighlight($('.highlighted'));
if (unsafeWindow.selected_link) unsafeWindow.selected_link.click();
if (getPageType() == 'index') {
sessionStorage.lastIndex = window.location.href;
sessionStorage.scrollPosition = document.documentElement.scrollTop;
}
});
if (sessionStorage.scrollAfterLoad) {
window.scroll(0, sessionStorage.scrollPosition);
delete sessionStorage.scrollAfterLoad;
}
}
init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment