// ==UserScript==
// @name Change clicked link's color
// @license MIT
// @namespace http://tampermonkey.net/
// @author ngtuan.vn28@gmail.com
// @homepageURL https://greasyfork.org/vi/scripts/501244-change-clicked-link-s-color
// @version 1.2.6
// @description Change color of clicked links in body with color presets and custom hex input
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @icon https://cdn-icons-png.flaticon.com/512/4906/4906292.png
// ==/UserScript==
(function() {
'use strict';
// Config
let p_color_clicked = GM_getValue('colorClicked', '#800080');
let p_apply_all = GM_getValue('applyAll', true);
let p_apply_domains = GM_getValue('applyDomains', '');
// Variables
const style_id = "clicked-link-color-style";
const css_a_clicked = `
a:visited:not(nav a):not(.nav a):not(.navbar a):not(.menu a):not(.navigation a),
a:visited:not(nav a):not(.nav a):not(.navbar a):not(.menu a):not(.navigation a) *,
a.custom-visited,
a.custom-visited * {
color: %COLOR% !important;
}`;
const colorPresets = {
'Purple': '#800080',
'Red': '#FF0000',
'Blue': '#0000FF',
'Green': '#008000',
'Orange': '#FFA500',
'Pink': '#FFC0CB',
'Brown': '#A52A2A',
'Gray': '#808080',
'Cyan': '#00FFFF',
'Magenta': '#FF00FF',
'Lime': '#00FF00'
};
// Functions
function isDomainApplied(domains, site) {
if (p_apply_all) return true;
if (domains.trim() === '') return false;
let domainList = domains.split(",");
return domainList.some(domain => site.includes(domain.trim()));
}
function addStyle(css) {
let style = document.getElementById(style_id);
if (style === null) {
let head = document.getElementsByTagName("head")[0];
style = document.createElement("style");
style.setAttribute("id", style_id);
style.setAttribute("type", "text/css");
head.appendChild(style);
}
style.textContent = css;
}
function assignColor(css, color) {
return css.replace(/%COLOR%/ig, color);
}
function main() {
let url = document.documentURI;
let css = assignColor(css_a_clicked, p_color_clicked);
if (isDomainApplied(p_apply_domains, url)) {
addStyle(css);
addClickListener();
}
}
function addClickListener() {
document.body.addEventListener('click', (e) => {
if (e.target.tagName === 'A') {
e.target.classList.add('custom-visited');
}
});
}
function observeDOMChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
main();
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
function createDialog(id, content) {
const dialog = document.createElement('dialog');
dialog.className = 'custom-dialog';
dialog.id = id;
dialog.innerHTML = `
<form method="dialog">
${content}
</form>
`;
document.body.appendChild(dialog);
dialog.style.position = 'fixed';
dialog.style.left = '50%';
dialog.style.top = '50%';
dialog.style.transform = 'translate(-50%, -50%)';
dialog.style.maxWidth = '400px';
dialog.style.width = '90%';
dialog.style.border = '2px solid #007bff';
dialog.style.borderRadius = '8px';
dialog.style.padding = '16px';
dialog.style.backgroundColor = '#fff';
dialog.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
dialog.style.overflow = 'hidden';
dialog.showModal();
return dialog;
}
function showColorSelector() {
const content = `
<h3>Change Clicked Link's Color</h3>
<div class="section">
<label for="colorPreset">Choose a preset color:</label>
<select id="colorPreset" style="width: 100%;">
<option value="">-- Choose a preset color --</option>
${Object.entries(colorPresets).map(([name, value]) =>
`<option value="${value}" style="background-color: ${value}; color: white;">${name} (${value})</option>`
).join('')}
</select>
</div>
<div class="section">
<label for="customColor">Or enter a custom hex color:</label>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="text" id="customColor" placeholder="#RRGGBB" pattern="^#[0-9A-Fa-f]{6}$" style="flex: 1; height: 2em; border: 2px solid #007bff; border-radius: 4px;">
<input type="color" id="colorPicker" value="${p_color_clicked}" style="height: 2em; border: 2px solid #007bff; border-radius: 4px;">
</div>
</div>
<div class="button-container" style="margin-top: 20px;">
<button type="submit" id="applyBtn" style="background-color: #007bff; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; margin-right: 10px;">Apply</button>
<button type="button" id="cancelBtn" style="background-color: #ccc; color: #000; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer;">Cancel</button>
</div>
`;
const dialog = createDialog('colorDialog', content);
const colorPreset = dialog.querySelector('#colorPreset');
const customColor = dialog.querySelector('#customColor');
const colorPicker = dialog.querySelector('#colorPicker');
const applyBtn = dialog.querySelector('#applyBtn');
const cancelBtn = dialog.querySelector('#cancelBtn');
colorPreset.value = p_color_clicked;
customColor.value = p_color_clicked;
colorPicker.value = p_color_clicked;
colorPreset.addEventListener('change', function() {
if (this.value) {
customColor.value = this.value;
colorPicker.value = this.value;
}
});
colorPicker.addEventListener('input', function() {
customColor.value = this.value;
});
cancelBtn.addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => {
if (dialog.returnValue !== 'cancel') {
const newColor = customColor.value;
if (/^#[0-9A-Fa-f]{6}$/i.test(newColor)) {
p_color_clicked = newColor;
GM_setValue('colorClicked', p_color_clicked);
main();
} else {
alert("Invalid color code. Please use hex format (e.g., #800080).");
}
}
});
}
function showDomainSettings() {
const content = `
<h3>Manage Enabled Domains</h3>
<div class="section">
<label for="enabledSites">Enable on these sites (one per line):</label>
<textarea id="enabledSites" rows="10" style="width: 100%; border: 2px solid #007bff; border-radius: 4px; padding: 8px; box-sizing: border-box;">${p_apply_domains.replace(/,/g, '\n')}</textarea>
</div>
<div class="section" style="margin-top: 10px;">
<label>
<input type="checkbox" id="enableAllSites" ${p_apply_all ? 'checked' : ''}> Apply to all websites
</label>
</div>
<div class="button-container" style="display: flex; justify-content: space-between; margin-top: 20px;">
<button type="button" id="addThisSite" style="background-color: #28a745; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer;">Add this site</button>
<div class="button-group" style="display: flex; gap: 10px;">
<button type="submit" style="background-color: #007bff; color: #fff; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer;">Save</button>
<button type="button" id="cancelBtn" style="background-color: #ccc; color: #000; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer;">Cancel</button>
</div>
</div>
`;
const dialog = createDialog('domainDialog', content);
const enabledSitesTextarea = dialog.querySelector('#enabledSites');
const addThisSiteBtn = dialog.querySelector('#addThisSite');
const saveBtn = dialog.querySelector('button[type="submit"]');
const cancelBtn = dialog.querySelector('#cancelBtn');
const enableAllSitesCheckbox = dialog.querySelector('#enableAllSites');
addThisSiteBtn.addEventListener('click', () => {
const currentDomain = window.location.hostname.replace(/^www\./, '');
const domainList = enabledSitesTextarea.value.split('\n').map(site => site.trim()).filter(Boolean);
if (!domainList.includes(currentDomain)) {
domainList.push(currentDomain);
enabledSitesTextarea.value = domainList.join('\n');
}
});
cancelBtn.addEventListener('click', () => dialog.close('cancel'));
dialog.addEventListener('close', () => {
if (dialog.returnValue !== 'cancel') {
const newEnabledSites = enabledSitesTextarea.value.split('\n').map(site => site.trim()).filter(Boolean);
const newEnableAllSites = enableAllSitesCheckbox.checked;
if (JSON.stringify(newEnabledSites) !== JSON.stringify(p_apply_domains.split(',')) ||
newEnableAllSites !== p_apply_all) {
p_apply_domains = newEnabledSites.join(',');
p_apply_all = newEnableAllSites;
GM_setValue('applyDomains', p_apply_domains);
GM_setValue('applyAll', p_apply_all);
main();
}
}
});
}
// Menu commands
GM_registerMenuCommand("🎨 Change clicked link's color", showColorSelector, "C");
GM_registerMenuCommand("🌐 Domain settings", showDomainSettings, "D");
// Run main function immediately
main();
// Observe DOM changes
observeDOMChanges();
})();