Strava Text Auto-Selector

Automatically selects text in specific Strava elements and displays a notification near the cursor. Also allows right-click to copy text.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Strava Text Auto-Selector
// @namespace    typpi.online
// @version      1.0.9
// @description  Automatically selects text in specific Strava elements and displays a notification near the cursor. Also allows right-click to copy text.
// @author       Nick2bad4u
// @license      UnLicense
// @homepageURL  https://github.com/Nick2bad4u/UserScripts
// @grant        none
// @include      *://*.strava.com/activities/*
// @include      *://*.strava.com/athlete/training
// @icon         https://www.google.com/s2/favicons?sz=64&domain=strava.com
// ==/UserScript==

(function () {
	'use strict';

	// Log when the script starts
	console.log('Strava Text Auto-Selector script loaded.');

	// Wait for the page to fully load
	window.addEventListener('load', function () {
		console.log('Page fully loaded, initializing script.');

		// Delay script execution for 500 ms
		setTimeout(initializeScript, 500);
	});

	function initializeScript() {
		console.log('Initializing script after delay.');

		const selectors = [
			'#search-results > tbody > tr:nth-child(n) > td.view-col.col-title > a',
			'.summaryGrid .summaryGridDataContainer, .inline-stats strong, .inline-stats b',
			'#heading > div > div.row.no-margins.activity-summary-container > div.spans8.activity-summary.mt-md.mb-md > div.details-container > div > h1',
			'.ride .segment-effort-detail .effort-details table, .swim .segment-effort-detail .effort-details table',
			'.activity-description p:only-child',
			'.activity-description p:first-child',
		];
		const summarySelector = '.summaryGridDataContainer';

		// Function to add the event listener to target elements
		function addContextMenuListener(element) {
			console.log('Adding right-click event listener to element:', element);

			element.addEventListener('contextmenu', function (event) {
				console.log('Right-click detected on element:', element);

				event.preventDefault();
				console.log('Default right-click menu prevented.');

				const range = document.createRange();
				if (element.classList.contains('summaryGridDataContainer')) {
					const textNode = element.childNodes[0];
					range.selectNodeContents(textNode);
					console.log('Text range selected:', textNode.textContent);
				} else {
					range.selectNodeContents(element);
					console.log('Text range selected:', element.textContent);
				}

				const selection = window.getSelection();
				selection.removeAllRanges();
				selection.addRange(range);
				console.log('Text added to selection.');

				const copiedText = selection.toString();
				console.log('Text copied to clipboard:', copiedText);

				navigator.clipboard
					.writeText(copiedText)
					.then(() => {
						console.log('Clipboard write successful.');
						showNotification(event.clientX, event.clientY, 'Text Copied!');
					})
					.catch(() => {
						console.log('Clipboard write failed.');
						showNotification(event.clientX, event.clientY, 'Failed to Copy!');
					});
			});
		}

		// Query elements and add event listeners initially for the first three selectors
		selectors.forEach((selector) => {
			const elements = document.querySelectorAll(selector);
			console.log(`Found ${elements.length} elements for selector: ${selector}`);
			elements.forEach(addContextMenuListener);
		});

		// Function to handle the summaryGridDataContainer elements separately
		function handleSummaryGridDataContainer() {
			const elements = document.querySelectorAll(summarySelector);
			console.log(`Found ${elements.length} elements for selector: ${summarySelector}`);
			elements.forEach(addContextMenuListener);
		}

		// MutationObserver to detect changes in the DOM and add event listeners to new summaryGridDataContainer elements
		const observer = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				mutation.addedNodes.forEach((node) => {
					if (node.nodeType === Node.ELEMENT_NODE) {
						if (node.matches(summarySelector)) {
							addContextMenuListener(node);
						}
						node.querySelectorAll(summarySelector).forEach(addContextMenuListener);
					}
				});
			});
		});

		observer.observe(document.body, {
			childList: true,
			subtree: true,
		});
		console.log('MutationObserver set up to monitor the DOM for summaryGridDataContainer.');

		// Handle existing summaryGridDataContainer elements initially
		handleSummaryGridDataContainer();
	}

	function showNotification(x, y, message) {
		console.log('Displaying notification:', message);

		const notification = document.createElement('div');
		notification.textContent = message;
		notification.style.position = 'absolute';
		notification.style.left = `${x + 10}px`;
		notification.style.top = `${y + 10}px`;
		notification.style.background = 'rgba(0, 0, 0, 0.8)';
		notification.style.color = 'white';
		notification.style.padding = '5px 10px';
		notification.style.borderRadius = '5px';
		notification.style.fontSize = '12px';
		notification.style.zIndex = '1000';
		notification.style.pointerEvents = 'none';

		document.body.appendChild(notification);
		console.log('Notification added to DOM.');

		setTimeout(() => {
			notification.remove();
			console.log('Notification removed from DOM.');
		}, 2000);
	}
})();