DeGiro improved filters

Adds missing sort options and amount of shares to buy calculator

// ==UserScript==
// @name         DeGiro improved filters
// @namespace    https://yelidmod.com/degiro
// @version      0.7
// @description  Adds missing sort options and amount of shares to buy calculator
// @author       DonNadie
// @match        https://trader.degiro.nl/*
// @grant        none
// ==/UserScript==

// jshint esversion: 6

(function() {
    'use strict';

    const section = {
        equity: 1,
        etf: 131
    };

    let filtersIndex = {
        volume: 0,
        isin: null,
    };
    const sortButton = '<i role="img" data-name="icon" data-type="sort" aria-hidden="true" class="ife-sort-icon"><svg viewBox="0 0 24 24"><path d="M16.8 13.2L12 18l-4.8-4.8h9.6zM12 6l4.8 4.8H7.2L12 6z"></path></svg></i>';

    const addStyle = (styleString) => {
        const style = document.createElement('style');
        style.textContent = styleString;
        document.head.append(style);
    };

    const sort = () => {
        const trList = document.querySelectorAll('[data-name="productTypeSearch"] tr');

        new Promise((resolve, reject) => {
            let list = [];
            let val;
            trList.forEach((tr, e) => {
                tr.querySelectorAll('td').forEach((td, i) => {
                    if (i !== filtersIndex.volume) {
                        return;
                    }
                    val = parseInt(td.innerText.replace(".", ""));
                    list.push({
                        tr: tr,
                        value : isNaN(val) ? 0 : val
                    });
                });
                if (e == (trList.length - 1)) {
                    resolve(list);
                }
            });
        }).then(list => {
            list.sort((a, b) => (a.value < b.value) ? 1 : -1);
            list.forEach(entry => {
                document.querySelector('[data-name="productTypeSearch"] tbody').appendChild(entry.tr);
            });
        });

    };

    const onLoaded = () => {
        const url = new URL(location.href.replace("#", ''));

        if (!url.searchParams.has('productType')) {
            return;
        }

        const currentSection = parseInt(url.searchParams.get('productType'));

        document.querySelectorAll('[data-name="productTypeSearch"] th').forEach((el, i) => {
            if (el.innerText == "Volumen") {
                filtersIndex.volume = i;
                el.classList.add('ife-container');
                el.innerHTML += sortButton;
                el.addEventListener("click", sort);
            } else if (el.innerText.includes("ISIN")) {
                filtersIndex.isin = i;
            }
        });

        if (filtersIndex.isin == null || ![section.etf, section.equity].includes(currentSection)) {
            return;
        }
        const morningType = currentSection == section.etf ? 'ETF' : 'STOCK';

        document.querySelectorAll('[data-name="productTypeSearch"] tr').forEach((tr, e) => {
             tr.querySelectorAll('td').forEach((td, i) => {
                 if (i !== filtersIndex.isin) {
                     return;
                 }
                 let isin;

                 if (td.innerText.includes("/")) {
                   isin = td.innerText.split(" / ")[1];
                 } else if (td.innerText.length > 5) {
                   isin = td.innerText;
                 }

                 td.querySelector("span").innerHTML = td.innerText.replace(isin, '<a href="https://www.morningstar.es/es/funds/SecuritySearchResults.aspx?type=' + morningType + '&search=' + isin + '" class="ife-link" target="_blank">' + isin + '</a>');
             });
         });
    };

    const showCalculator = (mutationsList) => {
        let section = document.querySelector('[data-name="orderForm"] section');

        if (section == null ||
            document.getElementById('simple-calculator') != null ||
            typeof window.calculatorInjected !== "undefined") {
            return;
        }
        window.calculatorInjected = true;

        const calculatorContainer = document.createElement("div");
        calculatorContainer.id = "simple-calculator";
        calculatorContainer.classList.add("ife-calculator-container");
        calculatorContainer.innerHTML += '<div class="ife-input-container"><div class="ife-input-label"><input id="calculator-money" type="number" min="0" step="1" placeholder="0" class="ife-input"><span>€</span></div></div>'
        + "." +
        '<div class="ife-input-container"><div class="ife-input-label"><input id="calculator-shares" type="text" class="ife-input" placeholder="0" disabled><span>shares</span></div></div>';

        const moneyInput = calculatorContainer.querySelector("#calculator-money");
        const sharesInput = calculatorContainer.querySelector("#calculator-shares");

        moneyInput.addEventListener("input", () => {
            const sharePrice = parseFloat(document.querySelector('[data-field="CurrentPrice"]').title.replace(".", "").replace(",", "."));
            sharesInput.value = Math.floor(moneyInput.value / sharePrice);
        });

        setTimeout(() => {
            // it seems like section is recreated in just a few secs :S
            section = document.querySelector('[data-name="orderForm"] section');
            section.parentNode.insertBefore(calculatorContainer, section);
            delete(window.calculatorInjected);
        }, 1000);
    };

    addStyle(`
     .ife-container {
        align-items: center;
        display: flex;
        flex-direction: row;
        padding-right: 12px;
        position: relative;
     }
     .ife-sort-icon {
        contain: strict;
        display: inline-block;
        flex-shrink: 0;
        font-style: normal;
        font-weight: 400;
        line-height: 1;
        opacity: 1;
        overflow: hidden;
        width: 20px;
        height: 20px;
        text-align: center;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
        vertical-align: middle;
        position: absolute;
        right: -8px;
        top: 50%;
        transform: translateY(-50%);
        cursor: pointer;
   }
   .ife-link {
        color: #009fdf;
   }
   .ife-calculator-container {
        align-items: flex-start;
        display: flex;
        flex-direction: row;
    }
   .ife-input-container {
        display: flex;
        flex: 1 1 50%;
        flex-direction: column;
        max-width: 50%;
        margin-top: 10px;
        margin-bottom: 10px;
    }
   .ife-input-label {
       background-color: #f3f4f5;
       border: 1px solid transparent;
       display: inline-block;
       height: 32px;
       min-height: 32px;
       position: relative;
   }
   .ife-input {
       background-color: inherit;
       border: 0;
       border-radius: inherit;
       display: block;
       font-size: 1rem;
       height: 100%;
       line-height: 1.5;
       min-height: 100%;
       padding: 0 8px;
       width: 100%;
   }
   .ife-input[disabled], .ife-input[disabled]::placeholder {
       color: #00a658;
   }
   .ife-input-label span {
       color: #00a658;
       position: absolute;
       top: 30%;
       right: 10px;
   }
   `);

    let observerTimeout = null;
    const observerCallback = (mutationsList, observer) => {
        showCalculator(mutationsList);

        // since we can't track an specific id, we observe any body changes and
        // check if our classes are present.
        if (document.querySelectorAll('.ife-container').length > 0) {
            return;
        }
        if (observerTimeout != null) {
            clearTimeout(observerTimeout);
        }
        observerTimeout = setTimeout(onLoaded, 1000 * 2);
    };

    (new MutationObserver(observerCallback)).observe(document.body, {childList: true, subtree: true });
})();