Greasy Fork is available in English.
Copies Contact Name, Email, Phone, and Address in one click!
// ==UserScript==
// @name Copy Google Contact
// @version 1.8
// @description Copies Contact Name, Email, Phone, and Address in one click!
// @match https://contacts.google.com/*
// @namespace https://greasyfork.org/en/users/922168-mark-zinzow
// @author Mark Zinzow
// @author Gemini
// @grant none
// @license MIT
// ==/UserScript==
/* eslint-disable spaced-comment */
// Educational & Debug Edition
// A heavily commented, high-contrast, fully logged script demonstrating how to navigate Single Page Applications.
(function() {
'use strict';
// =========================================================================
// 1. THE CONTROL CENTER
// =========================================================================
// We use levels to control how much "noise" our script makes in the console.
// Level 1 = Major events only (like finding a new person or copying data).
// Level 2 = Verbose mode (the "heartbeat" that logs every time the page blinks).
const DEBUG_LEVEL = 0; // Change this to 1 or 2 to follow along in the Dev Tools Console!
// These variables act as our script's memory.
let lastSeenName = null; // Remembers the name of the last contact we looked at.
let buttonRef = null; // Keeps a direct link to our custom button so we know if it gets detached.
// A helper tool to print neat, timestamped messages in the browser's Developer Console.
function debugLog(level, action, message, data = '') {
if (DEBUG_LEVEL >= level) {
// Grab the current time and chop off the date for a clean timestamp
const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
console.log(`[${timestamp}] 🔍 ${action.padEnd(15)} | ${message}`, data !== '' ? data : '');
}
}
// =========================================================================
// 2. THE GHOST BUSTER (Visibility Checker)
// =========================================================================
// Modern web pages (Single Page Applications) don't always delete old data when
// you click to a new page; they just hide it invisibly in the background to save time.
// Standard code will accidentally grab data from these hidden "ghosts."
// This function searches the page but strictly ignores anything that isn't physically
// visible on the user's screen.
function getVisibleElement(selector) {
const nodes = document.querySelectorAll(selector);
for (let node of nodes) {
// 'offsetParent' checks if the browser is currently drawing the element on the screen.
if (node.offsetParent !== null || (node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0)) {
return node; // We found the real, visible one!
}
}
return null; // Nothing visible found yet.
}
// Searches specifically inside the current contact's box to see if our button is there.
function getVisibleButton(parentContainer) {
if (!parentContainer) return null;
return parentContainer.querySelector('#gemini-copy-essentials');
}
// =========================================================================
// 3. THE DETECTIVE (State Checker)
// =========================================================================
// This function runs every time the webpage twitches. It figures out where we are
// and decides if it needs to take action.
function checkDOMState() {
// Find the visible Name on the screen.
const nameEl = getVisibleElement('#details-header-name');
const currentName = nameEl ? nameEl.textContent.trim() : null;
// If the page is still loading and no name is visible, stop here and wait.
if (!nameEl) return;
// Look at the box holding the name, and check if our button is inside it.
const parentContainer = nameEl.parentElement;
const existingBtn = getVisibleButton(parentContainer);
// --- NAVIGATION DETECTION ---
// If the name we see now is different from the last one we saved, the user must have navigated!
if (currentName && currentName !== lastSeenName) {
debugLog(1, "NAV DETECTED", `Name changed from "${lastSeenName}" to "${currentName}"`);
lastSeenName = currentName; // Update our memory
// Visual Highlight: Make the active target obvious with high-contrast named colors
nameEl.style.backgroundColor = 'LightYellow';
nameEl.style.color = 'Black';
nameEl.style.padding = '2px 6px';
nameEl.style.borderRadius = '4px';
nameEl.style.border = '1px solid Gold';
}
// --- THE HEARTBEAT (Verbose Logging) ---
// These are the detective clues! If DEBUG_LEVEL is 2, this prints a report
// on the exact status of our button every time the page settles down.
const buttonReport = {
foundInVisibleContainer: !!existingBtn,
isOurSavedRef: existingBtn === buttonRef,
isConnectedToPage: buttonRef ? buttonRef.isConnected : false
};
if (DEBUG_LEVEL >= 2 && currentName) {
debugLog(2, "DOM SETTLED", `Checking state for ${currentName}...`, buttonReport);
}
// --- THE TRIGGER ---
// If we are looking at a real person, but our button isn't there, it's time to build it!
if (!existingBtn) {
debugLog(1, "ACTION", `Button missing for ${currentName}. Injecting!`);
injectButton(parentContainer);
}
}
// =========================================================================
// 4. THE BUILDER (Button Injector & Data Scraper)
// =========================================================================
function injectButton(parentContainer) {
// Create a brand new button out of thin air
const btn = document.createElement('button');
btn.id = 'gemini-copy-essentials';
btn.textContent = '📋 Copy Essentials';
// Style the button using plain English named colors
Object.assign(btn.style, {
marginLeft: '20px',
padding: '6px 14px',
backgroundColor: 'MediumBlue', // A nice, standard Google-like blue
color: 'White', // White text for high contrast
border: '2px solid Black', // Sharp border to stand out against light grey backgrounds
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '600'
});
// Tell the button what to do when someone clicks it
btn.onclick = (e) => {
e.preventDefault(); // Stop the click from doing anything else by accident
e.stopPropagation();
debugLog(1, "COPY CLICKED", "Starting data extraction...");
// Step 1: Grab the visible name
const currentNameEl = getVisibleElement('#details-header-name');
const name = currentNameEl ? currentNameEl.textContent.trim() : 'N/A';
debugLog(2, "DATA FETCH", `Name extracted:`, name);
// Step 2: Grab the visible email by looking for Google's specific invisible "aria-label"
const emailBtn = getVisibleElement('button[aria-label^="Copy email"]');
const email = emailBtn ? emailBtn.getAttribute('data-to-copy') : 'N/A';
debugLog(2, "DATA FETCH", `Email extracted:`, email);
// Step 3: Grab the visible phone number and strip out weird invisible formatting characters
const phoneBtn = getVisibleElement('button[aria-label^="Copy phone"]');
let phone = phoneBtn ? phoneBtn.getAttribute('data-to-copy') : 'N/A';
phone = phone.replace(/[\u200B-\u200D\uFEFF\u202A-\u202C]/g, ''); // RegEx magic to clean text
debugLog(2, "DATA FETCH", `Phone extracted:`, phone);
// Step 4: Grab the visible address. If it has multiple lines, flatten it into one line with commas.
const addressBtn = getVisibleElement('button[aria-label^="Copy address"]');
let address = addressBtn ? addressBtn.getAttribute('data-to-copy') : 'N/A';
if (address !== 'N/A') {
address = address.split('\n')
.map(part => part.trim())
.filter(part => part.length > 0)
.join(', ');
}
debugLog(2, "DATA FETCH", `Address extracted:`, address);
// Piece it all together into a clean block of text
const finalString = `Name: ${name}\nEmail: ${email}\nPhone: ${phone}\nAddress: ${address}`;
// Send it to the computer's clipboard
navigator.clipboard.writeText(finalString).then(() => {
debugLog(1, "SUCCESS", "Data copied to clipboard!");
// Temporarily change the button to show success
const originalText = btn.textContent;
btn.textContent = '✅ Copied!';
btn.style.backgroundColor = 'ForestGreen'; // Green for success
// Wait 1.5 seconds, then change it back
setTimeout(() => {
btn.textContent = originalText;
btn.style.backgroundColor = 'MediumBlue';
}, 1500);
});
};
// Physically attach the button to the webpage
parentContainer.appendChild(btn);
buttonRef = btn; // Save our leash reference
}
// =========================================================================
// 5. THE WATCHDOG (MutationObserver)
// =========================================================================
// This is the engine of the script. It tells the browser, "Let me know the instant
// anything on the webpage changes."
let timeoutId;
const observer = new MutationObserver(() => {
// Because a page loading fires hundreds of changes a second, we use a "debounce".
// We cancel the timer if changes are still happening rapidly.
clearTimeout(timeoutId);
// We only run our Detective function once the page has been quiet for 1/4 of a second.
timeoutId = setTimeout(() => {
checkDOMState();
}, 250);
});
// Start watching the entire body of the webpage for any additions or removals
observer.observe(document.body, { childList: true, subtree: true });
// Kick things off when the script first loads
debugLog(1, "INIT", "Userscript loaded and observing.");
setTimeout(checkDOMState, 500);
})();