Widget: DCTList: Difference between revisions

From LINKS Community Center
Jump to: navigation, search
Eschmidt (talk | contribs)
No edit summary
Marterer (talk | contribs)
No edit summary
 
(324 intermediate revisions by 3 users not shown)
Line 1: Line 1:
<noinclude>Development verstion of the DCT List.<br><span style="color: red; font-weight: bold;">Not ready for production!</span></noinclude>
<noinclude>DCT list widget.<br><span style="color: red; font-weight: bold;">Currently in use &ndash; do not modify!</span></noinclude>
 
<includeonly>
<includeonly>
     <link href="/resources/assets/tabulator.min.css" rel="stylesheet">
     <link href="/resources/assets/tabulator/dist/css/tabulator.min.css" rel="stylesheet">
     <script type="text/javascript" src="/resources/assets/tabulator.min.js"></script>
     <script type="text/javascript" src="/resources/assets/tabulator/dist/js/tabulator.min.js"></script>


     <style>
     <!-- STYLES BEGIN -->
        #dct-list-wrapper { font-family: 'Open Sans'; margin-top: 4em; }
<link rel="stylesheet" href="https://assets.links.communitycenter.eu/v2/links/lib?q=dct_list.css">
        #dct-list-wrapper h1, #dct-list-wrapper h2, #dct-list-wrapper h3 {
