|
// ==UserScript== |
|
// @name Next Button (TJX) |
|
// @namespace http://tampermonkey.net/ |
|
// @version 2025-08-23 |
|
// @description Adds a button to navigate to the next product on TJX sites |
|
// @author You |
|
// @match https://tjmaxx.tjx.com/* |
|
// @match https://www.marshalls.com/* |
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=tjx.com |
|
// @grant none |
|
// @run-at document-idle |
|
// ==/UserScript== |
|
|
|
(function () { |
|
'use strict'; |
|
|
|
const BTN_ID = 'tjx-next-product-btn'; |
|
|
|
function getNextProductUrl(href = window.location.href) { |
|
const url = new URL(href); |
|
// url.pathname doesn't include query, so no need to split on "?" |
|
const parts = url.pathname.split('/').filter(Boolean); |
|
const last = parts[parts.length - 1]; |
|
|
|
// Product pages look like .../<PRODUCT_ID> |
|
const id = parseInt(last, 10); |
|
if (!Number.isNaN(id)) { |
|
parts[parts.length - 1] = String(id + 1); |
|
url.pathname = '/' + parts.join('/'); |
|
return url.toString(); |
|
} |
|
return null; |
|
} |
|
|
|
function ensureButton() { |
|
// Avoid adding on non-product pages |
|
const nextUrl = getNextProductUrl(); |
|
const existing = document.getElementById(BTN_ID); |
|
|
|
if (!nextUrl) { |
|
if (existing) existing.remove(); |
|
return; |
|
} |
|
|
|
if (existing) { |
|
existing.onclick = () => (window.location.href = nextUrl); |
|
return; |
|
} |
|
|
|
// Make sure body exists |
|
if (!document.body) { |
|
requestAnimationFrame(ensureButton); |
|
return; |
|
} |
|
|
|
const button = document.createElement('button'); |
|
button.id = BTN_ID; |
|
button.textContent = 'Next Product'; |
|
Object.assign(button.style, { |
|
position: 'fixed', |
|
top: '10px', |
|
right: '10px', |
|
padding: '10px 20px', |
|
backgroundColor: '#007BFF', |
|
color: '#FFF', |
|
border: 'none', |
|
borderRadius: '5px', |
|
cursor: 'pointer', |
|
zIndex: '999999', |
|
fontSize: '16px', |
|
}); |
|
button.onclick = () => (window.location.href = nextUrl); |
|
document.body.appendChild(button); |
|
} |
|
|
|
// Run immediately (document-idle) and also on SPA route changes |
|
function hookHistory() { |
|
const _push = history.pushState; |
|
const _replace = history.replaceState; |
|
|
|
history.pushState = function () { |
|
const ret = _push.apply(this, arguments); |
|
queueMicrotask(ensureButton); |
|
return ret; |
|
}; |
|
history.replaceState = function () { |
|
const ret = _replace.apply(this, arguments); |
|
queueMicrotask(ensureButton); |
|
return ret; |
|
}; |
|
window.addEventListener('popstate', () => queueMicrotask(ensureButton)); |
|
} |
|
|
|
// Initial run + SPA hooks |
|
ensureButton(); |
|
hookHistory(); |
|
|
|
// In case the site lazily mounts content, do a one-time short retry burst |
|
let retries = 10; |
|
const iv = setInterval(() => { |
|
ensureButton(); |
|
if (--retries <= 0) clearInterval(iv); |
|
}, 300); |
|
})(); |