Tweakers Pricewatch search extension

Add additional search and filter functions to the Tweakers Pricewatch

// ==UserScript==
// @name         Tweakers Pricewatch search extension
// @namespace    http://remyblok.tweakers.net/
// @version      0.6
// @description  Add additional search and filter functions  to the Tweakers Pricewatch
// @author       Remy Blok
// @match        https://*.tweakers.net/*
// @run-at       document-idle
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

var debug = false;

function FilterFormExtended(filterForm) {
    this.filterForm = filterForm;
    this.originalHandleAjaxResponse = null;
}

Object.extend(FilterFormExtended.prototype, {
    init: function () {
        this.hideMoreLinks();
    },
    hideMoreLinks: function () {
        if (!filterForm.listing) return;
        // find all the links more/less options link
        var links = document.querySelectorAll('.showLink, .hideLink');
        var i = 0, link;
        while ((link = links[i++])) {
            try
            {
                //this is the id of the div element of the current filter
                var filterName = link.parentNode.parentNode.id;

                // if filtername is empty, generate one to keep unique elements
                if(!filterName) {
                    filterName = "fn" + (Math.round(Math.random()*10000000));
                }

                // creating HTML, could be rendered at the server
                this.generateHtml(link, filterName);

                //select the ul element of the extended filter
                var ul = link.parentNode.querySelectorAll(':scope #' + filterName + '_extendedFilter')[0];

                // get the actual filter element
                var filter = ul.querySelectorAll(':scope #' + filterName + '_filter')[0];
                filter.onkeyup = this.filterList;

                // get the actual select all element
                var selectAllCheckbox = ul.querySelectorAll(':scope #' + filterName + '_selectall')[0];
                selectAllCheckbox.onclick = this.selectAll;

                // create a options bag to store key references into
                var options = {};
                options.filterName = filterName;
                options.filterElement = filter;
                options.filterContainer = ul;
                options.selectAllCheckbox = selectAllCheckbox;
                // figure out what option values there are in the filer
                // we need to exclude the li elements from the extended filter
                options.optionsElements = link.parentNode.querySelectorAll(':scope ul:not(#' + ul.id + ') li');

                filter.options = options;
                selectAllCheckbox.options = options;
                link.options = options;

                // we want additional things to happen when the user clicks on the link
                this.filterForm.addSimpleEvent(link, 'onclick', this.toggleHideMore);

                // trigger the initial visibility
                this.toggleHideMore.call(link);

                //just a check to see the scipt is working
                if (debug) {
                    link.innerHTML = link.innerHTML + " extended";
                }
            }
            catch
            {
                //just a check to see the scipt is working
                if (debug) {
                    link.innerHTML = link.innerHTML + " failure";
                }
            }
        }

        // replace the original handleAjaxResponse method so we can inject our onw code first.
        this.originalHandleAjaxResponse = this.filterForm.handleAjaxResponse;
        this.filterForm.handleAjaxResponse = this.handleAjaxResponse;
    },
    generateHtml: function (link, filterName) {
        var html =
            ('<li>' +
                '<input type="text" id="{filterName}_filter" name="extendedFilter" class="text" placehoder="Filter">' +
            '</li>' +
            '<li>' +
                '<label for="{filterName}_selectall" class="checkbox">' +
                    '<span class="inputWrapper">' +
                        '<input type="checkbox" name="extendedSelectall" id="{filterName}_selectall" value="{filterName}_selectall">' +
                    '</span> ' +
                    '<span title="Alles Selecteren" class="facetLabel"><b>Alles Selecteren</b></span>' +
                '</label>' +
            '</li>').replace(/{filterName}/g, filterName);

        // add the a new ul to the page
        // separate ul used so that is can be easily hidden
        var ul = document.createElement('ul');
        ul.id = filterName + "_extendedFilter";
        ul.innerHTML = html;
        link.parentNode.insertBefore(ul, link.previousSibling.previousSibling); // before the link there ar two ul elements, we place it above these two elements
    },
    handleAjaxResponse: function (response) {
        // validate the data is OK
        var data = checkJsonResponse(response);

        // because the querystring returned from the server does not include the extended field these need to be added
        // otherwise the original handleAjaxResponse will clear the form, and not reset the values
        if (data && 'querystring' in data) {

            // first we do all the select all comboboxes
            var selectAlls = document.querySelectorAll("[name='extendedSelectall']");
            var i = 0, selectAll;

            data.querystring.extendedSelectall = [];
            while ((selectAll = selectAlls[i++])) {
                if (selectAll.checked) {
                    data.querystring.extendedSelectall[data.querystring.extendedSelectall.length] = selectAll.value;
                }
            }

            // now the do the user typed filer
            var filters = document.querySelectorAll("[name='extendedFilter']");
            var j = 0, filter;

            // we check if there is a value filled in and then add it to the query string.
            while ((filter = filters[j++])) {
                if (filter.value && filter.value !== '' ) {
                    data.querystring[filter.id] = filter.value;
                }
            }
        }

        // call the original method
        filterFormExtended.originalHandleAjaxResponse.call(this, response);
    },
    // 'this' is the 'a'-element of the show/hide more link
    toggleHideMore: function () {
        // because the original code has already run the check is the other way around
        //if (this.className != /*==*/ 'showLink') {
        if (this.className == 'hideLink') {
            this.options.filterContainer.classList.remove('hideMore');
        } else {
            this.options.filterContainer.classList.add('hideMore');
            // reset the filter and make sure all options are visible again
            this.options.selectAllCheckbox.parentNode.parentNode.classList.remove('selected');
            this.options.selectAllCheckbox.checked = false;
            this.options.filterElement.value = '';
            filterFormExtended.filterList.call(this.options.filterElement);
        }
    },
    // onKeyUp event of the filter box
    // 'this' is the 'input'-element text filter
    filterList: function () {
        // we don't want to be case sensitive
        var searchString = this.value.toLowerCase();
        var i = 0, li, needsScreenUpdate = false, shownItems = 0, checkedItems = 0;
        // loop over all options within this filter to see if they match
        // if they don't match we hide them, oterwise we show them
        // it also keeps track of the state of the checkbox before it was hidden
        // so then when it is shown again we can reset the correct state
        while ((li = this.options.optionsElements[i++])) {
            var label = singleOrNull(li.querySelectorAll(':scope .facetLabel'));
            var checkbox = singleOrNull(li.querySelectorAll(':scope input'));
            if (label !== null && checkbox !== null) {
                // do the string comparision, also here lower case
                if (label.innerText.toLowerCase().includes(searchString)) {
                    // if it is currently hidden, we need to make it visible
                    if (li.className == 'hideMore') {
                        li.classList.remove('hideMore');
                        // restore the state of check box from before the filter
                        if (checkbox.checkedBeforeFilter) {
                            checkbox.checked = checkbox.checkedBeforeFilter;
                        }
                        checkbox.checkedBeforeFilter = checkbox.checked;
                        // if a checked option is now visible we need to refresh the screen
                        needsScreenUpdate |= checkbox.checked;
                    }
                    shownItems++;

                    if (checkbox.checked) {
                        checkedItems++;
                    }
                }
                // only hide it if it is not already hidden
                // otherwise we override the checkedBeforeFilter state
                else if (li.className != 'hideMore') {
                    li.classList.add('hideMore');
                    // if a checked option is now hidden we need to refresh the screen
                    needsScreenUpdate |= checkbox.checked;
                    checkbox.checkedBeforeFilter = checkbox.checked;
                    // make sure the option in no longer checked
                    checkbox.checked = false;
                }
            }
        }
        // disable the select all button if there are no options
        this.options.selectAllCheckbox.disabled = shownItems === 0;

        // show the selected all button as selected if all filter options are selected
        if (shownItems > 0 && checkedItems === shownItems) {
            this.options.selectAllCheckbox.parentNode.parentNode.classList.add('selected');
            this.options.selectAllCheckbox.checked = true;
        } else {
            this.options.selectAllCheckbox.parentNode.parentNode.classList.remove('selected');
            this.options.selectAllCheckbox.checked = false;
        }

        // finally update the page with the selected options if needed
        if (needsScreenUpdate) {
            filterForm.ajaxTimer();
        }
    },
    // onClick event of the selected all combobox
    // 'this' is sthe 'input'-element combobox
    selectAll: function (e) {
        var i = 0, li, needsScreenUpdate = false;
        // loop over all options, check if there are not hidden due to the text filter
        // then check the options
        while ((li = this.options.optionsElements[i++])) {
            if (!this.checked || li.className != 'hideMore') {
                var checkbox = singleOrNull(li.querySelectorAll(':scope input'));
                if (checkbox !== null) {
                    // only update the screen if the checkbox is switched from state
                    needsScreenUpdate |= (checkbox.checked != this.checked);
                    checkbox.checked = this.checked;
                }
            }
        }
        // finally update the page with the selected options if needed
        if (needsScreenUpdate) {
            filterForm.ajaxTimer();
        }
    },
});

// little helper function to get the first element from an array
function singleOrNull(array) {
    if( array instanceof NodeList && array.length === 1) {
        return array[0];
    }
    if (Array.isArray(array) && array.length == 1) {
        return array[0];
    }
    return null;
}

// init the code
if (unsafeWindow.filterForm) {
    var filterFormExtended = new FilterFormExtended(unsafeWindow.filterForm);
    filterFormExtended.init();
}
})();