Skip to content

Instantly share code, notes, and snippets.

@gibson042
Last active September 8, 2023 21:52
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gibson042/c9b3406bc54f55726ec4 to your computer and use it in GitHub Desktop.
Save gibson042/c9b3406bc54f55726ec4 to your computer and use it in GitHub Desktop.
Focus Search user script
// ==UserScript==
// @name Focus Search
// @namespace https://github.com/gibson042
// @description Responds to <{Command,Ctrl}+/> by attempting to find and focus a page's search box.
// @source https://gist.github.com/gibson042/c9b3406bc54f55726ec4
// @updateURL https://gist.github.com/gibson042/c9b3406bc54f55726ec4/raw/focus_search.user.js
// @downloadURL https://gist.github.com/gibson042/c9b3406bc54f55726ec4/raw/focus_search.user.js
// @version 0.4.0
// @date 2023-09-08
// @author Richard Gibson <@gmail.com>
// @include *
// ==/UserScript==
//
// **COPYRIGHT NOTICE**
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
// For the CC0 Public Domain Dedication, see
// <https://creativecommons.org/publicdomain/zero/1.0/>.
//
// **END COPYRIGHT NOTICE**
//
//
// Changelog:
// 0.4.0 (2023-09-08)
// * New: CC0 Public Domain Dedication.
// 0.3.3 (2023-08-29)
// * Fixed: Event interception in Google Maps.
// 0.3.2 (2022-10-12)
// * Fixed: Remove errant console.log.
// 0.3.1 (2022-09-29)
// * Improved: Identified more fallback click elements.
// * Fixed: Avoid clicking submit buttons in the fallback click path.
// 0.3.0 (2022-09-29)
// * New: Added a fallback to click a search button etc. in the absence of a search input.
// * Improved: Added support for identifying search elements by ARIA data.
// * Fixed: Started ignoring disabled/unrendered elements.
// 0.2.3 (2022-03-21)
// * Improved: Better OS and user script manager compatibility.
// 0.2.2 (2020-12-15)
// Updated: modernized the implementation
// 0.2.1 (2020-04-05)
// Improved: click() the matched element before focus() & select() for dynamic changes à la dvd.netflix.com
// 0.2.0 (2015-07-17)
// Improved: Included input[type=search] (high priority) and textarea (low priority)
// 0.1.5 (2013-11-07)
// Fixed: Google Chrome unsafe window retrieval
// 0.1.4 (2011-04-21)
// Updated: basic support for Google Chrome
// 0.1.3 (2011-02-21)
// Fixed: Firefox/Windows security bug
// 0.1.2 (2011-02-19)
// Updated: update checking with meta.js
// 0.1.1 (2010-07-03)
// improved: expanded matching id fields
// 0.1 (2009-07-28)
// original release
(function() {
"use strict";
const SCRIPT_NAME = "Focus Search";
// Match only specific element id/name patterns.
const ID_RE = /^q$|query|search/i;
const NAME_RE = /^q$|query|search|^for/i;
// Keep track of the last focused element.
var gelLastFocused = null;
// Listen for <{Command,Ctrl}+/>, generally at the end of bubbling
// but with special cases that intercept at the beginning of capturing.
if (
// Google Maps
/(?:^|[.])google(?:[.]|$)/i.test(location.host) && /\bmaps?\b/.test(location.href)
) {
window.addEventListener(
"keydown",
evt => focusSearch(evt, () => { evt.stopImmediatePropagation(); }),
true,
);
} else {
window.addEventListener("keydown", evt => focusSearch(evt));
}
function focusSearch( evt, onMatch = () => {} ) {
if ( !((evt.ctrlKey ^ evt.metaKey) && evt.key === "/" && !evt.altKey && !evt.shiftKey) ) {
return;
}
// Collect candidate elements in priority order:
// 1. input[type=search], *[role=searchbox]
// 2. *[role=search] text-input
// 3. text-input
// 4. textarea (excluded from fallback cycling)
var candidates = new Set();
var hasLayout = el => {
var rect = el.getBoundingClientRect();
if ( !rect ) return false;
return rect.height > 0 || rect.width > 0;
};
document.querySelectorAll("input[type=search]:not(:disabled), *[role=searchbox]:not(:disabled)").forEach(
el => hasLayout(el) && candidates.add(el)
);
document.querySelectorAll("*[role=search] input:not(:disabled)").forEach(
el => (el.type || "text") === "text" && hasLayout(el) && candidates.add(el)
);
document.querySelectorAll("input:not(:disabled)").forEach(
el => (el.type || "text") === "text" && hasLayout(el) && candidates.add(el)
);
var idxCutoff = candidates.size;
document.querySelectorAll("textarea:not(:disabled)").forEach(el => hasLayout(el) && candidates.add(el));
var arrCandidates = Array.from(candidates);
// Use "id" and "name" attributes to find the first candidate following our last selection (if any).
var elMatch = null;
var idxNext = 0;
for ( let i = 0, el; (el = arrCandidates[i]); i++ ) {
// Collect the first match or the first match after the last-focused element, whichever comes later.
if ( (!elMatch || idxNext) && (NAME_RE.test(el.name) || ID_RE.test(el.id)) ) {
elMatch = el;
// If it's the first-ever match or we're in replacement mode, stop looking.
if ( !gelLastFocused || idxNext ) break;
}
// If we encounter the last-focused element, enter replacement mode.
if ( !idxNext && el.isSameNode(gelLastFocused) ) {
idxNext = i + 1;
}
}
// If we found no search inputs, fall back on clicking plausible search icons/buttons/etc.
if ( !idxCutoff ) {
for ( let el of document.querySelectorAll("[aria-label*=search i], [class*=search i], [id*=search i]") ) {
var attrs = [el.getAttribute("aria-label"), el.getAttribute("class"), el.id, el.getAttribute("type")].filter(val => !!val);
if ( attrs.some(str => /(?:\b|\P{Upper}(?=S))[Ss][Uu][Bb][Mm][Ii][Tt](?:\b|(?<=t)\P{Lower})/u.test(str)) ) continue;
if ( attrs.some(str => /(?:\b|\P{Upper}(?=S))[Ss][Ee][Aa][Rr][Cc][Hh](?:\b|(?<=h)\P{Lower})/u.test(str)) && hasLayout(el) ) {
console.log("[" + SCRIPT_NAME + "]", "fallback click", el);
el.click();
}
}
return onMatch();
}
// If we matched nothing, just cycle through text inputs.
if ( !elMatch ) {
elMatch = arrCandidates[Math.min(idxNext, idxCutoff) % idxCutoff];
console.log("[" + SCRIPT_NAME + "]", "fallback focus", elMatch);
}
// Focus the match.
if ( elMatch ) {
gelLastFocused = elMatch;
elMatch.click();
elMatch.focus();
elMatch.select();
return onMatch(elMatch);
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment