Tweakers Pricewatch search extension

Add additional search and filter functions to the Tweakers Pricewatch

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
}
})();