Google Maps Button

Adds the Maps link back to the Google Search navigation bar.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==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());
  });
})();