Widget: DCTList: Difference between revisions

From LINKS Community Center
Jump to: navigation, search
Eschmidt (talk | contribs)
No edit summary
Eschmidt (talk | contribs)
No edit summary
(353 intermediate revisions by 2 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="https://unpkg.com/tabulator-tables@5.2.7/dist/css/tabulator.min.css" rel="stylesheet">
     <link href="/resources/assets/tabulator/dist/css/tabulator.min.css" rel="stylesheet">
     <script type="text/javascript" src="https://unpkg.com/tabulator-tables@5.2.7/dist/js/tabulator.min.js"></script>
     <script type="text/javascript" src="/resources/assets/tabulator/dist/js/tabulator.min.js"></script>


     <style>
     <style>
         #dct-list-wrapper { font-family: 'Open Sans'; margin-top: 4em; }
         #dct-list-wrapper {
         #dct-list-wrapper h1, #dct-list-wrapper h2, #dct-list-wrapper h3 {
            font-family: 'Open Sans';
            margin-top: 4em;
        }
 
         #dct-list-wrapper h1,
        #dct-list-wrapper h2,
        #dct-list-wrapper h3,
        #dct-list-wrapper h4 {
             font-family: 'Raleway';
             font-family: 'Raleway';
             font-weight: 300;
             font-weight: 300;
             letter-spacing: .06em;
             letter-spacing: .06em;
         }
         }
         #dct-list-wrapper h1, #dct-filters h2 {
 
            color: var(--links-blue);
         #dct-list-wrapper h1,
        #dct-filters h2 {
             display: flex;
             display: flex;
             align-items: center;
             align-items: center;
         }
         }
         #dct-intro {
         #dct-intro {
             font-size: larger;
             font-size: larger;
             margin-bottom: 4em;
             margin-bottom: 4em;
         }
         }
        #dct-list-wrapper h1 {
            color: var(--links-blue);
        }
         #dct-list-wrapper h1 svg {
         #dct-list-wrapper h1 svg {
             height: 2.5em;
             height: 2.5em;
