NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Derpibooru Custom Shortcuts
// @description Configurable shortcuts and enhanced keyboard navigation. "Ctrl+Shift+/" to open settings.
// @version 1.2.19
// @author Marker
// @license MIT
// @namespace https://github.com/marktaiwan/
// @homepageURL https://github.com/marktaiwan/Derpibooru-Custom-Shortcuts
// @supportURL https://github.com/marktaiwan/Derpibooru-Custom-Shortcuts/issues
// @match https://*.derpibooru.org/*
// @match https://*.trixiebooru.org/*
// @grant unsafeWindow
// @grant GM_openInTab
// @inject-into content
// @noframes
// ==/UserScript==
(function () {
'use strict';
let lastSelectedTag = null;
const SCRIPT_ID = 'markers_custom_shortcuts';
const THUMB_SELECTOR = '.media-list .media-box';
const TAG_SELECTOR = '.tag-list .tag.dropdown';
const CSS = `/* Generated by Custom Shortcuts */
#${SCRIPT_ID}--panelWrapper {
position: fixed;
top: 0px;
left: 0px;
z-index: 10;
display: flex;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
}
.${SCRIPT_ID}--header {
padding: 0px 5px;
}
.${SCRIPT_ID}--body {
width: 600px;
max-height: calc(100vh - 80px);
overflow: auto;
}
.${SCRIPT_ID}--table {
display: grid;
grid-template-columns: 1fr 150px 150px;
grid-column-gap: 5px;
grid-row-gap: 5px;
}
.${SCRIPT_ID}--table input {
font-size: 12px;
align-self: center;
text-align: center;
}
.media-box.highlighted, .tag.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 = {
default: {
prev: [{key: 'KeyJ'}],
next: [{key: 'KeyK'}],
source: [{key: 'KeyS'}],
random: [{key: 'KeyR'}],
upvote: [{key: 'KeyU'}],
favorite: [{key: 'KeyF'}],
toIndex: [{key: 'KeyI'}],
tagEdit: [{key: 'KeyL'}],
},
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'}],
openSelected: [{key: 'KeyE'}],
openInNewTab: [],
openInBackground: [{key: 'KeyE', shift: true}],
prev: [{key: 'KeyZ'}],
next: [{key: 'KeyX'}],
source: [],
random: [{key: 'KeyR'}],
upvote: [{key: 'KeyG', shift: true}],
favorite: [{key: 'KeyF', shift: true}],
toIndex: [],
tagEdit: [{key: 'KeyL'}],
tagSubmit: [{key: 'KeyL', ctrl: true}],
toggleScale: [{key: 'KeyV'}],
toggleVideo: [{key: 'KeyN'}],
toggleSound: [{key: 'KeyM'}],
focusSearch: [{key: 'KeyS', shift: true}],
focusComment: [{key: 'KeyC', shift: true}],
refreshCommentList: [{key: 'KeyR', shift: true}],
historyBack: [{key: 'KeyA', shift: true}],
historyForward: [{key: 'KeyD', shift: true}],
},
preset_2: {},
preset_3: {},
/* Keybinds that are applied globally */
global: {
useDefault: [{key: 'Backquote', alt: true}],
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 keyboard selection mode',
fn: () => {
const highlighted = $('.media-box.highlighted, .tag-list .tag.dropdown.highlighted');
if (highlighted) {
unhighlight(highlighted);
} else {
const prevSelected = $(`.media-box[data-image-id="${sessionStorage.lastSelectedThumb}"]`);
if (prevSelected && isVisible(prevSelected)) {
highlight(prevSelected);
} else if (lastSelectedTag && isVisible(lastSelectedTag)) {
highlight(lastSelectedTag);
} else {
highlight(getFirstVisibleOrClosest(THUMB_SELECTOR) || getFirstVisibleOrClosest(TAG_SELECTOR));
}
}
}
},
openSelected: {
name: 'Open selected',
fn: () => {
const selected = $('.media-box.highlighted, .tag.highlighted');
if (selected) click('.media-box__content a, a.tag__name', selected);
}
},
openInNewTab: {
name: 'Open selected in new tab',
fn: () => {
const selected = $('.media-box.highlighted, .tag.highlighted');
if (selected) {
const anchor = $('.media-box__content a, a.tag__name', selected);
window.open(anchor.href, '_blank');
}
}
},
openInBackground: {
name: 'Open selected in background tab',
fn: () => {
const selected = $('.media-box.highlighted, .tag.highlighted');
if (selected) {
const anchor = $('.media-box__content a, a.tag__name', selected);
GM_openInTab(anchor.href, {active: false});
}
}
},
prev: {
name: 'Previous page/image',
fn: () => click('.js-prev')
},
next: {
name: 'Next page/image',
fn: () => click('.js-next')
},
source: {
name: 'Open source URL',
fn: () => click('.js-source-link')
},
random: {
name: 'Random image',
fn: () => click('.js-rand')
},
upvote: {
name: 'Upvote image',
fn: () => {
let mediaBox = $('.media-box.highlighted');
if (mediaBox) {
click('.media-box__header a.interaction--upvote', mediaBox);
} else {
mediaBox = $('.media-box:hover');
if (mediaBox) {
click('.media-box__header a.interaction--upvote', mediaBox);
} else {
click('.block__header a.interaction--upvote');
}
}
}
},
favorite: {
name: 'Favorite image',
fn: () => {
let mediaBox = $('.media-box.highlighted');
if (mediaBox) {
click('.media-box__header a.interaction--fave', mediaBox);
} else {
mediaBox = $('.media-box:hover');
if (mediaBox) {
click('.media-box__header a.interaction--fave', mediaBox);
} else {
click('.block__header a.interaction--fave');
}
}
}
},
toIndex: {
name: 'Go to index page containing the image being displayed',
fn: () => click('.js-up')
},
tagEdit: {
name: 'Open tags for editing',
fn: () => {
click('#edit-tags');
return {preventDefault: true};
}
},
tagSubmit: {
name: 'Save tags',
fn: e => {
const target = e.target;
const submitButtonSelector = '.js-imageform:not(.hidden) #tags-form #edit_save_button';
let stopPropagation = true;
let preventDefault = true;
if ((target.matches('#taginput-fancy-tag_input') && (e.ctrlKey || e.altKey))
|| !target.matches('.input, input, textarea')) {
click(submitButtonSelector);
} else {
stopPropagation = false;
preventDefault = false;
}
return {stopPropagation, preventDefault};
},
input: true
},
toggleScale: {
name: 'Cycle through image scaling',
fn: () => click('#image-display')
},
toggleVideo: {
name: 'Play/pause webms',
fn: () => {
const video = $('video#image-display, .highlighted .image-container video');
if (!video) return;
if (video.paused) {
video.play();
} else {
video.pause();
}
}
},
toggleSound: {
name: 'Mute/unmute webms',
fn: () => {
const video = $('video#image-display, .highlighted .image-container video');
if (!video) return;
video.muted = !video.muted;
// SIN: Compatibility hack with Webm Volume Toggle because I
// don't know how to fix it properly in the other script
const container = video.closest('.video-container');
if (!container) return;
const button = $('.volume-toggle-button', container);
container.dataset.isMuted = video.muted ? '1' : '0';
if (container.dataset.isMuted == '0') {
button.classList.add('fa-volume-up');
button.classList.remove('fa-volume-off');
} else {
button.classList.add('fa-volume-off');
button.classList.remove('fa-volume-up');
}
}
},
focusSearch: {
name: 'Focus on search field',
fn: () => {
const searchField = $('.header__input--search');
if (searchField) {
searchField.focus();
searchField.select();
return {preventDefault: true};
}
}
},
focusComment: {
name: 'Focus on comment form',
fn: () => {
const commentField = $('[name="comment[body]"], [name="post[body]"], [name="message[body]"]');
if (commentField) {
commentField.focus();
return {preventDefault: true};
}
}
},
refreshCommentList: {
name: 'Refresh comment list',
fn: () => click('#js-refresh-comments')
},
historyBack: {
name: 'Go back in browser history',
fn: () => window.history.back()
},
historyForward: {
name: 'Go forward in browser history',
fn: () => window.history.forward()
},
useDefault: {
name: 'Global: Switch to default keybinds',
fn: () => switchPreset('default'),
global: true
},
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 => {
const target = e.target;
let stopPropagation = true;
if (target.matches('#q, #taginput-fancy-tag_input') && $('.autocomplete:not(.hidden)')) {
stopPropagation = false;
} else {
// default behavior
target.blur();
}
return {stopPropagation};
},
input: true
},
toggleSettings: {
fn: () => {
const panel = $(`#${SCRIPT_ID}--panelWrapper`);
if (panel) {
panel.remove();
} else {
openSettings();
}
}
}
};
const onReady = (() => {
const callbacks = [];
document.addEventListener('DOMContentLoaded', () => callbacks.forEach(fn => fn()), {once: true});
return fn => {
if (document.readyState == 'loading') {
callbacks.push(fn);
} else {
fn();
}
};
})();
const smoothscroll = (function () {
let startTime = null;
let pendingFrame = null;
let keydown = {up: false, down: false, left: false, right: false};
function reset() {
startTime = null;
keydown = {up: false, down: false, left: false, right: false};
unsafeWindow.cancelAnimationFrame(pendingFrame);
}
function noKeyDown() {
return !(keydown.up || keydown.down || keydown.left || keydown.right);
}
function step(timestamp) {
if (noKeyDown() || !document.hasFocus()) {
reset();
return;
}
startTime = startTime || timestamp;
const elapsed = timestamp - startTime;
const maxVelocity = 40; // px/frame
const easeDuration = 250; // ms
const scale = window.devicePixelRatio;
const velocity = ((elapsed > easeDuration)
? maxVelocity
: maxVelocity * (elapsed / easeDuration)
) / scale;
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 length = Math.hypot(x, y);
if (length > 0) {
window.scrollBy(
Math.round(x / length * velocity),
Math.round(y / length * velocity)
);
}
pendingFrame = window.requestAnimationFrame(step);
}
return function (direction, type) {
switch (type) {
case 'keydown':
if (noKeyDown()) pendingFrame = window.requestAnimationFrame(step);
keydown[direction] = true;
break;
case 'keyup':
keydown[direction] = false;
if (noKeyDown()) reset();
break;
}
};
})();
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);
return ((mid > 0 + margin && mid < clientHeight - margin)
|| (top < 0 + margin && bottom > clientHeight - margin));
}
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 highlight(selection, setSmooth = true) {
if (!selection) return;
unhighlight($('.highlighted'));
$('.media-box__content a, .tag a.tag__name', selection).focus({preventScroll: true, focusVisible: false});
selection.classList.add('highlighted');
if (!isVisible(selection)) {
if (setSmooth) {
selection.scrollIntoView({behavior: 'smooth', block: 'center'});
} else {
selection.scrollIntoView({behavior: 'auto', block: 'nearest'});
}
}
if (selection.matches('.media-box')) {
sessionStorage.lastSelectedThumb = selection.dataset.imageId;
} else {
lastSelectedTag = selection;
}
}
function unhighlight(ele) {
if (!ele) return;
ele.classList.remove('highlighted');
document.activeElement.blur();
}
function scroll(direction, event) {
const type = event.type;
const selection = $('.highlighted');
if (selection && type == 'keydown') {
keyboardNav(direction, selection, !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);
}
const rect = getRect(highlighted);
const originalPos = {x: rect.x, y: rect.y};
const margin = Math.max(4, rect.height / 4); // px
const selector = (highlighted.matches('.media-box')) ? THUMB_SELECTOR : TAG_SELECTOR;
const nodeList = $$(selector);
let ele = highlighted;
let index = [...nodeList].indexOf(ele);
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
&& Object.prototype.hasOwnProperty.call(actions, 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);
input.disabled = (getStorage('usePreset') == 'default');
}
// 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="">
<div class="${SCRIPT_ID}--header block__header">
<b>Custom Shortcuts Settings</b>
<select id="${SCRIPT_ID}--preset-selector">
<option value="default">Default</option>
<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 block__tab">
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);
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, .input') || 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, .input'))
&& !e.target.matches(ownSettingsSelector)) {
const o = actions[command].fn(e) || {};
if (Object.prototype.hasOwnProperty.call(o, 'stopPropagation')) stopPropagation = o.stopPropagation;
if (Object.prototype.hasOwnProperty.call(o, 'preventDefault')) preventDefault = o.preventDefault;
}
if (stopPropagation) e.stopPropagation();
if (preventDefault) e.preventDefault();
}
function init() {
if (!document.getElementById(`${SCRIPT_ID}-style`)) {
const styleElement = document.createElement('style');
styleElement.setAttribute('type', 'text/css');
styleElement.id = `${SCRIPT_ID}-style`;
styleElement.innerHTML = CSS;
document.body.insertAdjacentElement('afterend', styleElement);
}
// Initialize localStorage on first run
if (localStorage.getItem(SCRIPT_ID) == null) localStorage.setItem(SCRIPT_ID, '{}');
if (getStorage('keybinds') == null) setStorage('keybinds', {
default: presets.default,
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});
// Disable highlight when navigating away from current page.
// Workaround for Firefox preserving page state when moving forward
// and back in history.
window.addEventListener('pagehide', function () {
unhighlight($('.highlighted'));
});
}
onReady(init);
})();