Tweakers Pricewatch search extension

Add additional search and filter functions to the Tweakers Pricewatch

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
}
})();