Steam Community modals as links

Converts pseudo-links from Steam Community into real anchor tags. Instead of opening them as iframes inside modal dialogs, they now open as a full page, they can be opened in another tab, and their URLs can be copied.

// ==UserScript==
// @name            Steam Community modals as links
// @namespace       https://denilson.sa.nom.br/
// @author          Denilson Sá Maia
// @version         1.0
// @description     Converts pseudo-links from Steam Community into real anchor tags. Instead of opening them as iframes inside modal dialogs, they now open as a full page, they can be opened in another tab, and their URLs can be copied.
// @match           *://steamcommunity.com/*
// @run-at          document-end
// @icon            https://steamcommunity.com/favicon.ico
// @license         MIT
// ==/UserScript==

(function () {
	"use strict";

	//////////////////////////////////////////////////
	// Convenience functions

	// Returns a new function that will call the callback without arguments
	// after timeout milliseconds of quietness.
	function debounce(callback, timeout = 500) {
		let id = null;
		return function() {
			clearTimeout(id);
			id = setTimeout(callback, timeout);
		};
	}

	const active_mutation_observers = [];

	// Returns a new MutationObserver that observes a specific node.
	// The observer will be immediately active.
	function debouncedMutationObserver(rootNode, callback, timeout = 500) {
		const func = debounce(callback, timeout);
		func();
		const observer = new MutationObserver(func);
		observer.observe(rootNode, {
			subtree: true,
			childList: true,
			attributes: false,
		});
		active_mutation_observers.push(observer);
		return observer;
	}

	// Adds a MutationObserver to each root node matched by the CSS selector.
	function debouncedMutationObserverSelectorAll(rootSelector, callback, timeout = 500) {
		for (const root of document.querySelectorAll(rootSelector)) {
			debouncedMutationObserver(root, callback, timeout);
		}
	}

	function stopAllMutationObservers() {
		for (const mo of active_mutation_observers) {
			mo.disconnect();
		}
		active_mutation_observers.length = 0;
	}

	//////////////////////////////////////////////////

	function main() {
			debouncedMutationObserverSelectorAll("#AppHubContent", function() {
				for (const card of document.querySelectorAll(".apphub_Card.modalContentLink[data-modal-content-url]")) {
					const a = document.createElement("a");
					a.href = card.dataset.modalContentUrl;
					a.append(...card.childNodes);
					card.append(a);
					card.classList.remove('modalContentLink');
				}
			});
	}

	main();

})();