No More Collapsed Google Contacts Details

Stop hiding Google Contacts details under collapsed sections

// ==UserScript==
// @name          No More Collapsed Google Contacts Details
// @version       1.0.2
// @description   Stop hiding Google Contacts details under collapsed sections
// @namespace     http://userscripts.org/users/377329
// @author        Jonathan Brochu (http://userscripts.org/users/377329)
// @license       GPLv3 or later (http://www.gnu.org/licenses/gpl-3.0.en.html)
// @match         https://contacts.google.com/person/*
// @include       /^https:\/\/contacts\.google\.com\/u\/[1-9][0-9]*\/person\/.+$/
// @icon          https://ssl.gstatic.com/images/branding/product/2x/contacts_2022_64dp.png
// @grant         none
// ==/UserScript==

/***
 * History:
 *
 * 1.0.2  Change made:
 *        - Added an @include rule to also match contact detail pages on
 *          secondary Google account sessions.
 *        NOTE: For now, only targets English locales. Feel free to suggest
 *              additional ones, or else you can always fork this script to
 *              include new ones.
 *        (2024-01-07)
 * 1.0.1  Changes made:
 *        - Simplified payload code.
 *        - Now keeping the DOM listener live instead of disabling it.
 *        (2023-08-20)
 * 1.0.0  First release. (2023-08-19)
 *
 */

(function() {
	'use strict';

	// constants
	var USERSCRIPT_NAME = 'Don\'t Collapse Google Contacts Details',
		// Change values below to reflect your active Google Contacts UI language
		L_EXPAND_MORE = 'More',
		L_COLLAPSE_LESS = 'Less';

	// reference some outside objects
	var console = window.console || (function() {
		if (typeof(unsafeWindow) == 'undefined') return { 'log': function() {} };
		return unsafeWindow.console;
	})();

	// Polyfills, just in case
	// - document.getElementById()
	document.getElementById||(document.getElementById=function(a){for(var b=1<arguments.length?arguments[1]:document.documentElement,c=[b];c.length;){var d=c.shift();if(d.id===a)return d;d.children.length&&(c=c.concat(Array.prototype.slice.call(d.children)))}});
	// - document.querySelectorAll()
	//     eslint-disable-next-line
	document.querySelectorAll||(document.querySelectorAll=function(e){var t,n=document.createElement("style"),o=[];for(document.documentElement.firstChild.appendChild(n),document._qsa=[],n.styleSheet.cssText=e+"{x-qsa:expression(document._qsa && document._qsa.push(this))}",window.scrollBy(0,0),n.parentNode.removeChild(n);document._qsa.length;)(t=document._qsa.shift()).style.removeAttribute("x-qsa"),o.push(t);return document._qsa=null,o}),document.querySelector||(document.querySelector=function(e){e=document.querySelectorAll(e);return e.length?e[0]:null});

	// DOM helpers: $(), $$() & $1st()
	var $ = function(id) {
		return document.getElementById(id);
	}, $$ = function(selector /*, rootEl */) {
		return (arguments.length > 1 && arguments[1] && arguments[1].nodeType == 1
				? arguments[1]
				: document
			).querySelectorAll(selector);
	}, $1st = function(selector /*, rootEl */) {
		return (arguments.length > 1 && arguments[1] && arguments[1].nodeType == 1
				? arguments[1]
				: document
			).querySelector(selector);
	};
	// Shorthand to delete an element
	var deleteElem = function(el) {
		if (el instanceof HTMLElement && el.parentNode) {
			el.parentNode.removeChild(el);
			return true;
		}
		return false;
	};

	// DOM changes listener
	// Source: https://stackoverflow.com/a/14570614/3865919
	var observeDOM = (function() {

		var EVT_DOM_INSERT = 'DOMNodeInserted',
			EVT_DOM_REMOVED = 'DOMNodeRemoved',
			MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

		return function(callback /*, rootElement */) {
			if (typeof callback !== 'function') return;
			var rootElement =
				(arguments.length > 1 && arguments[1] && arguments[1].nodeType == 1
				 ? arguments[1]
				 : document.documentElement);
			if (MutationObserver) {
				// Define a new observer
				var muObserver = new MutationObserver(callback);
				// Start observing changes
				muObserver.observe(rootElement, { childList: true, subtree: true });
				return muObserver.disconnect;
			} else if (window.addEventListener) { // Fallback
				rootElement.addEventListener(EVT_DOM_INSERT, callback, false);
				rootElement.addEventListener(EVT_DOM_REMOVED, callback, false);
				return function() {
					rootElement.removeEventListener(EVT_DOM_INSERT, callback, false);
					rootElement.removeEventListener(EVT_DOM_REMOVED, callback, false);
				};
			} else throw new Error('[' + USERSCRIPT_NAME + '] Creating of a DOM listener failed');
		}
	})();

	var payloadFn,
		cancelFn,
		timeoutID,
		timeoutDelay = 500,
		loaded;

	console.log('[' + USERSCRIPT_NAME + '] Installing listener for DOM updates, waiting until they cease.');

	// Install a DOM listener, since contact details are loaded asynchronously,
	//  to notify us once DOM changes have ceased
	cancelFn = observeDOM(function(muLst) {
		if (timeoutID) {
			clearTimeout(timeoutID);
		}
		timeoutID = setTimeout(payloadFn, timeoutDelay);
	});

	// Our page mods
	payloadFn = function() {
		// Check if we have [More]/[Less] link containers, indicating collapsed sections
		var elCtnr,
			elBtn;
		while (elCtnr = $1st('div.iy8V6c')) {
			elBtn = $1st('button', elCtnr);
			if (elBtn.textContent == L_EXPAND_MORE) {
				if (!(loaded=(loaded !== undefined))) console.log('[' + USERSCRIPT_NAME + '] Page has finished loading, applying mods.');
				// Activate the [More] link and delete its container
				elBtn.click();
				deleteElem(elCtnr);
			} else if (elBtn.textContent == L_COLLAPSE_LESS) {
				// Remove the [Less] link and its container
				deleteElem(elCtnr);
				// Stop observing DOM change events
				///cancelFn();
				// ... or not
			}
		}
	};
})();