anador / Sbermarket price per unit calculator

// ==UserScript==
// @name         Sbermarket price per unit calculator
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  Добавление цены за единицу объема
// @author       anador
// @license MIT
// @include https://sbermarket.ru/*
// @grant        none
// ==/UserScript==

(function () {

    function getPageType() {

        // любая страница со списком продуктов
        if (document.querySelectorAll('.product') && document.querySelectorAll('.product').length > 0) {
            return "productList";
        }

        // страница избранного
        else if (location.pathname === '/user/favorites') {
            //console.log('favorites found'); 
            return 'favorites';
        }

        else {
            //console.log('nothing found');
            return false;
        }
    }

    //вычисление цены за ед. измерения
    function countPricePerUnit(price, unit, volumeValue) {
        if (unit === 'г') {
            return {
                outputPrice: price / (volumeValue * 0.001),
                outputUnit: "кг",
            }
        }
        else if (unit === 'кг') {
            return {
                outputPrice: price / (volumeValue),
                outputUnit: "кг",
            }
        }
        else if (unit === 'л') {
            return {
                outputPrice: price / (volumeValue),
                outputUnit: "л",
            }
        }
        else if (unit === 'мл') {
            return {
                outputPrice: price / (volumeValue * 0.001),
                outputUnit: "л",
            }
        }
        else {
            return false;
        }
    };

    //вставка элемента с ценой за ед. измерения
    function insertCountedPrice(elem, price, unit) {
        elem.insertAdjacentHTML('afterend', `<p class="pricePerUnit" style="
        bottom: 0;
        color: #9fabb7;
        font-size: 13px;
        line-height: 1;
        position: absolute;
        right: 0px;
        ">
        ${price} ₽ / ${unit}
        </p>`)
    }

    //вставка элемента с ценой за ед. измерения для favorites
    function insertCountedPriceForFavorites(elem, price, unit) {
        elem.insertAdjacentHTML('afterend', `<p class="pricePerUnit" style="
        position: absolute;
        bottom: 10px;
        font-size: 13px;
        color: #8f8e94;
        right: 12px;
        // font-weight: 700;
        ">
        ${price} ₽ / ${unit}
        </p>`)
    }

    //вставка элемента с ценой за ед. измерения для popup
    function insertCountedPriceForPopup(elem, price, unit) {
        elem.insertAdjacentHTML('afterend', `<p class="pricePerUnit" style="
        position: absolute;
        margin-top: -38px;
        right: 0px;
        color: #8f8e94;
        font-size: 15px;
        font-weight: 400;
        letter-spacing: .2px;
        ">
        ${price} ₽ / ${unit}
        </p>`)
    }

    //вставка элемента с ценой за ед. измерения для выпадающих результатов поиска
    function insertCountedPriceForLiveSearchResults(elem, price, unit) {
        elem.insertAdjacentHTML('afterend', `<span class="pricePerUnit" style="
        color: #888;
        font-size: 13px;
        line-height: 1;
        margin-left: 1em;
        ">
        ${price} ₽ / ${unit}
        </p>`)
    }

    function productsHandler() {
        productsList = document.querySelectorAll('.product');
        productsList.forEach(el => {
            if (el.querySelector('div[class="price price--default"]') && el.querySelector('div[class="price price--default"]').innerText) {
                priceString = el.querySelector('div[class="price price--default"]').innerText.replace(/ /g, '').replace(/,/g, '.');
                price = parseFloat(priceString);
                volumeString = el.querySelector('.product__volume').innerText;
                volumeValue = volumeString.split(' ')[0].replace(/,/, '.');
                volumeUnit = volumeString.split(' ')[1];
                if (countPricePerUnit(price, volumeUnit, volumeValue) !== false) {
                    pricePerUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputPrice.toFixed(2);
                    finalUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputUnit;

                    //вставка элемента
                    pricePerUnitWithComma = pricePerUnit.replace(/\./, ','); // замена точки между разрядами на запятую обратно
                    if (!el.querySelector('.pricePerUnit')) {
                        insertCountedPrice(el.querySelector('.product__volume'), pricePerUnitWithComma, finalUnit);
                    }
                }
            }
        });

        //обсервер для отслеживания подрузки товаров при скролле
        observer = new MutationObserver((mutationsList) => {
            //if (mutationsList[0].removedNodes.length > 0) { //пока убрал, сейчас работает и без этого
            observer.disconnect();
            //console.log(mutationsList)
            console.log('Products changed');
            productsHandler();
            //}
        });
        if (document.querySelector('.load_container')) {
            observer.observe(document.querySelector('.load_container'), {
                childList: true,
                //subtree: true,
            })
        }
    }

    function favoritesHandler() {
        productsList = document.querySelectorAll('a[class^="favorites_"]');
        productsList.forEach(el => {
            if (el.querySelectorAll('div>div>div>span')[0] && el.querySelectorAll('div>div>div>span')[0].innerText) {
                priceString = el.querySelectorAll('div>div>div>span')[0].innerText.replace(/ /g, '').replace(/,/g, '.').replace(/ /g,'');
                console.log(priceString)
                price = parseFloat(priceString);
                volumeString = el.lastChild.lastChild.innerText;
                volumeValue = volumeString.split(' ')[0].replace(/,/, '.');
                volumeUnit = volumeString.split(' ')[1];
                if (countPricePerUnit(price, volumeUnit, volumeValue) !== false) {
                    pricePerUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputPrice.toFixed(2);
                    finalUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputUnit;

                    //вставка элемента
                    pricePerUnitWithComma = pricePerUnit.replace(/\./, ','); // замена точки между разрядами на запятую обратно
                    if (!el.querySelector('.pricePerUnit')) {
                        insertCountedPriceForFavorites(el.lastChild.lastChild, pricePerUnitWithComma, finalUnit);
                    }
                }
            }
        });

        //обсервер для отслеживания подгрузки товаров/перезагрузки списка
        observerFavoritesList = new MutationObserver((mutationsList) => {

            //дополнительная проверка, что в мутации добавляется элемент для случая удаления из избранного, когда элементы только удаляются
            if (mutationsList[0].addedNodes.length > 0) {
                console.log('Favorites changed');
                favoritesHandler();
            }

        });
        if (document.querySelectorAll('.favorite-product') && document.querySelectorAll('.favorite-product').length > 0) {
            observerFavoritesList.observe(document.querySelector('.ui-content-wrapper').firstChild.lastChild, {
                childList: true,
                //subtree: true,
            })

            observerFavoritesList.observe(document.querySelector('.favorites-list'), {
                childList: true,
                //subtree: true,
            })
        }
    }

    function popupHandler() {
        el = document.querySelector('div[class^=frames_module]');
        if (el.querySelector('meta[itemprop="price"]')) { //проверка для случая открытия напрямую товара не в наличии
            // priceString = el.querySelector('meta[itemprop="price"]').content.replace(/ /g, '').replace(/,/g, '.'); // строка убрана, стоимость получается сразу в верном формате
            priceString = el.querySelector('meta[itemprop="price"]').content;
            price = parseFloat(priceString);
            volumeString = el.querySelector('div[class^="product_cards"] div[itemprop="offers"]>p').innerText;
            volumeValue = volumeString.split(' ')[0].replace(/,/, '.');
            volumeUnit = volumeString.split(' ')[1];
            if (countPricePerUnit(price, volumeUnit, volumeValue) !== false) {
                pricePerUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputPrice.toFixed(2);
                finalUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputUnit;

                //вставка элемента
                pricePerUnitWithComma = pricePerUnit.replace(/\./, ','); // замена точки между разрядами на запятую обратно
                if (!el.querySelector('.pricePerUnit')) {
                    insertCountedPriceForPopup(el.querySelector('div[class^="product_cards"] div[itemprop="offers"]>p'), pricePerUnitWithComma, finalUnit);
                }

            }
        }

        //снова вешаем обсервер, потому что прошлый закрыли дисконнектом
        if (typeof observerPopup !== 'undefined') { //проверяем, объявлен ли обсервер для случая загрузки страницы товара, когда запущен продактс обсервер, а попап открыт сразу
            observerPopup.observe(document.querySelector('div[class^=frames_module]'), {
                childList: true,
                subtree: true,
            })
        }
    }

    function liveSearchResultsHandler() {
        productsList = document.querySelectorAll('.header-search-list-product');
        productsList.forEach(el => {
            if (el.querySelector('div[class="header-search-list-product__price"]') && el.querySelector('div[class="header-search-list-product__price"]').innerText) {
                priceString = el.querySelector('div[class="header-search-list-product__price"]').innerText.replace(/ /g, '').replace(/,/g, '.'); //изменен символ пробела на &#160;
                price = parseFloat(priceString);
                volumeString = el.querySelector('span[class="header-search-list-product__price-unit"]').innerText.trim(); //добавлен trim
                volumeValue = volumeString.split(' ')[0].replace(/,/, '.');
                volumeUnit = volumeString.split(' ')[1].replace(/\./g, ''); //добавлена замена для точки
                if (countPricePerUnit(price, volumeUnit, volumeValue) !== false) {
                    pricePerUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputPrice.toFixed(2);
                    finalUnit = countPricePerUnit(price, volumeUnit, volumeValue).outputUnit;

                    //вставка элемента
                    pricePerUnitWithComma = pricePerUnit.replace(/\./, ','); // замена точки между разрядами на запятую обратно
                    if (!el.querySelector('.pricePerUnit')) {
                        insertCountedPriceForLiveSearchResults(el.querySelector('span[class="header-search-list-product__price-unit"]'), pricePerUnitWithComma, finalUnit);
                    }
                    else {
                        el.querySelector('.pricePerUnit').innerText = `${pricePerUnitWithComma} ₽ / ${finalUnit}`; //если уже цена есть, перезаписываем ее
                    }
                }
                else { //обработка случая, когда прошлый товар для этой позиции был в подходящих единицах, а новый нет, тогда убираем рассчитанную старую цену
                    if (el.querySelector('.pricePerUnit')) {
                        el.querySelector('.pricePerUnit').remove();
                    }
                }
            }
        });

        //обсервер для отслеживания изменения выпадающих резульатов поиска
        observerLiveSearchResultUpdates = new MutationObserver(() => {
            console.log('live search results changed');
            observerLiveSearchResultUpdates.disconnect();
            liveSearchResultsHandler();
        });
        if (document.querySelector('div[data-qa="result-list"]')) { //такая проверка, чтобы скрипт не падал при неудаче запуска обсервера
            observerLiveSearchResultUpdates.observe(document.querySelector('div[data-qa="result-list"]'), {
                childList: true,
                subtree: true,
                characterData: true, //обязательно, для отслеживания изменния текста в элементах
            })
        }
    }

    function init() {

        // проверка типа содержимого страницы
        if (getPageType() == 'productList') {
            productsHandler();

            //проверка, есть открытый при загрузке ли попап с товаром
            if (document.querySelector('div[itemscope]')) {
                popupHandler();
            }
        }
        else if (getPageType() == 'favorites') {

            // обсервер для body, чтобы поймать момент загрузки элемента списка товаров
            observerFavoritesListInit = new MutationObserver(() => {
                if (document.querySelectorAll('a[class^="favorites_"]') && document.querySelectorAll('a[class^="favorites_"]').length > 0) {
                    observerFavoritesListInit.disconnect();
                    console.log('favorites-list found');
                    favoritesHandler();
                }
            });
            observerFavoritesListInit.observe(document.body, {
                childList: true,
                subtree: true,
            })
        }

        // обсервер для появления попапа
        observerPopup = new MutationObserver(() => {
            if (document.querySelector('meta[itemprop="price"]')) {
                observerPopup.disconnect();
                console.log('popup changed');
                popupHandler()
            }
        });
        if (document.querySelector('div[class^=frames_module]')) {
            observerPopup.observe(document.querySelector('div[class^=frames_module]'), {
                childList: true,
                subtree: true,
            })
        }

        // обсервер для подсказок из поиска
        observerLiveSearchResults = new MutationObserver(() => {
            //observerFavoritesListInit.disconnect();
            console.log('search results changed');
            liveSearchResultsHandler();
        });
        if (document.querySelector('.header-search-wrapper')) {
            observerLiveSearchResults.observe(document.querySelector('.header-search-wrapper'), {
                childList: true,
                //subtree: true,
            })
        }
    };

    init();

})();