Greasy Fork is available in English.
Adds the Maps link back to the Google Search navigation bar.
// ==UserScript==
// @name Google Maps Button
// @namespace local.google-maps-button
// @version 1.5
// @description Adds the Maps link back to the Google Search navigation bar.
// @match https://*.google.com/search*
// @icon https://www.google.com/images/branding/product/ico/web_maps_icon_32dp.ico
// @license MIT
// @grant none
// @run-at document-start
// ==/UserScript==
(() => {
"use strict";
const BUTTON_ID = "google-maps-button-userscript";
const UNIT_ATTR = "data-google-maps-button-userscript-unit";
const MAP_IMAGE_ATTR = "data-google-maps-button-userscript-map-image";
let updateScheduled = false;
function mapsUrl() {
const searchParams = new URLSearchParams(window.location.search);
const input = document.querySelector('textarea[name="q"], input[name="q"]');
const query = searchParams.get("q") || input?.value || "";
const url = new URL("https://www.google.com/maps/search/");
url.searchParams.set("api", "1");
if (query) {
url.searchParams.set("query", query);
}
return url.href;
}
function searchUrl(anchor) {
if (!anchor?.href) return null;
try {
return new URL(anchor.href, window.location.href);
} catch {
return null;
}
}
function isDefaultSearchAnchor(anchor) {
const url = searchUrl(anchor);
return Boolean(url)
&& url.pathname === "/search"
&& !url.searchParams.has("udm")
&& !url.searchParams.has("tbm");
}
function isSearchModeAnchor(anchor) {
const url = searchUrl(anchor);
return Boolean(url)
&& url.pathname === "/search"
&& (url.searchParams.has("udm") || url.searchParams.has("tbm"));
}
function tabContainerFor(unit) {
return unit?.closest('[role="list"], nav, [role="navigation"]') || unit?.parentElement || null;
}
function tabAnchorsIn(container) {
return [...container.querySelectorAll('[role="listitem"] > a[href]')];
}
function findPrimaryTabContainers() {
const containers = [];
document.querySelectorAll('[role="listitem"] > a[href]').forEach((anchor) => {
if (!isSearchModeAnchor(anchor)) return;
const container = tabContainerFor(anchor.closest('[role="listitem"]'));
if (container && !containers.includes(container)) {
containers.push(container);
}
});
return containers
.map((container, index) => ({
container,
index,
score: tabAnchorsIn(container).filter(isSearchModeAnchor).length * 100
+ (container.querySelector('[role="listitem"] [aria-current="page"], [role="listitem"] a[aria-disabled="true"]') ? 10 : 0)
+ (tabAnchorsIn(container).some(isDefaultSearchAnchor) ? 5 : 0),
}))
.sort((a, b) => b.score - a.score || a.index - b.index)
.map(({ container }) => container);
}
function hasSearchModeSiblings(unit) {
const container = tabContainerFor(unit);
return Boolean(container)
&& tabAnchorsIn(container).some(isSearchModeAnchor);
}
function choosePrimaryTabUnit(units) {
return units
.map((unit, index) => ({
unit,
index,
score: (hasSearchModeSiblings(unit) ? 100 : 0)
+ (unit.querySelector('[aria-current="page"], a[aria-disabled="true"]') ? 10 : 0),
}))
.sort((a, b) => b.score - a.score || a.index - b.index)[0]?.unit ?? null;
}
function findDefaultTabUnit() {
for (const container of findPrimaryTabContainers()) {
const defaultUnit = tabAnchorsIn(container)
.find(isDefaultSearchAnchor)
?.closest('[role="listitem"]');
if (defaultUnit) return defaultUnit;
const currentUnit = [...container.querySelectorAll('[role="listitem"]')]
.find((unit) => unit.querySelector('[aria-current="page"], a[aria-disabled="true"]'));
if (currentUnit) return currentUnit;
}
const linked = [...document.querySelectorAll('[role="listitem"] > a[href]')]
.filter(isDefaultSearchAnchor)
.map((anchor) => anchor.closest('[role="listitem"]'))
.filter(Boolean);
if (linked.length) return choosePrimaryTabUnit(linked);
const currentTabs = [...document.querySelectorAll('[role="listitem"]')]
.filter((unit) => unit.querySelector('a[aria-disabled="true"] [aria-current="page"]'));
return choosePrimaryTabUnit(currentTabs);
}
function findPlacement() {
const reference = findDefaultTabUnit();
return reference
? { reference, position: "after" }
: null;
}
function findTabUnit(reference) {
return reference.matches('[role="listitem"]')
? reference
: reference.closest('[role="listitem"]');
}
function replaceLabel(anchor) {
const walker = document.createTreeWalker(anchor, NodeFilter.SHOW_TEXT);
let textNode;
while ((textNode = walker.nextNode())) {
if (textNode.nodeValue.trim()) {
textNode.nodeValue = textNode.nodeValue.replace(/\S(?:.*\S)?/, "Maps");
return;
}
}
anchor.textContent = "Maps";
}
function placeUnit(placement, mapsUnit) {
const { referenceUnit, position } = placement;
if (position === "after") {
if (referenceUnit.nextElementSibling !== mapsUnit) {
referenceUnit.after(mapsUnit);
}
} else {
if (mapsUnit.nextElementSibling !== referenceUnit) {
referenceUnit.before(mapsUnit);
}
}
}
function isMapImage(image) {
const { width, height } = image.getBoundingClientRect();
if (width < 120 || height < 100) return false;
const source = image.currentSrc || image.src;
if (!source) return false;
if (source.startsWith("data:image/") && isRightSidePreview(image)) {
return true;
}
try {
const url = new URL(source, window.location.href);
return url.hostname === "maps.googleapis.com"
|| url.hostname === "maps.gstatic.com"
|| url.pathname.includes("/maps/");
} catch {
return false;
}
}
function isRightSidePreview(image) {
const imageRect = image.getBoundingClientRect();
for (let element = image.parentElement; element; element = element.parentElement) {
const rect = element.getBoundingClientRect();
const sameHeight = Math.abs(rect.top - imageRect.top) < 2
&& Math.abs(rect.bottom - imageRect.bottom) < 2;
const containsImage = rect.left <= imageRect.left && rect.right >= imageRect.right;
const sideBySide = rect.width > imageRect.width * 1.5 && rect.width < imageRect.width * 2.2;
const rightSide = imageRect.left + imageRect.width / 2 > rect.left + rect.width / 2;
if (sameHeight && containsImage && sideBySide && rightSide) return true;
}
return false;
}
function updateMapImages() {
document.querySelectorAll("img").forEach((image) => {
if (!isMapImage(image)) return;
image.setAttribute(MAP_IMAGE_ATTR, "");
image.style.cursor = "pointer";
image.tabIndex = 0;
image.setAttribute("role", "link");
});
}
function addOrUpdateButton() {
const existingButton = document.getElementById(BUTTON_ID);
if (existingButton) {
existingButton.href = mapsUrl();
const placement = findPlacement();
const mapsUnit = existingButton.closest(`[${UNIT_ATTR}]`);
if (placement && mapsUnit) {
placement.referenceUnit = findTabUnit(placement.reference);
if (placement.referenceUnit) {
placeUnit(placement, mapsUnit);
}
}
return;
}
const placement = findPlacement();
if (!placement) return;
const referenceUnit = findTabUnit(placement.reference);
if (!referenceUnit) return;
placement.referenceUnit = referenceUnit;
const mapsUnit = referenceUnit.cloneNode(true);
const mapsAnchor = mapsUnit.matches("a") ? mapsUnit : mapsUnit.querySelector("a");
if (!mapsAnchor) return;
mapsUnit.querySelectorAll("[id]").forEach((el) => el.removeAttribute("id"));
mapsUnit.removeAttribute("id");
mapsAnchor.id = BUTTON_ID;
mapsAnchor.href = mapsUrl();
mapsAnchor.removeAttribute("aria-disabled");
mapsAnchor.removeAttribute("aria-current");
mapsAnchor.removeAttribute("data-ved");
mapsUnit.querySelectorAll("[aria-current]").forEach((el) => el.removeAttribute("aria-current"));
mapsUnit.querySelectorAll("[selected]").forEach((el) => el.removeAttribute("selected"));
replaceLabel(mapsAnchor);
mapsUnit.setAttribute(UNIT_ATTR, "");
placeUnit(placement, mapsUnit);
}
function scheduleUpdate() {
if (updateScheduled) return;
updateScheduled = true;
requestAnimationFrame(() => {
updateScheduled = false;
addOrUpdateButton();
updateMapImages();
});
}
addOrUpdateButton();
updateMapImages();
new MutationObserver(scheduleUpdate).observe(document, {
childList: true,
subtree: true,
});
window.addEventListener("pageshow", scheduleUpdate);
document.addEventListener("click", (event) => {
const image = event.target.closest?.(`img[${MAP_IMAGE_ATTR}]`);
if (!image || image.closest("a[href]")) return;
window.location.assign(mapsUrl());
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") return;
const image = event.target.closest?.(`img[${MAP_IMAGE_ATTR}]`);
if (!image || image.closest("a[href]")) return;
event.preventDefault();
window.location.assign(mapsUrl());
});
})();