Makes it easier to shop for groceries on tesco.com
Actually it does work fine on a list page, just not on a product page which is where the "flip" behaviour happens. So the MutationObserver stuff in Tescomonkey to wait for page load can work, I'm just not sure why it doesn't on a product page. I'll spend a bit more time trying to understand this figure out why.
The second issue of having to wait for the page to completely load is actually with the 'Usually bought next' section which appears several seconds after the rest of the content. Maybe MutationObserver should work for that but the reason it's not is at least because it contains plain text of e.g. "... £1/kg ... £0.10/100g ... ", that needs a regex to edit it...
Alright, I couldn't fix the product pages, but I've fixed the static text in the slow 'Usually bought next' section in the list page with a regex (although this doesn't account for multi-buys) and made it possible to chose between £/kg and £/100g. Here's the diff of what I've done:
--- Tescomonkey.user.js 2020-08-05 23:28:29.006458567 +0100
+++ Tescomonkey-ld.user.js 2020-08-05 23:34:22.602818789 +0100
@@ -17,6 +17,28 @@
(function() {
'use strict';
+
+ var pref_scale = "100g"; //set this if you prefer £/kg
+ //var pref_scale = "kg"; //set this if you prefer £/100g
+
+ let convertKtoM;
+ if (pref_scale == "kg") {
+ //looks for 100g and converts to kg
+ var regexp = /.*£(\d+\.\d\d)\/100g.*/;
+ var convertable_weight = "/100g";
+ convertKtoM = function (price) {
+ return (price * 10).toFixed(2);
+ }
+
+ } else {
+ //looks for kg and converts to 100g
+ var regexp = /.*£(\d+\.\d\d)\/kg.*/;
+ var convertable_weight = "/kg";
+ convertKtoM = function (price){
+ return (price / 10).toFixed(2);
+ }
+ }
+
/*--------------------------------------------------------------------------------------------------------------------
------------------------------------------- General functions --------------------------------------------------
--------------------------------------------------------------------------------------------------------------------*/
@@ -69,9 +91,6 @@
}
return null;
};
- function convertKtoM(price){ // converts kg to g, for example
- return (price / 10).toFixed(2);
- }
function percentColour(percent){ // 100% = red, 0% = green
var color = 'rgb(' + (percent *2.56) +',' + ((100 - percent) *2.96) +',0)'
return color;
@@ -97,6 +116,23 @@
if (mutation.target.className.includes("filter-option--link")){ // sometimes happens after the page has loaded
enhanceMainContent(document);
}
+ if (mutation.target.className.includes("recommender-wrapper")){
+ enhanceRecommender(mutation.target);
+ }
+ }
+ function enhanceRecommender(mutation) {
+ var weightTextElements = mutation.querySelectorAll("h6"); // class name seems random, and these are the only h6's
+ var child;
+ for (var i = 0; i < weightTextElements.length; i++) {
+ child = weightTextElements[i].childNodes[0];
+ if (weightTextElements[i].hasChildNodes() && child.nodeType == 3) {
+ var found = child.nodeValue.match(regexp);
+ if (found) {
+ let new_price = convertKtoM(found[1]);
+ child.nodeValue = "£".concat(new_price).concat("/").concat(pref_scale).concat(" (mod2)");
+ }
+ }
+ }
}
function enhanceMainContent(mutation){ // main content being the list of products
// console.log(mutation);
@@ -117,11 +153,11 @@
var weightElements = mutation.querySelectorAll(".weight"); // get all "weight" elements - "/100g" etc
if (weightElements.length < 1){return} // if there are none, don't bother continuing
for (var i=0,j = weightElements.length;i
and the full version for convenience:
// ==UserScript==
// @name Tescomonkey
// @author Than
// @version 0.01
// @description Makes it easier to shop for groceries on tesco.com
// @match https://*.tesco.com/groceries/*
// @include https://*.tesco.com/groceries/*
// @connect tesco.com
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_setClipboard
// @run-at document-end
// @namespace https://greasyfork.org/users/288098
// ==/UserScript==
(function() {
'use strict';
var pref_scale = "100g"; //set this if you prefer £/kg
//var pref_scale = "kg"; //set this if you prefer £/100g
let convertKtoM;
if (pref_scale == "kg") {
//looks for 100g and converts to kg
var regexp = /.*£(\d+\.\d\d)\/100g.*/;
var convertable_weight = "/100g";
convertKtoM = function (price) {
return (price * 10).toFixed(2);
}
} else {
//looks for kg and converts to 100g
var regexp = /.*£(\d+\.\d\d)\/kg.*/;
var convertable_weight = "/kg";
convertKtoM = function (price){
return (price / 10).toFixed(2);
}
}
/*--------------------------------------------------------------------------------------------------------------------
------------------------------------------- General functions --------------------------------------------------
--------------------------------------------------------------------------------------------------------------------*/
//Check the DOM for changes and run a callback function on each mutation
function observeDOM(callback){
var mutationObserver = new MutationObserver(function(mutations) { //https://davidwalsh.name/mutationobserver-api
mutations.forEach(function(mutation) {
callback(mutation) // run the user-supplied callback function,
});
});
// Keep an eye on the DOM for changes
mutationObserver.observe(document.body, { //https://blog.sessionstack.com/how-javascript-works-tracking-changes-in-the-dom-using-mutationobserver-86adc7446401
attributes: true,
// characterData: true,
childList: true,
subtree: true,
// attributeOldValue: true,
// characterDataOldValue: true,
attributeFilter: ["class"] // We're really only interested in stuff that has a className
});}
/**
https://gomakethings.com/climbing-up-and-down-the-dom-tree-with-vanilla-javascript/
* Get the closest matching element up the DOM tree.
* @private
* @param {Element} elem Starting element
* @param {String} selector Selector to match against
* @return {Boolean|Element} Returns null if not match found
*/
var getClosest = function ( elem, selector ) {
// Element.matches() polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// Get closest match
for ( ; elem && elem !== document; elem = elem.parentNode ) {
if ( elem.matches( selector ) ) return elem;
}
return null;
};
function percentColour(percent){ // 100% = red, 0% = green
var color = 'rgb(' + (percent *2.56) +',' + ((100 - percent) *2.96) +',0)'
return color;
}
/*--------------------------------------------------------------------------------------------------------------------
------------------------------------------- Init functions --------------------------------------------------
--------------------------------------------------------------------------------------------------------------------*/
observeDOM(doDomStuff); // Observe the DOM for changes & peform actions accordingly
function doDomStuff(mutation){
// console.log(mutation.target); // a flow of "mutations" comes through this function as the page changes state.
if (mutation.target.className.includes("main__content")){
enhanceMainContent(mutation.target);
}
if (mutation.target.className.includes("dfp-wrapper")){ // this usually means the page has fylly loaded after a refresh
enhanceMainContent(document);
}
if (mutation.target.className.includes("product-lists-wrapper")){
enhanceMainContent(mutation.target);
}
if (mutation.target.className.includes("product-list grid")){
enhanceMainContent(mutation.target);
}
if (mutation.target.className.includes("filter-option--link")){ // sometimes happens after the page has loaded
enhanceMainContent(document);
}
if (mutation.target.className.includes("recommender-wrapper")){
enhanceRecommender(mutation.target);
}
}
function enhanceRecommender(mutation) {
var weightTextElements = mutation.querySelectorAll("h6"); // class name seems random, and these are the only h6's
var child;
for (var i = 0; i < weightTextElements.length; i++) {
child = weightTextElements[i].childNodes[0];
if (weightTextElements[i].hasChildNodes() && child.nodeType == 3) {
var found = child.nodeValue.match(regexp);
if (found) {
let new_price = convertKtoM(found[1]);
child.nodeValue = "£".concat(new_price).concat("/").concat(pref_scale).concat(" (mod2)");
}
}
}
}
function enhanceMainContent(mutation){ // main content being the list of products
// console.log(mutation);
showAllPricesPerGram(); // first of all, show all weight-based prices per gram
try{
colourBasedOnValue("100g"); // then compare all the prices with one another and use colour to show the best value
colourBasedOnValue("100ml");
colourBasedOnValue("each");
}
catch(err){
console.log(err);
}
pricePerWeightForOffers(); // finally, if there are any special offers, get another price per weight if you go for the offer.
//
//The rest of this function is defining the functions called above
//
function showAllPricesPerGram(){ // if anything is labeled price per kilo, change it to price per gram
var weightElements = mutation.querySelectorAll(".weight"); // get all "weight" elements - "/100g" etc
if (weightElements.length < 1){return} // if there are none, don't bother continuing
for (var i=0,j = weightElements.length;i span") // this is what we use to judge the position of the new element
newPricePerGramElement.style.position = "absolute"; // a bit hacky this, but it seems to work
newPricePerGramElement.style.left = `${referencePosition.offsetLeft}px`; // put the new price at this many pixels from the left
newPricePerGramElement.style.top = `${referencePosition.offsetTop + 13}px`; // and this many pixels from the top of the page
var currentPrice = productItems[i].querySelector(".price-details--wrapper"); // we'll add our new element under this
currentPrice.appendChild(newPricePerGramElement); // adding the element
}
catch(err){ // for any errors
console.log(err,productItems[i]); // tell me what's wrong
var productElement = getClosest(productItems[i],".product-list--list-item");
// productElement.style.backgroundColor = "red"; // and highlight the item on the page so I see I need to bugfix
continue; // skip to the next item
}
}
}
}
enhanceMainContent(document); // Also, run all of the above when the document loads initially, not just during mutations
// Your code here...
})();
*edit: fixed it not changing the prices on the main page from 100g to kg.
If you want to take this and create v0.02 with it I'm happy with that. Thanks for creating this in the first place btw.
This doesn't work completely, there are page elements that get modified after the script runs, and the script needs to run after they're modified. I see the price /100g during page load but then it flips back to /kg later.
Before I found this I started writing it myself, and fixed that problem with a very long, fixed timeout (which I know is a poor solution, you have to wait for the timeout then the prices change (I choose to change everything to /kg)):
var observer = new MutationObserver(resetTimer); var timer = setTimeout(action, 6000, observer); // wait for the page to stay still for 6 seconds observer.observe(document, {childList: true, subtree: true}); function resetTimer(changes, observer) { clearTimeout(timer); timer = setTimeout(action, 6000, observer); } function action(o) { o.disconnect(); var elements = document.querySelectorAll(".price-per-quantity-weight"); for (var i = 0; i < elements.length; i++) { var weight_span = elements[i].getElementsByClassName("weight")[0].childNodes[0]; if (weight_span.nodeValue == "/100g") { var price_span = elements[i].getElementsByClassName("value")[0].childNodes[0]; price_span.nodeValue = (price_span.nodeValue*10).toFixed(2); weight_span.nodeValue = "/kg (mod)"; } if (weight_span.nodeValue == "/100ml") { var price_span = elements[i].getElementsByClassName("value")[0].childNodes[0]; price_span.nodeValue = (price_span.nodeValue*10).toFixed(2); weight_span.nodeValue = "/litre (mod)"; } } }The MutationObserver stuff was copy/pasted from stackoverflow, my javascript fu isn't great. The ideal solution would be to wait until the page gets completely loaded and finishes being modified, but I think that's akin to solving the halting problem because it's loaded and modified by other slow js?