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.15 // @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 = '.js-resizable-media-container .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('#taginput-fancy-tag_input') && $('.autocomplete')) { 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 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)); 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}); 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); })();