Last active
September 8, 2023 21:52
-
-
Save gibson042/c9b3406bc54f55726ec4 to your computer and use it in GitHub Desktop.
Focus Search user script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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