Skip to content

Instantly share code, notes, and snippets.

@ccjmne
Last active September 30, 2018 00:22
Show Gist options
  • Save ccjmne/d1f25305d0fb437c4e922da7db840e27 to your computer and use it in GitHub Desktop.
Save ccjmne/d1f25305d0fb437c4e922da7db840e27 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Google Search Hotkeys
// @namespace https://gist.github.com/ccjmne
// @version 2.0.0
// @description Navigate results from Google seaches using arrow keys or numbers from 1 through 9, switch results tabs (Images, Videos, News...) using hotkeys, focus the search input field with forward-slash, and more! Use Shift-? for help.
// @author ccjmne <[email protected]> (https://github.com/ccjmne)
// @include *://www.google.tld/search*
// @downloadURL https://gist.githubusercontent.com/ccjmne/d1f25305d0fb437c4e922da7db840e27/raw/google-search-hotkeys.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
/*
* Utils
*/
function addStyle(css) {
(sheet => sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length))((document.getElementById("GM_addStyleBy8626") || (function() {
const style = document.createElement('style');
style.type = 'text/css';
style.id = "GM_addStyleBy8626";
document.head.appendChild(style);
return style;
})()).sheet);
}
function throttle(fn, delay = 0) {
let lastCall = 0;
return function (...args) {
const now = (new Date).getTime();
if ( now - lastCall >= delay ) {
lastCall = now;
return fn(...args);
}
}
}
// https://stackoverflow.com/a/6021027
function updateUrlParameter(key, value) {
return (uri => {
var i = uri.indexOf('#');
var hash = i === -1 ? '' : uri.substr(i);
uri = i === -1 ? uri : uri.substr(0, i);
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if ( uri.match(re) ) {
uri = uri.replace(re, '$1' + key + "=" + value + '$2');
} else {
uri = uri + separator + key + "=" + value;
}
return uri + hash;
})(window.location.href);
}
function getUrlParameter(key) {
return (window.location.search.match(new RegExp(`(?<=[&?]${key}=)[^&]*`)) || [])[0];
}
const onceSome = selector => new Promise(resolve => {
const _ = setInterval(() => {
if ( document.querySelectorAll(selector).length > 0 ) {
clearInterval(_);
resolve(document.querySelectorAll(selector));
}
}, 100);
});
/*
* Help panel
*/
const backdrop = document.createElement('div');
backdrop.id = 'ccjmne--google-search-hotkeys--backdrop';
backdrop.innerHTML = `
<div id="ccjmne--google-search-hotkeys--anchor">
<div id="ccjmne--google-search-hotkeys--tilt-origin">
<div id="ccjmne--google-search-hotkeys--help-shadow"></div>
<div id="ccjmne--google-search-hotkeys--help-card-container">
<div id="ccjmne--google-search-hotkeys--help-card">
<div id="ccjmne--google-search-hotkeys--help-card-title"><span>Google Search Hotkeys</span><small>by&nbsp;<a href="https://github.com/ccjmne">ccjmne</a></small></div>
<table></table>
</div>
</div>
</div>
</div>`;
const tiltOrigin = backdrop.querySelector('#ccjmne--google-search-hotkeys--tilt-origin'),
helpCard = backdrop.querySelector('#ccjmne--google-search-hotkeys--help-card'),
helpTable = helpCard.querySelector('table');
tiltOrigin.addEventListener('mouseleave', () => (tiltOrigin.style.transform = ``));
tiltOrigin.addEventListener('mousemove', throttle(e => {
const { left, top, right, bottom } = tiltOrigin.getBoundingClientRect();
const x = e.clientX - (left + right) / 2,
y = e.clientY - (top + bottom) / 2;
const amplitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / Math.max(helpCard.clientWidth, helpCard.clientHeight);
tiltOrigin.style.transform = `rotate3d(${ Math.round(y) }, ${ -Math.round(x) }, 0, ${ amplitude * 10 }deg)`;
}, 100));
function toggleHelp(shown) {
if( shown && !backdrop.parentNode ) {
document.body.prepend(backdrop);
backdrop.animate({ opacity: [0, 1] }, { duration: 200, easing: 'ease-out' });
helpCard.animate({ transform: ['translateY(-50%) rotateX(80deg)', 'translateY(2%) rotateX(0)', 'translateY(0)'], opacity: [0, 1, 1], offset: [0, .8] }, { duration: 300, delay: 100, easing: 'ease-out', fill: 'backwards' });
} else if( !shown && backdrop.parentNode ) {
tiltOrigin.style.transform = ``;
backdrop.animate({ opacity: [1, 0] }, { duration: 200, easing: 'ease-out' }).onfinish = () => backdrop.parentNode.removeChild(backdrop);
helpCard.animate({ transform: ['scale(1)', 'scale(1.1)', 'scale(.5)'] }, { duration: 200, easing: 'ease-out' });
}
}
backdrop.addEventListener('click', () => toggleHelp(false));
helpCard.addEventListener('click', e => e.stopPropagation());
[[
{ desc: `Focus [next] result`, hotkey: `Down` },
{ desc: `Focus [previous] result`, hotkey: `Up` },
{ desc: `Navigate to [next] page`, hotkey: `Right` },
{ desc: `Navigate to [previous] page`, hotkey: `Left` }
], [
{ desc: `[Open] focused`, hotkey: `Space|Enter` },
{ desc: `[Open] focused in [new tab]`, hotkey: `Ctrl-[Space|Enter]` },
{ desc: `Open #[1] to #[9]`, hotkey: `1..9` },
{ desc: `Open #[1] to #[9] in [new tab]`, hotkey: `Ctrl-[1..9]` }
], [
{ desc: `Focus [search] field`, hotkey: `/` },
{ desc: `Search [all]`, hotkey: `a` },
{ desc: `Search [videos]`, hotkey: `v` },
{ desc: `Search [images]`, hotkey: `i` },
{ desc: `Search [news]`, hotkey: `n` }
], [
{ desc: `[Show] help`, hotkey: `Shift-?` },
{ desc: `[Close] help`, hotkey: `Escape` }
]].forEach(block => block.forEach((op, idx) => {
const helpItem = document.createElement('tr');
helpTable.appendChild(helpItem);
helpItem.innerHTML = `
<td>${ op.desc.replace(/\[([^\]]+)\]/g, (unused, d) => `<em>${ d }</em>`) }</td>
<td><kbd>${op.hotkey.replace(/[[\]+|-]|\.\./g, s => `</kbd>${ { '..': ' to ', '-': '+', '|': ' or ', '[': ' [', ']': '] ' }[s] }<kbd>`)}</kbd></td>`;
if( idx === 0 ) {
helpItem.classList.add('ccjmne--google-search-hotkeys--new-section');
}
}));
/*
* Main code
*/
const indicator = document.createElement('span');
indicator.id = 'ccjmne--google-search-hotkeys--indicator';
indicator.innerHTML = '▶';
const tooltip = document.createElement('div');
tooltip.id = 'ccjmne--google-search-hotkeys--tooltip';
Object.assign(tooltip, {
concealTimer: null,
reveal: e => {
if( tooltip.parentNode !== e.target ) {
clearTimeout(tooltip.concealTimer);
e.target.appendChild(tooltip);
tooltip.concealTimer = setTimeout(tooltip.conceal, 3000);
tooltip.animate({
transform: ['translate(-50%, 0) scale(0)', 'translate(-100%, 1em) scale(1.1)', 'translate(-100%, 1em) scale(1)'],
offset: [0, .8]
}, { duration: 200, easing: 'ease-out', fill: 'both' });
}
},
conceal: () => (tooltip.animate({
transform: ['translate(-100%, 1em) scale(1)', 'translate(-100%, 1em) scale(1.1)', 'translate(-100%, 1em) scale(.5)'],
opacity: [1, 1, 0],
offset: [0, .3]
}, { duration: 200, easing: 'ease-in', fill: 'backwards' }).onfinish = () => tooltip.parentNode && tooltip.parentNode.removeChild(tooltip)) && clearTimeout(tooltip.concealTimer)
});
tooltip.addEventListener('click', () => { tooltip.conceal(); toggleHelp(true); });
indicator.addEventListener('mouseenter', tooltip.reveal);
const opsMap = {
27: /* Escape -> close help */ () => toggleHelp(false),
65: /* a -> 'All' tab */ () => (window.location.href = updateUrlParameter('tbm', '')),
73: /* i -> 'Images' tab */ () => (window.location.href = updateUrlParameter('tbm', 'isch')),
77: /* m -> 'Maps' tab */ () => null, // TODO: impl
78: /* n -> 'Vews' tab */ () => (window.location.href = updateUrlParameter('tbm', 'nws')),
86: /* v -> 'Videos' tab */ () => (window.location.href = updateUrlParameter('tbm', 'vid'))
};
document.body.onkeydown = (e => {
/*
* Don't mess with typing into the Search field, or w/ Ctrl and Shift keys, unless:
* Ctrl-Space (32)
* Ctrl-Shift-Space (32)
* Ctrl-ArrowUp (38) -> to allow browsing up while maintainting Ctrl and/or Shift
* Ctrl-ArrowDown (40) -> to allow browsing down while maintainting Ctrl and/or Shift
* Ctrl-[1..9] (49..57)
* Ctrl-[Numpad1..Numpad9] (97..105)
* Shift-? (191)
*/
if( e.srcElement === document.querySelector('input[title=Search]')
|| (e.shiftKey && !~[32, 38, 40, 191].indexOf(e.keyCode))
|| (e.ctrlKey && !~[32, 38, 40].concat([...Array(9).keys()].map(x => 49 + x)).concat([...Array(9).keys()].map(x => 97 + x)).indexOf(e.keyCode)) ) {
return;
}
// Results browsing only supported on 'All', 'Videos' and 'News' tabs
if( ~([37, 38, 39, 40].indexOf(e.keyCode)) && !~([undefined, null, '', 'nws', 'vid'].indexOf(getUrlParameter('tbm'))) ) {
return;
}
(op => typeof op === 'function' && Promise.resolve(op(e)).then(() => e.preventDefault()))(opsMap[e.keyCode]);
});
// Once results are listed, additionally handle browsing them
onceSome('#main a > h3:first-child, #main h3 > a:first-child').then((function (nodes) {
Object.assign(this, {
cur: nodes.length > 0 ? 0 : -1,
results: [].map.call(nodes, x => ({ container: x.parentNode.parentNode, link: x.closest('a') })),
go: e => this.results[this.cur] && this.results[this.cur].link.dispatchEvent(new MouseEvent('click', e)),
focus: idx => (this.cur = idx) === -1 ? (input => {
input.focus();
input.setSelectionRange(input.value.length, input.value.length);
indicator.parentNode && indicator.parentNode.removeChild(indicator);
})(document.querySelector('input[title=Search]')) : (result => {
result.link.focus();
result.container.prepend(indicator);
result.container.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
indicator.animate([{ transform: 'translateX(-50px)' }, { transform: 'translateX(0)' }], { duration: 100, easing: 'ease-out' });
})(this.results[idx])
});
this.results.forEach((result, idx) => {
Object.assign(result.container.style, { position: 'relative', overflow: 'visible' });
const numberIndicator = document.createElement('div');
numberIndicator.classList.add('ccjmne--google-search-hotkeys--number-indicator');
numberIndicator.innerHTML = idx + 1;
numberIndicator.addEventListener('mouseenter', tooltip.reveal);
result.container.prepend(numberIndicator);
});
this.focus(this.cur);
// 1 through 9 on the bar and on the numpad -> follow corresponding result
[... Array(9).keys()].forEach(x => (op => Object.assign(opsMap, { [49 + x]: op, [97 + x]: op }))(e => { this.focus(x); this.go(e); }));
Object.assign(opsMap, {
32: /* space -> follow focused */ e => this.go({ ctrlKey: e.ctrlKey, shiftKey: e.shiftKey }),
191: /* slash -> search input
* shift-? -> show help */ e => e.shiftKey ? toggleHelp(true) : this.focus(-1),
37: /* left -> previous page */ () => window.location.replace(updateUrlParameter('start', Math.max(parseInt(getUrlParameter('start') || '0') - this.results.length, 0))),
38: /* up -> previous result */ () => this.focus(this.cur > 0 ? this.cur - 1 : this.results.length - 1),
39: /* right -> next page */ () => window.location.replace(updateUrlParameter('start', parseInt(getUrlParameter('start') || '0') + this.results.length)),
40: /* down -> next result */ () => this.focus(++this.cur % this.results.length)
});
}).bind({}));
/*
* Styling
*/
addStyle(`#ccjmne--google-search-hotkeys--indicator { position: absolute; left: -1.5em; font-size: 18px; color: crimson; }`);
addStyle(`.ccjmne--google-search-hotkeys--number-indicator { position: absolute; left: -1.5em; transform: translate(-100%, 20%); font-family: monospace; }`);
addStyle(`#ccjmne--google-search-hotkeys--indicator + .ccjmne--google-search-hotkeys--number-indicator { display: none; }`);
addStyle(`#ccjmne--google-search-hotkeys--indicator, .ccjmne--google-search-hotkeys--number-indicator { cursor: help; }`);
addStyle(`#ccjmne--google-search-hotkeys--tooltip { display: flex; align-items: center; position: absolute; top: 0; left: -.5em; height: 30px; padding: 0 1em; font-family: sans-serif; font-size: 13px; white-space: nowrap; border: 1px solid slategrey; border-radius: 10px 0 10px 10px; background-color: white; color: slategrey; transition: box-shadow .2s ease-out; }`);
addStyle(`#ccjmne--google-search-hotkeys--tooltip:hover, #ccjmne--google-search-hotkeys--tooltip:active { border-color: darkslategrey; color: darkslategrey; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }`);
addStyle(`#ccjmne--google-search-hotkeys--tooltip:active { background-color: whitesmoke; }`);
addStyle(`#ccjmne--google-search-hotkeys--tooltip::before { content: "What's this?" }`);
addStyle(`#ccjmne--google-search-hotkeys--backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 999; background-color: rgba(0, 0, 0, .35); }`);
addStyle(`#ccjmne--google-search-hotkeys--anchor { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); perspective: 1000px; }`);
addStyle(`#ccjmne--google-search-hotkeys--tilt-origin { transform-style: preserve-3d; transition: transform .2s ease-out; }`);
addStyle(`#ccjmne--google-search-hotkeys--tilt-origin:hover #ccjmne--google-search-hotkeys--help-shadow { opacity: .5 }`);
addStyle(`#ccjmne--google-search-hotkeys--tilt-origin:hover #ccjmne--google-search-hotkeys--help-card { box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); }`);
addStyle(`#ccjmne--google-search-hotkeys--help-shadow { position: absolute; top: 0; right: 0; bottom: 0; left: 0; border-radius: 5px; background-color: slategrey; transform: scale(1.7) translateZ(-650px); opacity: 0; transition: opacity .2s ease-out; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card-container { perspective: 2000px; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card { overflow: hidden; box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); border-radius: 3px; font-size: 16px; background-color: ghostwhite; color: #444; transition: box-shadow .2s ease-out; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card-title { display: flex; align-items: center; justify-content: space-evenly; padding: 2vh 2vw; border-bottom: 1px solid crimson; font-size: 18px; background-color: slategrey; color: ghostwhite; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table { border-collapse: collapse; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table tr:nth-child(2n) { background-color: rgba(0, 0, 0, .1); }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table:last-child tr:last-child td { border-bottom: 2vh solid ghostwhite; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table tr:hover { background-color: #9aa6b1; color: black; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table tr:hover kbd { color: crimson; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table tr.ccjmne--google-search-hotkeys--new-section td { border-top: 2vh solid ghostwhite; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table tr td { padding: .3em 0 .3em 2vw; white-space: nowrap; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card table tr td:last-child { padding-right: 2vw; text-align: right; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card a, #ccjmne--google-search-hotkeys--help-card a:visited { color: ghostwhite; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card em { color: crimson; font-weight: normal; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card kbd { display: inline-block; padding: .1em .6em; box-shadow: 0 1px 0px rgba(0, 0, 0, .2); border: 1px solid #ccc; border-radius: 3px; font-family: monospace; white-space: nowrap; background-color: ghostwhite; cursor: default; }`);
addStyle(`#ccjmne--google-search-hotkeys--help-card kbd:empty { display: none; }`);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment