Warning: fopen(/www/sites/update.greasyfork.icu/index/store/temp/baef35ec93867109b24ddcaf49d68f66.js): failed to open stream: No space left on device in /www/sites/update.greasyfork.icu/index/scriptControl.php on line 65
// ==UserScript==
// @name Ocado + and - controlls into basket
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Adds reliable + / - buttons to Ocado basket product tiles (SPA-aware, robust binding to React buttons)
// @author pepepepepe
// @match https://ww2.ocado.com/orders/*/details
// @grant none
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/541557/Ocado%20%2B%20and%20-%20controlls%20into%20basket.user.js
// @updateURL https://update.greasyfork.icu/scripts/541557/Ocado%20%2B%20and%20-%20controlls%20into%20basket.meta.js
// ==/UserScript==
(function () {
'use strict';
const SPA_CHECK_INTERVAL = 500;
const MAX_RETRIES = 10;
let lastUrl = location.href;
function waitForReactButtonReady(button, retries = 0) {
return new Promise((resolve, reject) => {
const tryClick = () => {
const originalValue = button.getAttribute('aria-disabled');
button.click();
// After click, check if quantity changed (React responded)
setTimeout(() => {
if (button.getAttribute('aria-disabled') !== originalValue || retries >= MAX_RETRIES) {
resolve();
} else {
requestIdleCallback
? requestIdleCallback(() => waitForReactButtonReady(button, retries + 1).then(resolve))
: setTimeout(() => waitForReactButtonReady(button, retries + 1).then(resolve), 300);
}
}, 200);
};
tryClick();
});
}
function injectControls() {
const productTiles = document.querySelectorAll('[data-test^="tile-fop-wrapper"]');
productTiles.forEach(tile => {
const productId = tile.getAttribute('data-test')?.split(':')[1];
if (!productId || tile.querySelector('.tm-functional-controls')) return;
const incBtn = document.querySelector(`div[data-synthetics*="${productId}"] button[data-test="counter:increment"]`);
const decBtn = document.querySelector(`div[data-synthetics*="${productId}"] button[data-test="counter:decrement"]`);
if (!incBtn || !decBtn) return;
const container = document.createElement('div');
container.className = 'tm-functional-controls';
container.style.display = 'flex';
container.style.gap = '6px';
container.style.marginTop = '8px';
const makeProxyButton = (label, realBtn) => {
const btn = document.createElement('button');
btn.textContent = label;
btn.setAttribute('style', 'padding: 6px 10px; background: #eee; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; cursor: pointer;');
btn.addEventListener('click', (e) => {
e.stopPropagation();
waitForReactButtonReady(realBtn).then(() => realBtn.click());
});
return btn;
};
const minus = makeProxyButton('−', decBtn);
const plus = makeProxyButton('+', incBtn);
container.appendChild(minus);
container.appendChild(plus);
const imageArea = tile.querySelector('.sc-guJBdh') || tile;
imageArea.parentElement.appendChild(container);
});
}
function refreshControls() {
setTimeout(() => injectControls(), 1000);
}
// Watch for SPA navigation
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
if (location.href.match(/^https:\/\/ww2\.ocado\.com\/orders\/.*\/details/)) {
console.log('🔄 URL changed, refreshing injected controls...');
refreshControls();
}
}
}, SPA_CHECK_INTERVAL);
// Watch for DOM changes (for lazy-loaded tiles)
const observer = new MutationObserver(() => {
if (location.href.match(/^https:\/\/ww2\.ocado\.com\/orders\/.*\/details/)) {
injectControls();
}
});
observer.observe(document.body, { childList: true, subtree: true });
// First run
refreshControls();
})();