Line 26: Line 40:
             margin-right: .5em;
             margin-right: .5em;
         }
         }
        #filter-bar {
            position: relative;
            margin: 1em 0 2em 0;
        }
        #dct-filters {
            position: absolute;
            top: 0;
            right: 0;
            z-index: 100;
            padding: 2em;
            width: 45vw;
            background: #fff;
            border: 1px solid var(--links-blue);
            clip-path: inset(0 0 100% 100%);
            box-shadow: -10px 10px 10px 5px rgb(0 0 0 / 10%);
            transition: all 400ms ease-in-out;
        }
        #dct-filters.open {
            clip-path: inset(0 0 -50px -50px);
        }
        #close-filter-button {
            display: block;
            cursor: pointer;
            font-size: 2.5em;
            line-height: .7em;
            margin-top: -.2em;
            font-weight: 100;
            color: var(--links-blue);
        }
         #dct-filters h2 svg {
         #dct-filters h2 svg {
             height: 1.5em;
             height: 1.5em;
             width: 1.5em;
             width: 1.5em;
            fill: var(--links-blue);
             margin-right: .5em;
             margin-right: .5em;
             margin-left: -.2em;
             margin-left: -.2em;
         }
         }
         #clear-filters-button {
 
         .large-button {
             border: 1px solid var(--links-blue);
             border: 1px solid var(--links-blue);
             font-size: 1.5em;
             font-size: 1.5em;
Line 43: Line 91:
             background: transparent;
             background: transparent;
             font-variant: small-caps;
             font-variant: small-caps;
            display: inline-block;
            transition: all 200ms ease-in-out;
        }
        .large-button:hover {
            background-color: var(--links-blue);
            color: #fff;
         }
         }
         #dct-list-wrapper h2 {
         #dct-list-wrapper h2 {
             margin-bottom: 1em;
             margin-bottom: 1em;
         }
         }
         .filter-button-wrapper {
         .filter-button-wrapper {
             margin: 1.5em 0 1em 0;
             margin-bottom: 1em;
         }
         }
         .filter-wrapper button {
         .filter-wrapper button {
             border: 0 none;
             border: 0 none;
Line 59: Line 117:
             padding: 0;
             padding: 0;
         }
         }
        .filter-wrapper .filter-toggle {
            cursor: pointer;
        }
        .filter-wrapper .filter-container {
            display: none;
        }
        .filter-wrapper.open .filter-container {
            display: block;
        }
        .filter-wrapper.open .plus.icon::after {
            transform: rotate(0);
        }
         .filter-content {
         .filter-content {
             font-size: 1.2em;
             font-size: 1.2em;
             margin-bottom: 4em;
             margin-bottom: 2em;
             display: grid;
             display: grid;
             grid-template-columns: repeat(auto-fit, minmax(14em, 1fr));
             grid-template-columns: repeat(auto-fit, minmax(14em, 1fr));
            gap: 1em 1em;
             align-items: start;
             align-items: end;
         }
         }
        .filter-content.loose {
            gap: .5em .5em;
        }
         .filter-group-header {
         .filter-group-header {
             grid-column: 1/-1;
             grid-column: 1/-1;
Line 75: Line 154:
             letter-spacing: .06em;
             letter-spacing: .06em;
         }
         }
         .filter-group-start { grid-column-start: 1; }
 
         .filter-content img { width: 2em; height: 2em; margin: 0 .5em; }
         .filter-group-start {
         #dct-tabulator.tabulator { border: 0 none; background-color: transparent; font-size: 1.2em; }
            grid-column-start: 1;
         #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; }
        .filter-content input[type="checkbox"] {
         #dct-tabulator.tabulator .tabulator-header .tabulator-col-sorter { right: 1em; }
            margin-right: .5em;
         #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; }
         .filter-content img {
         .source-img-placeholder { display:inline-block; width: 2em; height: 2em; }
            width: 2em;
            height: 2em;
            margin: 0 .5em 0 0;
        }
 
        .subfunc-filter-block {
            font-size: smaller;
            padding-left: 1em;
        }
 
         #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-top: 1px solid var(--links-blue);
            padding: .5em 0;
        }
 
         .data-source-img,
        .func-img {
            width: 1.2rem;
            height: 1.2rem;
            margin-right: .4rem;
            margin-bottom: .4rem;
            filter: grayscale(1);
        }
 
         .func-img {
            width: 1.5rem;
            height: 1.5rem;
        }
 
         #dct-tabulator img.unselected {
         #dct-tabulator img.unselected {
            filter: grayscale(1);
             opacity: .15;
             opacity: .25;
         }
         }
         .sources-collapse {
         .sources-collapse {
             display: inline-block;
             display: inline-block;
            opacity: .3;
         }
         }
        .sources-collapse-toggle {
            display: inline-block;
            width: 2em;
            height: 2em;
        }
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse {
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse {
             padding: .5em 0 1.5em 0;
             padding: 0 .5em 2em 0;
             border: 0 none;
             border: 0 none;
         }
         }
        #filter-summary {
            margin-right: 1em;
        }
        #filter-summary table tr td {
            vertical-align: top;
        }
        #filter-summary table tr td:first-of-type {
            padding-right: 10px;
        }
        #dct-tabulator .tabulator-row .tabulator-responsive-collapse table {
            font-size: smaller;
        }
        #filter-summary table tr td strong,
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse table tr td strong {
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse table tr td strong {
            font-family: 'Raleway';
             font-weight: 300;
             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>
     </style>


     <script>
     <script>
    'use strict';
        'use strict';


    /**
        const FUNC_KEY = 'functions';
    * @typedef {Object} DCT
        const DESC_KEY = 'description';
    * @property {string} name
    * @property {string} url
    * @property {string[]} dataSources
    * @property {string[]} functions
    * @property {string} logo
    */


    let table;
        /** @typedef {Map<string, { [DESC_KEY]: string, [FUNC_KEY]: string[] }>} FuncData */


    const FN_SEARCH = 'Search & Monitor';
        /** @type FuncData */
    const FN_POST = 'Post & Schedule';
        const functionsData = new Map();   // Data on functions and function categories
    const FN_ANALYSIS = 'Analysis';
    const FN_METRICS = 'Metrics';
    const FN_REPORT = 'Report';
    const FN_COLLAB = 'Collaboration';
    const FN_INTEROP = 'Interoperability';
    const FN_META = 'Meta';


    const fnImages = {};
        /**
    fnImages[FN_SEARCH] = '/index.php/Special:FilePath/File:Func_search.svg';
        * @typedef {Object} DCT
    fnImages[FN_POST] = '/index.php/Special:FilePath/File:Func_post.svg';
        * @property {string} name
    fnImages[FN_ANALYSIS] = '/index.php/Special:FilePath/File:Func_analysis.svg';
        * @property {string} url
    fnImages[FN_METRICS] = '/index.php/Special:FilePath/File:Func_metrics.svg';
        * @property {string[]} dataSources
    fnImages[FN_REPORT] = '/index.php/Special:FilePath/File:Func_report.svg';
        * @property {string[]} businessModel
    fnImages[FN_COLLAB] = '/index.php/Special:FilePath/File:Func_collaboration.svg';
        * @property {FuncData} functions
    fnImages[FN_INTEROP] = '/index.php/Special:FilePath/File:Func_interoperability.svg';
        * @property {string} usedByDmo
    fnImages[FN_META] = '/index.php/Special:FilePath/File:Func_meta.svg';
        * @property {string} archived
        * @property {string} hasUC
        * @property {string} logo
        */


    const sourcesLayout = [
        let table;  // Tabulator instance
        {
 
            title: 'General',
        // This defines how sources are grouped in the filter.
            sources: ['Crowd', 'Web']
        // Any source not listed here will be added to the last group entitled "More platforms".
        },
        const sourcesLayout = [
        {
            {
            title: 'Platforms',
                title: 'General',
            sources: ['Facebook', 'Twitter', 'Instagram', 'YouTube']
                sources: ['Crowd', 'Web']
            },
            {
                title: 'Platforms',
                sources: ['Facebook', 'Twitter', 'Instagram', 'YouTube']
            }
        ];
 
        // This object defines the sort order of function categories and matches them to icons.
        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',
        };
 
        // Helpers
        const escapeAttr = text => text ? text.replace(/\W/g, '-') : text;
        const getFilePath = title => title ? '/index.php/Special:FilePath/' + title : title;
        const getQueryUrl = query => '/api.php?action=ask&format=json&query=' + encodeURIComponent(query);
        const removePrefix = str => str.substring(str.indexOf(':') + 1);
 
        // Fetches platform / data source information.
        async function getSources() {
            const IMG_KEY = 'IMAGE';
            const sourceResponse = await fetch(
                getQueryUrl('[[Category:Social media platform]]|?' + IMG_KEY)
            ).then(response => response.json());
 
            return Object.keys(sourceResponse.query.results).map(platform => {
                const img = sourceResponse.query.results[platform].printouts[IMG_KEY][0];
                return {
                    name: platform,
                    image: img ? getFilePath(img.fulltext) : undefined
                };
            });
         }
         }
    ];


    const getUrl = title =>  title ? '/index.php/Special:FilePath/' + title : title;
        // Fetches DCTs and their functions.
        async function getDcts() {
            const functionsQuery = '[[Category:Function_category]]'
                + '|?-Subproperty_of=' + FUNC_KEY
                + '|?Has_description=' + DESC_KEY;
            const functionsQueryResponse = await fetch(getQueryUrl(functionsQuery)).then(response => response.json());


    const escapeAttr = text => text ? text.replace(/\s/g, '-') : text;
            const sortOrder = Object.keys(fnImages);
            Object.entries(functionsQueryResponse.query.results)
                .map(([key, value]) => ([removePrefix(key), value]))
                .sort(([keyA,], [keyB,]) => sortOrder.indexOf(keyA) - sortOrder.indexOf(keyB))
                .forEach(([categoryName, results]) => {
                    functionsData.set(
                        categoryName,
                        {
                            [DESC_KEY]: results.printouts[DESC_KEY][0],
                            [FUNC_KEY]: results.printouts[FUNC_KEY]
                                .map(func => removePrefix(func.fulltext))
                                .filter(func => func !== 'Content processing languages' && func !== 'Supported content types')
                                .sort()
                        }
                    );
                });


    const hasFunction = (dctResult, subfunctions) => subfunctions.some(func => {
            const allFunctions = Array.from(functionsData.values(), entry => entry[FUNC_KEY]).flat();
        const printoutResult = dctResult.printouts[func];
        return printoutResult && printoutResult[0] && printoutResult[0].fulltext.toLowerCase() === 'yes'
    });


    async function getSources() {
            const DATASRC_PROP = 'Data Sources';
        const sourceQuery = '/api.php?action=ask&format=json&query=' + encodeURIComponent('[[Category:Social media platform]]|?IMAGE');
            const IMG_PROP = 'Image';
        const sourceResponse = await fetch(sourceQuery).then(response => response.json());
            const BUSINESS_PROP = 'Pricing';
            const DMO_PROP = 'Used by Practitioners';
            const UC_PROP = 'Use Cases available';
            const ARCHIVED = 'Is Archived';
            const dctQuery = '[[Category:Disaster Community Technology]]'
                // + '[[Is Archived::No]]'
                + '|limit=500'
                + '|?' + ARCHIVED
                + '|?' + IMG_PROP
                + '|?' + DATASRC_PROP
                + '|?' + BUSINESS_PROP
                + '|?' + DMO_PROP
                + '|?' + UC_PROP
                + '|?' + allFunctions.join('|?');


        const results = sourceResponse.query.results;
            const dctResponse = await fetch(getQueryUrl(dctQuery)).then(response => response.json());
        const sources = Object.getOwnPropertyNames(results).map(platformName => ({
            name: platformName,
            image: getUrl(results[platformName].printouts['IMAGE'][0].fulltext.replace('PAGENAME:', ''))
        }));


        return sources;
            const dctList = Object.keys(dctResponse.query.results).map(dctName => {
    }
                const dctResult = dctResponse.query.results[dctName];


    async function getDcts() {
                /** @type {DCT} */
        const searchFunctions = [
                const dct = {};
            'Advanced search features',
                dct.name = dctName;
            'Keyword search',
                dct.url = dctResult.fullurl;
            'Hashtag search',
                dct.dataSources = dctResult.printouts[DATASRC_PROP].map(source => source.fulltext).sort();
            'Keyword monitoring',
                dct.businessModel = dctResult.printouts[BUSINESS_PROP].map(bModel => bModel.fulltext);
            'Hashtag monitoring',
                dct.logo = dctResult.printouts[IMG_PROP][0] ? getFilePath(dctResult.printouts[IMG_PROP][0].fulltext) : undefined;
            'Event monitoring',
                dct.usedByDmo = dctResult.printouts[DMO_PROP][0] === 't' ? 'yes' : 'no';    // not quite, but we only care about yes
            'Event notifications'
                dct.hasUC = dctResult.printouts[UC_PROP][0] === 't' ? 'yes' : 'no';   // not quite, but we only care about yes
        ];
                dct.archived = dctResult.printouts[ARCHIVED][0] === 't' ? 'yes' : 'no';    // not quite, but we only care about yes
        const postFunctions = [
 
            'Posting content',
                dct.functions = new Map();
            'Scheduling content',
                functionsData.forEach((categoryData, funcCategory) => {
            'Post time optimization',
                    const confirmedFunctions = categoryData[FUNC_KEY]
            'Content library'
                        .filter(func => dctResult.printouts[func][0] && dctResult.printouts[func][0].fulltext.toLowerCase() === 'yes');
        ];
        const analysisFunctions = [
            'Text analysis',
            'Image analysis',
            'Video analysis',
            'Topic analysis',
            'Sentiment analysis',
            'Trend analysis'
        ];
        const metricsFunctions = [
            'Post metrics',
            'Profile or Site metrics',
            'Network metrics',
            'Follower metrics',
            'Audience metrics',
            'Competitor metrics'
        ];
        const reportFunctions = [
            'Filtering sorting searching',
            'Clustering Aggregation',
            'Visualization options',
            'PDF export'
        ];
        const collaborationFunctions = [
            'Multiuser',
            'Permission management',
            'Inbox workflow',
            'Approval workflows'
        ];
        const interoperabilityFunctions = [
            'Data export',
            'Third party tool integration',
            'API support'
        ];
        const metaFunctions = [
            'White Label',
            'GDPR compliant',
            'Historical data access',
            'Multiple accounts per platform'
        ];


        const dctQuery = '[[Category:Disaster Community Technology]]' +
                    if (confirmedFunctions.length > 0) {
                        '[[Is Archived::No]]' +
                        dct.functions.set(funcCategory, { [DESC_KEY]: categoryData[DESC_KEY], [FUNC_KEY]: confirmedFunctions });
                        '|limit=500' +
                    }
                        '|?Image' +
                });
                        '|?Data Sources' +
                        '|?' + searchFunctions.join('|?') +
                        '|?' + postFunctions.join('|?') +
                        '|?' + analysisFunctions.join('|?') +
                        '|?' + metricsFunctions.join('|?') +
                        '|?' + reportFunctions.join('|?') +
                        '|?' + collaborationFunctions.join('|?') +
                        '|?' + interoperabilityFunctions.join('|?') +
                        '|?' + metaFunctions.join('|?');


        const dctQueryUrl = '/api.php?action=ask&format=json&query=' + encodeURIComponent(dctQuery);
                return dct;
        const dctResponse = await fetch(dctQueryUrl).then(response => response.json());
            });


        const results = dctResponse.query.results;
            return dctList;
         const dctList = Object.getOwnPropertyNames(results).map(dctKey => {
         }


            /** @type {DCT} */
        /**
            const dct = {};
        * @param {DCT} dct
             const dctResult = results[dctKey];
        */
            dct.name = dctKey;
        function dctFilter(dct, filterState) {
            dct.url = dctResult.fullurl;
             // If filtering property is empty, don't apply the filter (set the check to true).
            dct.dataSources = dctResult.printouts['Data Sources'].map(source => source.fulltext).sort();
             // Passing an empty object (as with applyFilters(true)) should result in an unfiltered table.
             dct.logo = dctResult.printouts['Image'][0] ? getUrl(dctResult.printouts['Image'][0].fulltext) : void 0;
            dct.functions = [];


             if (hasFunction(dctResult, searchFunctions)) dct.functions.push(FN_SEARCH);
             let functionsCheck = true;  // automatically pass functions check if the filter missing
             if (hasFunction(dctResult, postFunctions)) dct.functions.push(FN_POST);
             if (filterState.functions) {
            if (hasFunction(dctResult, analysisFunctions)) dct.functions.push(FN_ANALYSIS);
                // If filterState has a category but subfunctions array is empty, we only care about the category.
            if (hasFunction(dctResult, metricsFunctions)) dct.functions.push(FN_METRICS);
                const emptyCategories = [], nonemptyCategories = [];
            if (hasFunction(dctResult, reportFunctions)) dct.functions.push(FN_REPORT);
                Array.from(filterState.functions).forEach(([key, subs]) => {
            if (hasFunction(dctResult, collaborationFunctions)) dct.functions.push(FN_COLLAB);
                    if (subs.length > 0) { nonemptyCategories.push(key); } else { emptyCategories.push(key); }
            if (hasFunction(dctResult, interoperabilityFunctions)) dct.functions.push(FN_INTEROP);
                });
            if (hasFunction(dctResult, metaFunctions)) dct.functions.push(FN_META);


            return dct;
                // Empty categories should only be checked by their name
        });
                const checkEmpty = emptyCategories.every(cat => dct.functions.has(cat));


        return dctList;
                // Nonempty categories should only be checked for presence of their subfunctions. Category itself is irrelevant.
    }
                const checkNonempty = nonemptyCategories.every(cat => {
                    const selectedSubs = filterState.functions.get(cat);
                    const dctSubs = dct.functions.get(cat);
                    return dctSubs && dctSubs[FUNC_KEY] && selectedSubs.every(sub => dctSubs[FUNC_KEY].includes(sub));
                });


    /**
                functionsCheck = checkEmpty && checkNonempty;
    * @param {DCT} dct
                // TODO: Empty should no longer exist, once every category is fully listed in the filter.
    * @param {Partial<DCT>} filterState
                // TODO: The filtering of functions therefore should be reduced to nonempty only.
    */
             }
    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) {
            const sourcesCheck = filterState.dataSources
        if (!table) return;
                ? filterState.dataSources.every(source => dct.dataSources.includes(source))
                : true;
            const businessModelCheck = filterState.businessModel
                ? dct.businessModel.some(bm => filterState.businessModel.includes(bm))
                : true;
            const dmUseCheck = filterState.usedByDmo
                ? filterState.usedByDmo === dct.usedByDmo
                : true;
            const ucCheck = filterState.hasUC
                ? filterState.hasUC === dct.hasUC
                : true;
            const archivedCheck = filterState.showArchived
                ? true
                : dct.archived !== 'yes';


        // If clear=true, pass empty object to the filter to disable it.
             return sourcesCheck && functionsCheck && businessModelCheck && dmUseCheck && ucCheck && archivedCheck;
        if (clear) {
            table.setFilter(dctFilter, {});
             return;
         }
         }


         /** @type {Partial<DCT>} */
         function applyFilters(clear) {
        const filterState = {};
            if (!table) return; // prevent filtering before the table is ready


        const selectedFunctions = Array.from(document.querySelectorAll('#functions-filter input[type="checkbox"]:checked')).map(checkbox => checkbox.value);
            // If clear=true, pass empty object to the filter to disable it.
        filterState.functions = selectedFunctions;
            if (clear) {
                table.setFilter(dctFilter, {});
                return;
            }


        const selectedSources = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]:checked')).map(checkbox => checkbox.value);
            const filterState = {};
        filterState.dataSources = selectedSources;


        table.setFilter(dctFilter, filterState);
            if (document.querySelectorAll('#functions-filter input[type="checkbox"]:checked').length > 0) {
    }
                const functionFilterBlocks = document.querySelectorAll('#functions-filter .func-filter-block');
                const funcOpts = new Map();
                functionFilterBlocks.forEach(block => {
                    const cat = block.querySelector('input.func-cat');
                    if (cat.checked) {
                        funcOpts.set(
                            cat.value,
                            Array.from(block.querySelectorAll('.subfunc-filter-block input[type="checkbox"]:checked'))
                                .map(box => box.value)
                        );
                    }
                });
                filterState.functions = funcOpts;
            }
 
            const selectedSources = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]:checked'))
                .map(checkbox => checkbox.value);
            if (selectedSources.length > 0) filterState.dataSources = selectedSources;


    // Load data and build page.
            const selectedBModels = Array.from(document.querySelectorAll('#business-model-filter input[type="checkbox"]:checked'))
    Promise.all([getSources(), getDcts()]).then(data => {
                .map(checkbox => checkbox.value);
        const dataSources = data[0];
            if (selectedBModels.length > 0) filterState.businessModel = selectedBModels;
        const dcts = data[1];


        // Set up filters.
            if (document.getElementById('used-by-practitioners').checked) filterState.usedByDmo = 'yes';
        const functionFilterHtml = Object.getOwnPropertyNames(fnImages).reduce((acc, funcName) => {
            if (document.getElementById('has-use-case').checked) filterState.hasUC = 'yes';
            const identifier = escapeAttr(funcName);
            if (document.getElementById('show-archived').checked) filterState.showArchived = 'yes';
            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 = [];
            table.setFilter(dctFilter, filterState);
        const sourcesCopy = Array.from(dataSources);
         }
        for (const layoutGroup of sourcesLayout) {
 
            const group = [];
        // Load data and build page.
            for (const source of layoutGroup.sources) {
        Promise.all([getSources(), getDcts()]).then(data => {
                let idx = sourcesCopy.findIndex(src => src.name === source);
            const [dataSources, dcts] = data;
                if (idx !== -1) { group.push(sourcesCopy.splice(idx, 1)[0]); }
 
            // Set up functions filter
            let funcFilterHtml = '';
            Array.from(functionsData).forEach(([fnCat, fnInfo], index) => {
                const identifier = 'func-filter-' + escapeAttr(fnCat);
                funcFilterHtml +=
                    `<div class="func-filter-block">
                        <div>
                            <input type="checkbox" id="${identifier}" value="${fnCat}" class="func-cat">
                            <label for="${identifier}" title="${fnInfo[DESC_KEY]}"><img src="${fnImages[fnCat]}"> ${fnCat}</label>
                        </div>`;
 
                // add subfunctions
                funcFilterHtml += '<div class="subfunc-filter-block">';
                for (const func of fnInfo.functions) {
                    const subfuncId = 'subfunc-filter-' + escapeAttr(func);
                    funcFilterHtml +=
                        `<div>
                            <input type="checkbox" id="${subfuncId}" value="${func}">
                            <label for="${subfuncId}">${func}</label>
                        </div>`;
                }
                funcFilterHtml += '</div>';
 
                funcFilterHtml += '</div>';
            });
            document.getElementById('functions-filter').innerHTML = funcFilterHtml;
 
            // Set up sources filter
            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 }); }
             }
             }
             if (group.length > 0) { groupedSources.push({ title: layoutGroup.title, sources: group }); }
             groupedSources.push({ title: 'More platforms', sources: sourcesCopy });
        }
 
        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 + '">' +
                        '<label for="filter-' + identifier + '"><img src="' + curr.image + '"> ' + curr.name + '</label></div>'
                }, '');
            });
            document.getElementById('data-source-filter').innerHTML = dataSourceFilterHtml;


        let dataSourceFilterHtml = '';
            // TODO: Fetch from server?
        groupedSources.forEach(group => {
            const FREE_KEY = 'Free', FREE_PLAN_KEY = 'Free & Paid';
             dataSourceFilterHtml += '<div class="filter-group-header">' + group.title + '</div>';
             const pricing = [FREE_KEY, FREE_PLAN_KEY, 'Paid'];
             dataSourceFilterHtml += group.sources.reduce((acc, curr, idx) => {
             let pricingFilterHtml = pricing.reduce((acc, curr) => {
                 const identifier = escapeAttr(curr.name);
                 const identifier = escapeAttr(curr);
                 return acc +
                 return acc
                '<div ' + (idx === 0 ? ' class="filter-group-start">' : '>') +
                    + '<div><input type="checkbox" id="bm-filter-' + identifier
                '<input type="checkbox" id="filter-' + identifier + '" value="' + curr.name + '" checked>' +
                    + '" value="' + curr + '">'
                '<label for="filter-' + identifier + '"><img src="' + curr.image + '"> ' + curr.name + '</label></div>'
                    + '<label for="bm-filter-' + identifier + '">' + curr
                    + '</label></div>'
             }, '');
             }, '');
        })
            document.getElementById('business-model-filter').innerHTML = pricingFilterHtml;
        document.getElementById('data-source-filter').innerHTML = dataSourceFilterHtml;


        // Set up table.
            // Set up table.
        const tabulator = new Tabulator("#dct-tabulator", {
            const tabulator = new Tabulator("#dct-tabulator", {
            layout: 'fitColumns',
                layout: 'fitColumns',
            responsiveLayout: 'collapse',
                responsiveLayout: 'collapse',
            columns: [
                columns: [
                {
                    {
                    title: 'Name',
                        title: 'Name',
                    field: 'name',
                        field: 'name',
                    minWidth: 300, // required for responsiveness when using fitColumns
                        minWidth: 300, // required for responsiveness when using fitColumns
                    formatter: function (cell) {
                        widthGrow: 2,
                        /** @type {DCT} */
                        formatter: function (cell) {
                        const dct = cell.getData();
                            /** @type {DCT} */
                        return '<a href="' + dct.url + '">' + dct.name + '</a>';
                            const dct = cell.getData();
                            let out = '<a href="' + dct.url + '" translate="no">' + dct.name + '</a><br>';
                            if (dct.archived.toLowerCase() === 'yes') {
                                out += '<small><span class="badge lcc-badge-grey">Archived</span></small> ';
                            }
                            if (dct.businessModel.includes(FREE_KEY)) {
                                out += '<small><span class="badge lcc-badge-green">' + FREE_KEY + '</span></small> ';
                            }
                            if (dct.businessModel.includes(FREE_PLAN_KEY)) {
                                out += '<small><span class="badge lcc-badge-green">' + FREE_PLAN_KEY + '</span></small> ';
                            }
                            if (dct.usedByDmo.toLowerCase() === 'yes') {
                                out += '<small><span class="badge lcc-badge-red">Used by practitioners</span></small> ';
                            }
                            if (dct.hasUC.toLowerCase() === 'yes') {
                                out += '<small><span class="badge lcc-badge-purple">Use case available</span></small> ';
                            }
 
                            return out;
                        }
                    },
                    {
                        title: 'Functions',
                        field: 'functions',
                        minWidth: 200, // required for responsiveness when using fitColumns
                        cssClass: 'functions-cell',
                        formatter: function (cell) {
                            return Array.from(cell.getValue().keys())
                                .map(fn => `<img class="func-img"
                                    src="${fnImages[fn]}"
                                    data-value="${fn}"
                                    alt="${fn}"
                                    title="${fn}\n\n${functionsData.get(fn)[DESC_KEY]}">`
                                )
                                .join('');
                        }
                    },
                    {
                        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 = '<div>';
 
                            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 + '</div>';
                        }
                     }
                     }
                 },
                 ]
                {
            });
                    title: 'Functions',
 
                    field: 'functions',
            tabulator.on('tableBuilt', () => {
                     minWidth: 300, // required for responsiveness when using fitColumns
                tabulator.setData(dcts);
                     cssClass: 'functions-cell',
                table = tabulator;
                     formatter: function (cell) {
 
                         const output = cell.getValue()
                // Set up the table if parameter was passed.
                             .map(func => '<img class="func-img" src="' + fnImages[func] +
                const params = new URLSearchParams(window.location.search);
                                 '" data-value="' + func +
                const encoded = params.get('do');
                                '" alt="' + func +
 
                                '" title="' + func +
                if (encoded) {
                                '">')
                     const actions = JSON.parse(decodeURIComponent(atob(encoded)));
                            .join('');
 
                         return output;
                     const filter = actions.filter;
                     if (filter) {
                        // Functions filter
                         const functions = filter.functions;
                        if (functions) {
                             Object.keys(functions).forEach(subfun => {
                                const subfunEl = document.getElementById('subfunc-filter-' + escapeAttr(subfun));
                                subfunEl.checked = !!functions[subfun];
                                 subfunEl.dispatchEvent(new Event('change', { bubbles: true }));
                            });
 
                            document.getElementById('functions-filter').closest('.filter-wrapper').classList.toggle('open');
                        }
 
                        // Further filters
                        // ...
 
                        applyFilters();
                         toggleFilter();
                     }
                     }
                },
 
                {
                     // Further actions (e.g. open filter panel, etc.)
                     title: 'Supported Platforms',
                     // ...
                    field: 'dataSources',
                }
                     minWidth: 300, // required for responsiveness when using fitColumns
 
                    cssClass: 'data-sources-cell',
                applyFilters();
                    formatter: function (cell) {
            });
                        const val = cell.getValue();
 
                        let out = '';
            tabulator.on('dataFiltered', (filters, rows) => {
                        groupedSources.forEach((group, gIndex) => {
                const summary = document.getElementById('filter-summary');
                            if (gIndex === groupedSources.length - 1) { out += '<div class="sources-collapse">'; }
                const filter = filters[0];
                            out += group.sources.reduce((prev, curr) => {
 
                                const idx = val.findIndex(src => src === curr.name);
                // Set result counter
                                if (idx === -1) {
                document.getElementById('result-count').textContent = rows.length;
                                    return prev;
 
                                } else {
                // Exit if filter object/type doesn't exist (happens after Tabulator's own filter reset).
                                    return curr.image
                if (!(filter && filter.type)) { summary.textContent = 'No filter. Showing all results.'; return; }
                                        ? prev + '<img class="data-source-img" data-value="' + curr.name + '" src="' + curr.image + '" alt="' + curr.name +'" title="' + curr.name + '">'
 
                                        : prev + ' ' + curr.name;
                // Update filter text.
                                }
                if (
                             }, '');
                    !filter.type.functions &&
                             if (gIndex === groupedSources.length - 1) {
                    !filter.type.dataSources &&
                                 out += '<span class="sources-collapse-toggle">...</span>';
                    !filter.type.businessModel &&
                                 out += '</div>';
                    !filter.type.usedByDmo &&
                    !filter.type.hasUC
                ) { summary.textContent = 'No filter. Showing all results.'; }
                else {
                    let summaryHtml = '<table>';
                    if (filter.type.functions) {
                        summaryHtml += '<tr><td><strong>Functions</strong></td><td>';
                        if (filter.type.functions.size === 0) {
                             summaryHtml += 'none';
                        } else {
                             for (const [cat, subs] of filter.type.functions) {
                                 summaryHtml += cat;
                                summaryHtml += subs.length > 0 ? ' <small>(' + subs.join(', ') + ')</small>' : '';
                                 summaryHtml += ', ';
                             }
                             }
                            summaryHtml = summaryHtml.substring(0, summaryHtml.length - 2);
                        }
                        summaryHtml += '</td></tr>';
                    }
                    if (filter.type.dataSources) {
                        summaryHtml += '<tr><td><strong>Platforms</strong></td><td>'
                            + (filter.type.dataSources.length > 0 ? filter.type.dataSources.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    if (filter.type.businessModel) {
                        summaryHtml += '<tr><td><strong>Pricing</strong></td><td>'
                            + (filter.type.businessModel.length > 0 ? filter.type.businessModel.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    if (filter.type.usedByDmo) {
                        summaryHtml += '<tr><td><strong>Used by practitioners</strong></td><td>Yes</td></tr>';
                    }
                    if (filter.type.hasUC) {
                        summaryHtml += '<tr><td><strong>Use case available</strong></td><td>Yes</td></tr>';
                    }
                    summaryHtml += '</table>';
                    summary.innerHTML = summaryHtml;
                }
                const markImages = () => {
                    const selectedSources = filter.type.dataSources;
                    const selectedFunctions = filter.type.functions ? Array.from(filter.type.functions.keys()) : undefined;
                    // 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'); }
                         });
                         });
                         return out;
 
                     }
                    // 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); // TODO: Prevent this from running if corresponding filters are not active.
        });
            });
 
            // Listen for changes in filter checkbox state.
             document.getElementById('functions-filter').addEventListener('change', event => {
                const filterBlock = event.target.closest('.func-filter-block');
                const category = filterBlock.querySelector('input.func-cat');
                const subfunctions = filterBlock.querySelectorAll('.subfunc-filter-block input[type="checkbox"]');


        tabulator.on('tableBuilt', () => {
                if (event.target === category) {
            tabulator.setData(dcts);
                    // Selecting/deselecting the category checks/unchecks all subfunctions.
            table = tabulator;
                    subfunctions.forEach(checkbox => checkbox.checked = category.checked);
        });
                } else {
                    // If no subfunctions are selected, deactivate the category. Activate otherwise.
                    const checkedSubs = Array.from(subfunctions).filter(sub => sub.checked).length;
                    category.checked = checkedSubs > 0;
                }


        tabulator.on('dataFiltered', (filters, rows) => {
                applyFilters();
             if (!(filters[0] && filters[0].type)) { return; }
            }, { passive: true });
            document.getElementById('data-source-filter').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });
            document.getElementById('business-model-filter').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });
             document.getElementById('bool-filters').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });


             const markImages = () => {
             // Listen for clicks on filter toggles
                 const selectedSources = filters[0].type.dataSources;
            document.querySelectorAll('.filter-wrapper .filter-toggle').forEach(el => {
                 const selectedFunctions = filters[0].type.functions;
                 const wrapper = el.closest('.filter-wrapper');
                 el.addEventListener('click', event => void wrapper.classList.toggle('open'));
            });


                // Mark data source images
            // Fix bug where the table is truncated to zero height despite having visible rows.
                document.querySelectorAll('.data-sources-cell .data-source-img').forEach(img => {
            tabulator.on('renderComplete', function () {
                    if (!selectedSources || selectedSources.includes(img.dataset.value)) { img.classList.remove('unselected'); }
                // TODO: Check the bugfix for a possible infinite event loop.
                    else { img.classList.add('unselected'); }
                // This will help detect it, in case it happens.
                });
                // console.log('Table height bugfix: render complete.');


                 // Mark functions images
                 try {
                document.querySelectorAll('.functions-cell .func-img').forEach(img => {
                    const holderHeight = tabulator.rowManager.element.offsetHeight;
                     if (!selectedFunctions || selectedFunctions.includes(img.dataset.value)) { img.classList.remove('unselected'); }
                    const tableHeight = tabulator.rowManager.tableElement.offsetHeight;
                    else { img.classList.add('unselected'); }
                     if (
                });
                        holderHeight < tableHeight ||                           // table is truncated vertically (including zero-height)
                        holderHeight - tableHeight > window.screen.availHeight  // table is more than a screen longer than content
                    ) {
                        tabulator.redraw();
                    }
                } catch (ignore) { }
            });
            // End bugfix


                tabulator.off('renderComplete', markImages);
            }
            tabulator.on('renderComplete', markImages);
         });
         });


         document.getElementById('data-source-filter').addEventListener('change', event => {
         function selectAll(context) {
            document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = true);
             applyFilters();
             applyFilters();
         }, { passive: true });
         }
        document.getElementById('functions-filter').addEventListener('change', event => {
 
        function deselectAll(context) {
            document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = false);
             applyFilters();
             applyFilters();
         }, { passive: true });
         }
    });


    function selectAll(context) {
        function clearFilters() {
        document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = true);
            document.querySelectorAll('#dct-filters input[type="checkbox').forEach(checkbox => checkbox.checked = checkbox.defaultChecked);
        applyFilters();
            applyFilters(true);
    }
        }


    function deselectAll(context) {
        function toggleFilter() {
        document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = false);
            document.getElementById('dct-filters').classList.toggle('open');
         applyFilters();
         }
    }


    function clearFilters() {
        // Close filter pane when clicked outside of it.
        document.querySelectorAll('#dct-filters input[type="checkbox').forEach(checkbox => checkbox.checked = checkbox.defaultChecked);
        document.body.addEventListener('click', event => {
         applyFilters(true);
            const filterPane = document.getElementById('dct-filters');
    }
            if (
                filterPane.classList.contains('open') &&
                !filterPane.contains(event.target) &&
                event.target !== document.querySelector('#filter-bar .large-button.open-filters')
            ) { filterPane.classList.remove('open'); }
         }, { passive: true });
     </script>
     </script>
    <!-- Icon definitons -->
    <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" style="display:none;">
        <defs>
            <symbol id="chevron-down" viewBox="0 0 96 96">
                <clipPath id="chev">
                    <path d="M592 312h96v96h-96z" />
                </clipPath>
                <g clip-path="url(#chev)" transform="translate(-592 -312)">
                    <path
                        d="m640 370.586-25.293-25.293-1.414 1.414L640 373.414l26.707-26.707-1.414-1.414L640 370.586Z" />
                </g>
            </symbol>
        </defs>
    </svg>


     <div id="dct-list-wrapper">
     <div id="dct-list-wrapper">
         <h1>
         <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>
             <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" viewBox="0 0 96 96">
             <span>Technologies</span>
                <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>
             <div>
                <div>Technologies</div>
                <div style="font-size:small; letter-spacing:.03em; margin-left: .6em;">Social Media and Crowdsourcing
                    Library</div>
            </div>
         </h1>
         </h1>
         <div id="dct-intro">
         <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>
        <div id="filter-bar">
            <div style="display: flex; justify-content: space-between;">
                <div style="flex: 1 1;">
                    <h2 style="margin-bottom: 1rem;">Selected Filters</h2>
                    <div id="filter-summary">No filter. Showing all results.</div>
                </div>
                <div>
                    <button class="large-button open-filters" type="button" onclick="toggleFilter()">Open
                        Filters</button>
                </div>
            </div>


        <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
             <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 2.5rem;">
             relevant technologies according to your needs and then click on the name of a&nbsp;tool to&nbsp;get further information.</p>
                <h2 style="margin-bottom: 0;">Results: <span id="result-count"></span></h2>
        </div>
                <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>
        <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>
             <div class="filter-wrapper">
 
                <h3>Functions</h3>
             <div id="dct-filters">
                <div class="filter-button-wrapper">
                <h2 style="display: flex; justify-content: space-between;">
                    <button type="button" onclick="selectAll('#functions-filter')">Select all</button> |
                    <div>
                     <button type="button" onclick="deselectAll('#functions-filter')">Deselect all</button>
                        <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>
                    </div>
                    <a onclick="toggleFilter()" id="close-filter-button">&times;</a>
                </h2>
                <div style="text-align: center;">
                    <button class="large-button" type="button" onclick="clearFilters()">Clear Filters</button>
                </div>
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Functions <div class="plus icon"></div>
                    </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>
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Supported Platforms <div class="plus icon"></div>
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#data-source-filter')">Clear all</button>
                        </div>
                        <div class="filter-content loose" id="data-source-filter"></div>
                    </div>
                 </div>
                 </div>
                 <div class="filter-content" id="functions-filter" ></div>
                 <div class="filter-wrapper">
            </div>
                    <h4 class="filter-toggle">Pricing <div class="plus icon"></div>
            <div class="filter-wrapper">
                    </h4>
                <h3>Supported Platforms</h3>
                    <div class="filter-container">
                <div class="filter-button-wrapper">
                        <div class="filter-button-wrapper">
                    <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
                            <button type="button" onclick="selectAll('#business-model-filter')">Select all</button> |
                    <button type="button" onclick="deselectAll('#data-source-filter')">Deselect 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>
                <!-- <div class="filter-wrapper">
                    <h4 class="filter-toggle">Used by Practitioners <div class="plus icon"></div>
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <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> -->
                <div id="bool-filters"
                    style="border-top: 1px solid var(--links-blue); margin-top: 1em; padding-top: 1em;">
                    <div>
                        <input type="checkbox" id="used-by-practitioners" value="yes">
                        <label for="used-by-practitioners">Used by practitioners</label>
                    </div>
                    <div>
                        <input type="checkbox" id="has-use-case" value="yes">
                        <label for="has-use-case">Use case available</label>
                    </div>
                    <div>
                        <input type="checkbox" id="show-archived" value="yes">
                        <label for="show-archived">Show archived</label>
                    </div>
                 </div>
                 </div>
                <div class="filter-content" id="data-source-filter"></div>
             </div>
             </div>
         </div>
         </div>

Revision as of 14:30, 19 December 2023

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