<!-- STYLES END -->
            font-family: 'Raleway';
            font-weight: 300;
            letter-spacing: .06em;
        }
        #dct-list-wrapper h1, #dct-filters h2 {
            color: var(--links-blue);
            display: flex;
            align-items: center;
        }
        #dct-intro {
            font-size: larger;
            margin-bottom: 4em;
        }
        #dct-list-wrapper h1 svg {
            height: 2.5em;
            width: 2.5em;
            fill: var(--links-blue);
            margin-right: .5em;
        }
        #dct-filters h2 svg {
            height: 1.5em;
            width: 1.5em;
            fill: var(--links-blue);
            margin-right: .5em;
            margin-left: -.2em;
        }
        #clear-filters-button {
            border: 1px solid var(--links-blue);
            font-size: 1.5em;
            font-family: 'Open Sans';
            font-weight: 100;
            margin-bottom: 2em;
            padding: 0.3em 0.8em;
            color: var(--links-blue);
            background: transparent;
            font-variant: small-caps;
        }
        #dct-list-wrapper h2 {
            margin-bottom: 1em;
        }
        .filter-button-wrapper {
            margin: 1.5em 0 1em 0;
        }
        .filter-wrapper button {
            border: 0 none;
            color: var(--links-blue);
            background-color: transparent;
            font-variant: small-caps;
            font-size: 1.2em;
            text-decoration: underline;
            padding: 0;
        }
        .filter-content {
            font-size: 1.2em;
            margin-bottom: 4em;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(14em, 1fr));
            gap: 1em 1em;
            align-items: end;
        }
        .filter-group-header {
            grid-column: 1/-1;
            margin-bottom: -.5em;
            margin-top: 1em;
            font-family: 'Raleway';
            font-weight: 200;
            letter-spacing: .06em;
        }
        .filter-group-start { grid-column-start: 1; }
        .filter-content img { width: 2em; height: 2em; margin: 0 .5em; }
        #dct-tabulator.tabulator { border: 0 none; background-color: transparent; font-size: 1.2em; }
        #dct-tabulator.tabulator .tabulator-header { border-bottom: 4px double var(--links-blue); background-color: transparent; }
        #dct-tabulator.tabulator .tabulator-header .tabulator-col { border-right: 0 none; background: transparent; }
        #dct-tabulator.tabulator .tabulator-header .tabulator-col-content { padding: .5em 0; }
        #dct-tabulator.tabulator .tabulator-header .tabulator-col-sorter { right: 1em; }
        #dct-tabulator.tabulator .tabulator-row { background-color: transparent; }
        #dct-tabulator.tabulator .tabulator-cell { border-right: 0 none; border-bottom: 1px solid var(--links-blue); padding: .5em 0; }
        .data-source-img, .func-img { width: 2em; height: 2em; margin-right: .4em; margin-bottom: .4em; }
        #dct-tabulator img.unselected {
            filter: grayscale(1);
            opacity: .25;
        }
        .sources-collapse {
            display: inline-block;
        }
        .sources-collapse-toggle { display:inline-block; width: 2em; height: 2em; }
        #dct-tabulator .tabulator-row .tabulator-responsive-collapse {
            padding: .5em 0 1.5em 0;
            border: 0 none;
        }
        #dct-tabulator .tabulator-row .tabulator-responsive-collapse table tr td strong {
            font-family: 'Raleway';
            font-weight: 300;
            display: block;
        }
        #dct-tabulator .tabulator-row .tabulator-responsive-collapse table tr:nth-child(n+2) td strong {
            margin-top: 1em;
        }
        #dct-tabulator .tabulator-row .tabulator-responsive-collapse table tr td {
            display: block;
        }
    </style>


     <script>
     <!-- SCRIPT BEGIN -->
     'use strict';
    <script type="text/javascript" src="https://assets.links.communitycenter.eu/v2/links/lib?q=dct_list.js"></script>
     <!-- SCRIPT END -->


     /**
     <!-- Icon definitons -->
    * @typedef {Object} DCT
    <!--<img src="https://assets.links.communitycenter.eu/v2/links/lib?q=unknown.svg">-->
    * @property {string} name
    * @property {string} url
    * @property {string[]} dataSources
    * @property {string[]} functions
    * @property {string} logo
    */


     let table;
     <div id="dct-list-wrapper">
        <h1>
            <img src="https://assets.links.communitycenter.eu/v2/links/lib?q=1-dct_list.svg">
            <div>
                <div>Technologies</div>
                <div style="font-size:small; letter-spacing:.03em; margin-left: .6em;">Social Media and Crowdsourcing Library</div>
            </div>
        </h1>
        <div id="dct-intro">
            <p>The overall goal of the Social Media and Crowdsourcing (SMCS) Technologies Library is to face the growing
                heterogeneous use of technologies in disasters and the overwhelming number of technologies on the
                market. It gathers and structures information about existing technologies to provide an up-to-date overview and
                thus support the selection of suitable technologies.
            </p>
            <p>
                You can use the filters to identify relevant technologies according to your needs and then click on the
                name of the technology to get further information.
            </p>
        </div>


    // This defines how sources are grouped in the filter.
        <div id="filter-bar">
    // Any source not listed here will be added to the last group entitled "More platforms".
            <div style="display: flex; justify-content: space-between;">
    const sourcesLayout = [
                <div style="flex: 1 1;">
        {
                    <h2 style="margin-bottom: 1rem;">Selected Filters</h2>
            title: 'General',
                    <div id="filter-summary">No filter. Showing all results.</div>
            sources: ['Crowd', 'Web']
                </div>
        },
                <div>
        {
                    <button class="large-button open-filters" type="button" onclick="toggleFilter()">Open Filters</button>
            title: 'Platforms',
                </div>
            sources: ['Facebook', 'Twitter', 'Instagram', 'YouTube']
            </div>
        }
    ];


    // Helpers
            <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 2.5rem;">
    const escapeAttr = text => text ? text.replace(/\s/g, '-') : text;
                <h2 style="margin-bottom: 0;">Results: <span id="result-count"></span></h2>
    const getFilePath = title => title ? '/index.php/Special:FilePath/' + title : title;
                <div><a href="/index.php/Form:Disaster_Community_Technology" style="color: var(--links-blue); font-size: 1.5em; font-variant:small-caps">Add new technology</a></div>
    const getQueryUrl = query => '/api.php?action=ask&format=json&query=' + encodeURIComponent(query);
            </div>
    const removePrefix = str => str.substring(str.indexOf(':') + 1);


    // Fetches platform / data source information.
             <div id="dct-filters">
    async function getSources() {
        const IMG_KEY = 'IMAGE';
        const sourceResponse = await fetch(
             getQueryUrl('[[Category:Social media platform]]|?' + IMG_KEY)
        ).then(response => response.json());
        const results = sourceResponse.query.results;


        return Object.keys(results).map(platform => {
                <h2 style="display: flex; justify-content: space-between;">
            const img = results[platform].printouts[IMG_KEY][0];
                    <div>
            console.log(img)
                        <img src="https://www.safetyinnovation.center/lcc/dct_list/img/filters.svg">
            return {
                        <span>Filters</span>
                name: platform,
                    </div>
                image: img ? getFilePath(img.fulltext) : undefined
                    <a onclick="toggleFilter()" id="close-filter-button">&times;</a>
            };
                </h2>
        });
    }


    // Fetches DCTs and their functions.
                <div style="text-align: center;">
    async function getDcts() {
                    <button class="large-button" type="button" onclick="clearFilters()">Clear Filters</button>
        const FUNC_KEY = 'functions';
                </div>
        const DESC_KEY = 'description';
       
        /** @type Map<string, { [DESC_KEY]: string, [FUNC_KEY]: string[] } */
        const functionsData = new Map();


        const functionsQuery = '[[Category:Function_category]]' +
                <div class="filter-wrapper">
                              '|?-Subproperty_of=' + FUNC_KEY +
                    <h4 class="filter-toggle">Functions <div class="plus icon"></div>
                              '|?Has_description=' + DESC_KEY;
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#functions-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#functions-filter')">Clear all</button>
                        </div>
                        <div class="filter-content loose" id="functions-filter"></div>
                    </div>
                </div>


        const functionsQueryResponse = await fetch(getQueryUrl(functionsQuery)).then(response => response.json());
                <div class="filter-wrapper">
        const functionsQueryResults = functionsQueryResponse.query.results;
                    <h4 class="filter-toggle">Supported Platforms <div class="plus icon"></div>
       
                    </h4>
        Object.keys(functionsQueryResults).forEach(key => {
                    <div class="filter-container">
            const functionCategory = functionsQueryResults[key];
                        <div class="filter-button-wrapper">
            functionsData.set(
                            <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
                removePrefix(functionCategory.fulltext),
                            <button type="button" onclick="deselectAll('#data-source-filter')">Clear all</button>
                {
                        </div>
                    [DESC_KEY]: functionCategory.printouts[DESC_KEY][0],
                        <div class="filter-content loose" id="data-source-filter"></div>
                     [FUNC_KEY]: functionCategory.printouts[FUNC_KEY].map(subfunction => removePrefix(subfunction.fulltext))
                     </div>
                 }
                 </div>
            );
        });


        // TODO: Remove properties that are not relevant for the filter, e.g. 'Supported content types'.
                <div class="filter-wrapper">
        const allFunctions = Array.from(functionsData.values(), entry => entry[FUNC_KEY]).flat();
                    <h4 class="filter-toggle">Pricing <div class="plus icon"></div>
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#business-model-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#business-model-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="business-model-filter"></div>
                    </div>
                </div>


        const dctQuery = '[[Category:Disaster Community Technology]]' +
                <!-- <div class="filter-wrapper">
                        '[[Is Archived::No]]' +
                    <h4 class="filter-toggle">Used by Practitioners <div class="plus icon"></div>
                        '|limit=500' +
                    </h4>
                        '|?Image' +
                    <div class="filter-container">
                        '|?Data Sources' +
                        <div class="filter-button-wrapper">
                        '|?' + allFunctions.join('|?');
                            <button type="button" onclick="selectAll('#dm-use-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#dm-use-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="dm-use-filter"></div>
                    </div>
                </div> -->


        const dctResponse = await fetch(getQueryUrl(dctQuery)).then(response => response.json());
                <div id="bool-filters"
        const dctList = Object.keys(dctResponse.query.results).map(dctKey => {
                    style="border-top: 1px solid var(--links-blue); margin-top: 1em; padding-top: 1em;">
            const dctResult = dctResponse.query.results[dctKey];
                    <div>
 
                        <input type="checkbox" id="used-by-practitioners" value="yes">
            /** @type {DCT} */
                        <label for="used-by-practitioners">Used by practitioners</label>
            const dct = {};
                     </div>
            dct.name = dctKey;
                    <div>
            dct.url = dctResult.fullurl;
                        <input type="checkbox" id="has-use-case" value="yes">
            dct.dataSources = dctResult.printouts['Data Sources'].map(source => source.fulltext).sort();
                        <label for="has-use-case">Use case available</label>
            dct.logo = dctResult.printouts['Image'][0] ? getFilePath(dctResult.printouts['Image'][0].fulltext) : undefined;
                    </div>
            dct.functions = [];
                    <div>
 
                        <input type="checkbox" id="show-archived" value="yes">
            functionsData.forEach((value, key) => {
                        <label for="show-archived">Show archived</label>
                if (value[FUNC_KEY].some(
                    </div>
                    func => dctResult.printouts[func][0] && dctResult.printouts[func][0].fulltext.toLowerCase() === 'yes'
                </div>
                )) {
                     dct.functions.push(key);
                }
            });
 
            return dct;
        });
 
        return dctList;
    }
 
    /**
    * @param {DCT} dct
    * @param {Partial<DCT>} filterState
    */
    function dctFilter(dct, filterState) {
        // If filtering property is empty, don't apply the filter (set the check to true).
        // Passing an empty object (as with applyFilters(true)) should result in an unfiltered table.
        const sourcesCheck = filterState.dataSources
            ? dct.dataSources.some(source => filterState.dataSources.includes(source))
            : true;
        const functionsCheck = filterState.functions
            ? dct.functions.some(func => filterState.functions.includes(func))
            : true;
        return sourcesCheck && functionsCheck;
    }
 
    function applyFilters(clear) {
        if (!table) return;
 
        // If clear=true, pass empty object to the filter to disable it.
        if (clear) {
            table.setFilter(dctFilter, {});
            return;
        }
 
        /** @type {Partial<DCT>} */
        const filterState = {};
 
        const functionOptions = Array.from(document.querySelectorAll('#functions-filter input[type="checkbox"]'));
        const selectedFunctions = functionOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);


        // If all checkboxes are checked, disable the filter property.
        filterState.functions = selectedFunctions.length === functionOptions.length ? undefined : selectedFunctions;
        const sourceOptions = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]'));
        const selectedSources = sourceOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
        // If all checkboxes are checked, disable the filter property.
        filterState.dataSources = selectedSources.length === sourceOptions.length ? undefined : selectedSources;
        table.setFilter(dctFilter, filterState);
    }
    // Load data and build page.
    Promise.all([getSources(), getDcts()]).then(data => {
        const dataSources = data[0];
        const dcts = data[1];
        // The keys of this object must match function category names EXACTLY.
        const fnImages = {
            'Search and Monitor':  '/index.php/Special:FilePath/File:Func_search.svg',
            'Post and Schedule':    '/index.php/Special:FilePath/File:Func_post.svg',
            'Analysis':            '/index.php/Special:FilePath/File:Func_analysis.svg',
            'Metrics':              '/index.php/Special:FilePath/File:Func_metrics.svg',
            'Report':              '/index.php/Special:FilePath/File:Func_report.svg',
            'Collaboration':        '/index.php/Special:FilePath/File:Func_collaboration.svg',
            'Interoperability':    '/index.php/Special:FilePath/File:Func_interoperability.svg',
            'Meta':                '/index.php/Special:FilePath/File:Func_meta.svg',
        };
        // Set up filters.
        const functionFilterHtml = Object.keys(fnImages).reduce((acc, funcName) => {
            const identifier = escapeAttr(funcName);
            return acc + '<div><input type="checkbox" id="func-filter-' + identifier + '" value="' + funcName + '" checked>' +
                '<label for="func-filter-' + identifier + '"><img src="' + fnImages[funcName] + '"> ' + funcName + '</label></div>'
        }, '');
        document.getElementById('functions-filter').innerHTML = functionFilterHtml;
        const groupedSources = [];
        const sourcesCopy = Array.from(dataSources);
        for (const layoutGroup of sourcesLayout) {
            const group = [];
            for (const source of layoutGroup.sources) {
                let idx = sourcesCopy.findIndex(src => src.name === source);
                if (idx !== -1) { group.push(sourcesCopy.splice(idx, 1)[0]); }
            }
            if (group.length > 0) { groupedSources.push({ title: layoutGroup.title, sources: group }); }
        }
        groupedSources.push({ title: 'More platforms', sources: sourcesCopy });
        let dataSourceFilterHtml = '';
        groupedSources.forEach(group => {
            dataSourceFilterHtml += '<div class="filter-group-header">' + group.title + '</div>';
            dataSourceFilterHtml += group.sources.reduce((acc, curr, idx) => {
                const identifier = escapeAttr(curr.name);
                return acc +
                '<div ' + (idx === 0 ? ' class="filter-group-start">' : '>') +
                '<input type="checkbox" id="filter-' + identifier + '" value="' + curr.name + '" checked>' +
                '<label for="filter-' + identifier + '"><img src="' + curr.image + '"> ' + curr.name + '</label></div>'
            }, '');
        })
        document.getElementById('data-source-filter').innerHTML = dataSourceFilterHtml;
        // Set up table.
        const tabulator = new Tabulator("#dct-tabulator", {
            layout: 'fitColumns',
            responsiveLayout: 'collapse',
            columns: [
                {
                    title: 'Name',
                    field: 'name',
                    minWidth: 300, // required for responsiveness when using fitColumns
                    formatter: function (cell) {
                        /** @type {DCT} */
                        const dct = cell.getData();
                        return '<a href="' + dct.url + '">' + dct.name + '</a>';
                    }
                },
                {
                    title: 'Functions',
                    field: 'functions',
                    minWidth: 300, // required for responsiveness when using fitColumns
                    cssClass: 'functions-cell',
                    formatter: function (cell) {
                        const output = cell.getValue()
                            .map(func => '<img class="func-img" src="' + fnImages[func] +
                                '" data-value="' + func +
                                '" alt="' + func +
                                '" title="' + func +
                                '">')
                            .join('');
                        return output;
                    }
                },
                {
                    title: 'Supported Platforms',
                    field: 'dataSources',
                    minWidth: 300, // required for responsiveness when using fitColumns
                    cssClass: 'data-sources-cell',
                    formatter: function (cell) {
                        const val = cell.getValue();
                        let out = '';
                        groupedSources.forEach((group, gIndex) => {
                            if (gIndex === groupedSources.length - 1) { out += '<div class="sources-collapse">'; }
                            out += group.sources.reduce((prev, curr) => {
                                const idx = val.findIndex(src => src === curr.name);
                                if (idx === -1) {
                                    return prev;
                                } else {
                                    return curr.image
                                        ? prev + '<img class="data-source-img" data-value="' + curr.name
                                              + '" src="' + curr.image
                                              + '" alt="' + curr.name
                                              +'" title="' + curr.name + '">'
                                        : prev + ' ' + curr.name;
                                }
                            }, '');
                            if (gIndex === groupedSources.length - 1) {
                                // out += '<span class="sources-collapse-toggle"></span>';
                                out += '</div>';
                            }
                        });
                        return out;
                    }
                }
            ]
        });
        tabulator.on('tableBuilt', () => {
            tabulator.setData(dcts);
            table = tabulator;
        });
        tabulator.on('dataFiltered', (filters, rows) => {
            if (!(filters[0] && filters[0].type)) { return; }
            const markImages = () => {
                const selectedSources = filters[0].type.dataSources;
                const selectedFunctions = filters[0].type.functions;
                // Mark data source images
                document.querySelectorAll('.data-sources-cell .data-source-img, .tabulator-responsive-collapse .data-source-img')
                    .forEach(img => {
                        if (!selectedSources || selectedSources.includes(img.dataset.value)) { img.classList.remove('unselected'); }
                        else { img.classList.add('unselected'); }
                    });
                // Mark functions images
                document.querySelectorAll('.functions-cell .func-img').forEach(img => {
                    if (!selectedFunctions || selectedFunctions.includes(img.dataset.value)) { img.classList.remove('unselected'); }
                    else { img.classList.add('unselected'); }
                });
                tabulator.off('renderComplete', markImages);
            }
            tabulator.on('renderComplete', markImages);
        });
        document.getElementById('data-source-filter').addEventListener('change', event => {
            applyFilters();
        }, { passive: true });
        document.getElementById('functions-filter').addEventListener('change', event => {
            applyFilters();
        }, { passive: true });
    });
    function selectAll(context) {
        document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = true);
        applyFilters();
    }
    function deselectAll(context) {
        document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = false);
        applyFilters();
    }
    function clearFilters() {
        document.querySelectorAll('#dct-filters input[type="checkbox').forEach(checkbox => checkbox.checked = checkbox.defaultChecked);
        applyFilters(true);
    }
    </script>
    <div id="dct-list-wrapper">
        <h1>
            <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" viewBox="0 0 96 96"><defs><clipPath id="a"><path d="M65 334h96v96H65z"/></clipPath></defs><g clip-path="url(#a)" transform="translate(-65 -334)"><path d="m142 395.981.004-35.993H84.001v36.01Zm-55.999-33.993h54.003L140 393.981l-53.999.017Z"/><path d="M81.001 358.987a2.002 2.002 0 0 1 2-2h60.003a2.002 2.002 0 0 1 2 2V400h2v-41.013a4.003 4.003 0 0 0-4-4H83.001a4.003 4.003 0 0 0-4 4V400h2ZM118.003 402.981v2h-10.001v-2H67v1.999a5.006 5.006 0 0 0 5 5.001h82.004a5.006 5.006 0 0 0 5.001-5.001v-1.999Zm36.001 5H72a3.004 3.004 0 0 1-3-3h37.002a1.934 1.934 0 0 0 2 2h10.001a1.934 1.934 0 0 0 2-2h37.002a3.005 3.005 0 0 1-3.001 3Z"/><path d="M113 365c-7.18 0-13 5.82-13 13s5.82 13 13 13 13-5.82 13-13c-.008-7.176-5.824-12.992-13-13Zm10.949 12h-3.518a14.167 14.167 0 0 0-4.176-9.508 11.023 11.023 0 0 1 7.694 9.508ZM112 368.226V377h-4.426c.254-3.771 1.929-7.06 4.426-8.774ZM112 379v8.775c-2.494-1.717-4.17-5.017-4.425-8.775Zm2 8.774V379h4.425c-.255 3.754-1.932 7.056-4.425 8.774ZM114 377v-8.773c2.492 1.718 4.17 5.019 4.425 8.773Zm-4.273-9.502a14.124 14.124 0 0 0-4.158 9.502h-3.518a11.02 11.02 0 0 1 7.676-9.502ZM102.051 379h3.518a14.157 14.157 0 0 0 4.173 9.507 11.022 11.022 0 0 1-7.691-9.507Zm14.205 9.508a14.168 14.168 0 0 0 4.175-9.508h3.518a11.023 11.023 0 0 1-7.693 9.508Z"/></g></svg>
            <span>Technologies</span>
        </h1>
        <div id="dct-intro">
        <p>This page provides an&nbsp;overview of&nbsp;various Technologies related to Social Media and Crowdsourcing. You can use the&nbsp;filters to&nbsp;identify
            relevant technologies according to your needs and then click on the name of a&nbsp;tool to&nbsp;get further information.</p>
        </div>
        <div id="dct-filters">
            <h2>
                <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" viewBox="0 0 96 96"><defs><clipPath id="b"><path d="M592 312h96v96h-96z"/></clipPath></defs><g clip-path="url(#b)" transform="translate(-592 -312)"><path d="M636 356.012v38.011l8-8v-30.011L674 326h-68Zm6.588-1.412-.588.584v30.008l-4 4v-34.008l-.585-.586L610.828 328h58.348Z"/></g></svg>
                <span>Filters</span>
            </h2>
            <div style="text-align: center;">
                <button id="clear-filters-button" type="button" onclick="clearFilters()">Clear Filters</button>
            </div>
            <div class="filter-wrapper">
                <h3>Functions</h3>
                <div class="filter-button-wrapper">
                    <button type="button" onclick="selectAll('#functions-filter')">Select all</button> |
                    <button type="button" onclick="deselectAll('#functions-filter')">Deselect all</button>
                </div>
                <div class="filter-content" id="functions-filter" ></div>
            </div>
            <div class="filter-wrapper">
                <h3>Supported Platforms</h3>
                <div class="filter-button-wrapper">
                    <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
                    <button type="button" onclick="deselectAll('#data-source-filter')">Deselect all</button>
                </div>
                <div class="filter-content" id="data-source-filter"></div>
             </div>
             </div>
         </div>
         </div>
         <div id="dct-tabulator"></div>
         <div id="dct-tabulator"></div>
     </div>
     </div>
</includeonly>
</includeonly>

Latest revision as of 09:46, 2 October 2024

DCT list widget.
Currently in use – do not modify!