Difference between revisions of "Widget:DCTList"

From LINKS Community Center
Jump to: navigation, search
(262 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="/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>
 
     <style>
Line 12: Line 12:
 
         #dct-list-wrapper h1,
 
         #dct-list-wrapper h1,
 
         #dct-list-wrapper h2,
 
         #dct-list-wrapper h2,
         #dct-list-wrapper h3 {
+
         #dct-list-wrapper h3,
 +
        #dct-list-wrapper h4 {
 
             font-family: 'Raleway';
 
             font-family: 'Raleway';
 
             font-weight: 300;
 
             font-weight: 300;
Line 32: Line 33:
 
             color: var(--links-blue);
 
             color: var(--links-blue);
 
         }
 
         }
 +
 
         #dct-list-wrapper h1 svg {
 
         #dct-list-wrapper h1 svg {
 
             height: 2.5em;
 
             height: 2.5em;
Line 39: Line 41:
 
         }
 
         }
  
         #filter-bar { position: relative; margin: 1em 0 2em 0; }
+
         #filter-bar {
 +
            position: relative;
 +
            margin: 1em 0 2em 0;
 +
        }
 +
 
 
         #dct-filters {
 
         #dct-filters {
 
             position: absolute;
 
             position: absolute;
Line 53: Line 59:
 
             transition: all 400ms ease-in-out;
 
             transition: all 400ms ease-in-out;
 
         }
 
         }
 +
 
         #dct-filters.open {
 
         #dct-filters.open {
 
             clip-path: inset(0 0 -50px -50px);
 
             clip-path: inset(0 0 -50px -50px);
Line 85: Line 92:
 
             font-variant: small-caps;
 
             font-variant: small-caps;
 
             display: inline-block;
 
             display: inline-block;
 +
            transition: all 200ms ease-in-out;
 +
        }
 +
 +
        .large-button:hover {
 +
            background-color: var(--links-blue);
 +
            color: #fff;
 
         }
 
         }
  
Line 92: Line 105:
  
 
         .filter-button-wrapper {
 
         .filter-button-wrapper {
             margin: 1.5em 0 1em 0;
+
             margin-bottom: 1em;
 
         }
 
         }
  
Line 103: Line 116:
 
             text-decoration: underline;
 
             text-decoration: underline;
 
             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;
 
         }
 
         }
  
Line 125: Line 157:
 
         .filter-group-start {
 
         .filter-group-start {
 
             grid-column-start: 1;
 
             grid-column-start: 1;
 +
        }
 +
 +
        .filter-content input[type="checkbox"] {
 +
            margin-right: .5em;
 
         }
 
         }
  
Line 130: Line 166:
 
             width: 2em;
 
             width: 2em;
 
             height: 2em;
 
             height: 2em;
             margin: 0 .5em;
+
             margin: 0 .5em 0 0;
 +
        }
 +
 
 +
        .subfunc-filter-block {
 +
            font-size: smaller;
 +
            padding-left: 1em;
 
         }
 
         }
  
Line 169: Line 210:
 
         .data-source-img,
 
         .data-source-img,
 
         .func-img {
 
         .func-img {
             width: 2em;
+
             width: 1.2rem;
             height: 2em;
+
             height: 1.2rem;
             margin-right: .4em;
+
             margin-right: .4rem;
             margin-bottom: .4em;
+
             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;
 
 
         }
 
         }
  
Line 195: Line 241:
 
         }
 
         }
  
         #filter-summary { margin-right: 1em; }
+
         #filter-summary {
 +
            margin-right: 1em;
 +
        }
 +
 
 +
        #filter-summary table tr td {
 +
            vertical-align: top;
 +
        }
 +
 
 +
        #filter-summary table tr td:first-of-type {
 +
            padding-right: 10px;
 +
        }
  
        #filter-summary table tr td:first-of-type { padding-right: 10px; vertical-align: top; }
 
       
 
 
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse table {
 
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse table {
 
             font-size: smaller;
 
             font-size: smaller;
Line 211: Line 265:
 
     <script>
 
     <script>
 
         'use strict';
 
         'use strict';
 +
 +
        const FUNC_KEY = 'functions';
 +
        const DESC_KEY = 'description';
 +
 +
        /** @typedef {Map<string, { [DESC_KEY]: string, [FUNC_KEY]: string[] }>} FuncData */
 +
 +
        /** @type FuncData */
 +
        const functionsData = new Map();    // Data on functions and function categories
  
 
         /**
 
         /**
Line 217: Line 279:
 
         * @property {string} url
 
         * @property {string} url
 
         * @property {string[]} dataSources
 
         * @property {string[]} dataSources
         * @property {string[]} functions
+
         * @property {string[]} businessModel
 +
        * @property {FuncData} functions
 +
        * @property {string} usedByDmo
 +
        * @property {string} archived
 +
        * @property {string} hasUC
 
         * @property {string} logo
 
         * @property {string} logo
 
         */
 
         */
  
         let table;
+
         let table; // Tabulator instance
  
 
         // This defines how sources are grouped in the filter.
 
         // This defines how sources are grouped in the filter.
Line 235: Line 301:
 
             }
 
             }
 
         ];
 
         ];
 +
 +
        // 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
 
         // Helpers
Line 260: Line 338:
 
         // Fetches DCTs and their functions.
 
         // Fetches DCTs and their functions.
 
         async function getDcts() {
 
         async function getDcts() {
            const FUNC_KEY = 'functions';
 
            const DESC_KEY = 'description';
 
 
            /** @type Map<string, { [DESC_KEY]: string, [FUNC_KEY]: string[] } */
 
            const functionsData = new Map();
 
 
 
             const functionsQuery = '[[Category:Function_category]]'
 
             const functionsQuery = '[[Category:Function_category]]'
                                + '|?-Subproperty_of=' + FUNC_KEY
+
                + '|?-Subproperty_of=' + FUNC_KEY
                                + '|?Has_description=' + DESC_KEY;
+
                + '|?Has_description=' + DESC_KEY;
 
             const functionsQueryResponse = await fetch(getQueryUrl(functionsQuery)).then(response => response.json());
 
             const functionsQueryResponse = await fetch(getQueryUrl(functionsQuery)).then(response => response.json());
  
             Object.keys(functionsQueryResponse.query.results).forEach(key => {
+
             const sortOrder = Object.keys(fnImages);
                 const functionCategory = functionsQueryResponse.query.results[key];
+
            Object.entries(functionsQueryResponse.query.results)
                functionsData.set(
+
                .map(([key, value]) => ([removePrefix(key), value]))
                    removePrefix(functionCategory.fulltext),
+
                 .sort(([keyA,], [keyB,]) => sortOrder.indexOf(keyA) - sortOrder.indexOf(keyB))
                    {
+
                .forEach(([categoryName, results]) => {
                        [DESC_KEY]: functionCategory.printouts[DESC_KEY][0],
+
                    functionsData.set(
                        [FUNC_KEY]: functionCategory.printouts[FUNC_KEY].map(func => removePrefix(func.fulltext))
+
                        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()
 +
                        }
 +
                    );
 +
                });
  
            // TODO: Remove properties that are not relevant for the filter? (e.g. 'Supported content types')
 
 
             const allFunctions = Array.from(functionsData.values(), entry => entry[FUNC_KEY]).flat();
 
             const allFunctions = Array.from(functionsData.values(), entry => entry[FUNC_KEY]).flat();
  
 
             const DATASRC_PROP = 'Data Sources';
 
             const DATASRC_PROP = 'Data Sources';
 
             const IMG_PROP = 'Image';
 
             const IMG_PROP = 'Image';
 +
            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]]'
 
             const dctQuery = '[[Category:Disaster Community Technology]]'
                          + '[[Is Archived::No]]'
+
                // + '[[Is Archived::No]]'
                          + '|limit=500'
+
                + '|limit=500'
                          + '|?' + IMG_PROP
+
                + '|?' + ARCHIVED
                          + '|?' + DATASRC_PROP
+
                + '|?' + IMG_PROP
                          + '|?' + allFunctions.join('|?');
+
                + '|?' + DATASRC_PROP
 +
                + '|?' + BUSINESS_PROP
 +
                + '|?' + DMO_PROP
 +
                + '|?' + UC_PROP
 +
                + '|?' + allFunctions.join('|?');
  
 
             const dctResponse = await fetch(getQueryUrl(dctQuery)).then(response => response.json());
 
             const dctResponse = await fetch(getQueryUrl(dctQuery)).then(response => response.json());
 +
 
             const dctList = Object.keys(dctResponse.query.results).map(dctName => {
 
             const dctList = Object.keys(dctResponse.query.results).map(dctName => {
 
                 const dctResult = dctResponse.query.results[dctName];
 
                 const dctResult = dctResponse.query.results[dctName];
Line 303: Line 389:
 
                 dct.url = dctResult.fullurl;
 
                 dct.url = dctResult.fullurl;
 
                 dct.dataSources = dctResult.printouts[DATASRC_PROP].map(source => source.fulltext).sort();
 
                 dct.dataSources = dctResult.printouts[DATASRC_PROP].map(source => source.fulltext).sort();
 +
                dct.businessModel = dctResult.printouts[BUSINESS_PROP].map(bModel => bModel.fulltext);
 
                 dct.logo = dctResult.printouts[IMG_PROP][0] ? getFilePath(dctResult.printouts[IMG_PROP][0].fulltext) : undefined;
 
                 dct.logo = dctResult.printouts[IMG_PROP][0] ? getFilePath(dctResult.printouts[IMG_PROP][0].fulltext) : undefined;
 +
                dct.usedByDmo = dctResult.printouts[DMO_PROP][0] === 't' ? 'yes' : 'no';    // not quite, but we only care about yes
 +
                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
 +
 +
                dct.functions = new Map();
 +
                functionsData.forEach((categoryData, funcCategory) => {
 +
                    const confirmedFunctions = categoryData[FUNC_KEY]
 +
                        .filter(func => dctResult.printouts[func][0] && dctResult.printouts[func][0].fulltext.toLowerCase() === 'yes');
  
                dct.functions = [];
+
                     if (confirmedFunctions.length > 0) {
                functionsData.forEach((value, key) => {
+
                         dct.functions.set(funcCategory, { [DESC_KEY]: categoryData[DESC_KEY], [FUNC_KEY]: confirmedFunctions });
                     if (value[FUNC_KEY].some(
 
                        func => dctResult.printouts[func][0] && dctResult.printouts[func][0].fulltext.toLowerCase() === 'yes'
 
                    )) {
 
                         dct.functions.push(key);
 
 
                     }
 
                     }
 
                 });
 
                 });
Line 317: Line 408:
 
             });
 
             });
  
             return { dcts: dctList, funcData: functionsData };
+
             return dctList;
 
         }
 
         }
  
 
         /**
 
         /**
 
         * @param {DCT} dct
 
         * @param {DCT} dct
        * @param {Partial<DCT>} filterState
 
 
         */
 
         */
 
         function dctFilter(dct, filterState) {
 
         function dctFilter(dct, filterState) {
 
             // If filtering property is empty, don't apply the filter (set the check to true).
 
             // 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.
 
             // Passing an empty object (as with applyFilters(true)) should result in an unfiltered table.
 +
 +
            let functionsCheck = true;  // automatically pass functions check if the filter missing
 +
            if (filterState.functions) {
 +
                // If filterState has a category but subfunctions array is empty, we only care about the category.
 +
                const emptyCategories = [], nonemptyCategories = [];
 +
                Array.from(filterState.functions).forEach(([key, subs]) => {
 +
                    if (subs.length > 0) { nonemptyCategories.push(key); } else { emptyCategories.push(key); }
 +
                });
 +
 +
                // Empty categories should only be checked by their name
 +
                const checkEmpty = emptyCategories.every(cat => dct.functions.has(cat));
 +
 +
                // 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;
 +
                // TODO: Empty should no longer exist, once every category is fully listed in the filter.
 +
                // TODO: The filtering of functions therefore should be reduced to nonempty only.
 +
            }
 +
 
             const sourcesCheck = filterState.dataSources
 
             const sourcesCheck = filterState.dataSources
                 ? dct.dataSources.some(source => filterState.dataSources.includes(source))
+
                 ? filterState.dataSources.every(source => dct.dataSources.includes(source))
 
                 : true;
 
                 : true;
             const functionsCheck = filterState.functions
+
             const businessModelCheck = filterState.businessModel
                 ? dct.functions.some(func => filterState.functions.includes(func))
+
                 ? dct.businessModel.some(bm => filterState.businessModel.includes(bm))
 
                 : true;
 
                 : true;
             return sourcesCheck && functionsCheck;
+
            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';
 +
 
 +
             return sourcesCheck && functionsCheck && businessModelCheck && dmUseCheck && ucCheck && archivedCheck;
 
         }
 
         }
  
 
         function applyFilters(clear) {
 
         function applyFilters(clear) {
             if (!table) return;
+
             if (!table) return; // prevent filtering before the table is ready
  
 
             // If clear=true, pass empty object to the filter to disable it.
 
             // If clear=true, pass empty object to the filter to disable it.
Line 345: Line 469:
 
             }
 
             }
  
            /** @type {Partial<DCT>} */
 
 
             const filterState = {};
 
             const filterState = {};
  
             const functionOptions = Array.from(document.querySelectorAll('#functions-filter input[type="checkbox"]'));
+
             if (document.querySelectorAll('#functions-filter input[type="checkbox"]:checked').length > 0) {
            const selectedFunctions = functionOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
+
                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;
 +
            }
  
             // If all checkboxes are checked, disable the filter property.
+
             const selectedSources = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]:checked'))
             filterState.functions = selectedFunctions.length === functionOptions.length ? undefined : selectedFunctions;
+
                .map(checkbox => checkbox.value);
 +
             if (selectedSources.length > 0) filterState.dataSources = selectedSources;
  
             const sourceOptions = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]'));
+
             const selectedBModels = Array.from(document.querySelectorAll('#business-model-filter input[type="checkbox"]:checked'))
            const selectedSources = sourceOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
+
                .map(checkbox => checkbox.value);
 +
            if (selectedBModels.length > 0) filterState.businessModel = selectedBModels;
  
             // If all checkboxes are checked, disable the filter property.
+
             if (document.getElementById('used-by-practitioners').checked) filterState.usedByDmo = 'yes';
             filterState.dataSources = selectedSources.length === sourceOptions.length ? undefined : selectedSources;
+
             if (document.getElementById('has-use-case').checked) filterState.hasUC = 'yes';
 +
            if (document.getElementById('show-archived').checked) filterState.showArchived = 'yes';
  
 
             table.setFilter(dctFilter, filterState);
 
             table.setFilter(dctFilter, filterState);
Line 365: Line 504:
 
         // Load data and build page.
 
         // Load data and build page.
 
         Promise.all([getSources(), getDcts()]).then(data => {
 
         Promise.all([getSources(), getDcts()]).then(data => {
             const dataSources = data[0];
+
             const [dataSources, dcts] = data;
            const { dcts, funcData } = data[1];
+
 
 +
            // 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>`;
  
            // The keys of this object must match function category names EXACTLY.
+
                // add subfunctions
            // This also defines the sort order of function categories.
+
                funcFilterHtml += '<div class="subfunc-filter-block">';
            // TODO: Link images to categories directly.
+
                for (const func of fnInfo.functions) {
            const fnImages = {
+
                    const subfuncId = 'subfunc-filter-' + escapeAttr(func);
                'Search and Monitor': '/index.php/Special:FilePath/File:Func_search.svg',
+
                    funcFilterHtml +=
                'Post and Schedule':  '/index.php/Special:FilePath/File:Func_post.svg',
+
                        `<div>
                'Analysis':          '/index.php/Special:FilePath/File:Func_analysis.svg',
+
                            <input type="checkbox" id="${subfuncId}" value="${func}">
                'Metrics':            '/index.php/Special:FilePath/File:Func_metrics.svg',
+
                            <label for="${subfuncId}">${func}</label>
                'Report':            '/index.php/Special:FilePath/File:Func_report.svg',
+
                        </div>`;
                'Collaboration':      '/index.php/Special:FilePath/File:Func_collaboration.svg',
+
                 }
                 'Interoperability':  '/index.php/Special:FilePath/File:Func_interoperability.svg',
+
                 funcFilterHtml += '</div>';
                 'Meta':              '/index.php/Special:FilePath/File:Func_meta.svg',
 
            };
 
  
            // Set up filters.
+
                 funcFilterHtml += '</div>';
            const functionFilterHtml = Object.keys(fnImages).reduce((acc, funcName) => {
+
             });
                const identifier = escapeAttr(funcName);
+
             document.getElementById('functions-filter').innerHTML = funcFilterHtml;
                 return acc
 
                    + '<div><input type="checkbox" checked id="func-filter-' + identifier
 
                    + '" value="' + funcName + '">'
 
                    + '<label for="func-filter-' + identifier
 
                    + '"><img src="' + fnImages[funcName] + '"> ' + funcName + '</label></div>'
 
             }, '');
 
             document.getElementById('functions-filter').innerHTML = functionFilterHtml;
 
  
 +
            // Set up sources filter
 
             const groupedSources = [];
 
             const groupedSources = [];
 
             const sourcesCopy = Array.from(dataSources);
 
             const sourcesCopy = Array.from(dataSources);
Line 412: Line 553:
 
                     return acc +
 
                     return acc +
 
                         '<div ' + (idx === 0 ? ' class="filter-group-start">' : '>') +
 
                         '<div ' + (idx === 0 ? ' class="filter-group-start">' : '>') +
                         '<input type="checkbox" id="filter-' + identifier + '" value="' + curr.name + '" checked>' +
+
                         '<input type="checkbox" id="filter-' + identifier + '" value="' + curr.name + '">' +
 
                         '<label for="filter-' + identifier + '"><img src="' + curr.image + '"> ' + curr.name + '</label></div>'
 
                         '<label for="filter-' + identifier + '"><img src="' + curr.image + '"> ' + curr.name + '</label></div>'
 
                 }, '');
 
                 }, '');
             })
+
             });
 
             document.getElementById('data-source-filter').innerHTML = dataSourceFilterHtml;
 
             document.getElementById('data-source-filter').innerHTML = dataSourceFilterHtml;
 +
 +
            // TODO: Fetch from server?
 +
            const FREE_KEY = 'Free', FREE_PLAN_KEY = 'Free & Paid';
 +
            const pricing = [FREE_KEY, FREE_PLAN_KEY, 'Paid'];
 +
            let pricingFilterHtml = pricing.reduce((acc, curr) => {
 +
                const identifier = escapeAttr(curr);
 +
                return acc
 +
                    + '<div><input type="checkbox" id="bm-filter-' + identifier
 +
                    + '" value="' + curr + '">'
 +
                    + '<label for="bm-filter-' + identifier + '">' + curr
 +
                    + '</label></div>'
 +
            }, '');
 +
            document.getElementById('business-model-filter').innerHTML = pricingFilterHtml;
  
 
             // Set up table.
 
             // Set up table.
Line 427: Line 581:
 
                         field: 'name',
 
                         field: 'name',
 
                         minWidth: 300, // required for responsiveness when using fitColumns
 
                         minWidth: 300, // required for responsiveness when using fitColumns
 +
                        widthGrow: 2,
 
                         formatter: function (cell) {
 
                         formatter: function (cell) {
 
                             /** @type {DCT} */
 
                             /** @type {DCT} */
 
                             const dct = cell.getData();
 
                             const dct = cell.getData();
                             return '<a href="' + dct.url + '">' + dct.name + '</a>';
+
                             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;
 
                         }
 
                         }
 
                     },
 
                     },
Line 436: Line 608:
 
                         title: 'Functions',
 
                         title: 'Functions',
 
                         field: 'functions',
 
                         field: 'functions',
                         minWidth: 300, // required for responsiveness when using fitColumns
+
                         minWidth: 200, // required for responsiveness when using fitColumns
 
                         cssClass: 'functions-cell',
 
                         cssClass: 'functions-cell',
 
                         formatter: function (cell) {
 
                         formatter: function (cell) {
                             const order = Object.keys(fnImages);
+
                             return Array.from(cell.getValue().keys())
                            const output = cell.getValue()
+
                                 .map(fn => `<img class="func-img"  
                                .sort((a, b) => order.indexOf(a) - order.indexOf(b))
+
                                    src="${fnImages[fn]}"
                                 .map(func => '<img class="func-img" src="' + fnImages[func] +
+
                                     data-value="${fn}"
                                     '" data-value="' + func +
+
                                     alt="${fn}"
                                     '" alt="' + func +
+
                                     title="${fn}\n\n${functionsData.get(fn)[DESC_KEY]}">`
                                     '" title="' + func +
+
                                )
                                    '">')
 
 
                                 .join('');
 
                                 .join('');
                            return output;
 
 
                         }
 
                         }
 
                     },
 
                     },
Line 458: Line 628:
 
                         formatter: function (cell) {
 
                         formatter: function (cell) {
 
                             const val = cell.getValue();
 
                             const val = cell.getValue();
                             let out = '';
+
                             let out = '<div>';
  
 
                             groupedSources.forEach((group, gIndex) => {
 
                             groupedSources.forEach((group, gIndex) => {
                                 if (gIndex === groupedSources.length - 1) { out += '<div class="sources-collapse">'; }
+
                                 // if (gIndex === groupedSources.length - 1) { out += '<div class="sources-collapse">'; }
 
                                 out += group.sources.reduce((prev, curr) => {
 
                                 out += group.sources.reduce((prev, curr) => {
 
                                     const idx = val.findIndex(src => src === curr.name);
 
                                     const idx = val.findIndex(src => src === curr.name);
Line 477: Line 647:
 
                                 if (gIndex === groupedSources.length - 1) {
 
                                 if (gIndex === groupedSources.length - 1) {
 
                                     // out += '<span class="sources-collapse-toggle"></span>';
 
                                     // out += '<span class="sources-collapse-toggle"></span>';
                                     out += '</div>';
+
                                     // out += '</div>';
 
                                 }
 
                                 }
 
                             });
 
                             });
                             return out;
+
                             return out + '</div>';
 
                         }
 
                         }
 
                     }
 
                     }
Line 489: Line 659:
 
                 tabulator.setData(dcts);
 
                 tabulator.setData(dcts);
 
                 table = tabulator;
 
                 table = tabulator;
 +
 +
                // Set up the table if parameter was passed.
 +
                const params = new URLSearchParams(window.location.search);
 +
                const encoded = params.get('do');
 +
 +
                if (encoded) {
 +
                    const actions = JSON.parse(decodeURIComponent(atob(encoded)));
 +
 +
                    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.)
 +
                    // ...
 +
                }
 +
 +
                applyFilters();
 
             });
 
             });
  
 
             tabulator.on('dataFiltered', (filters, rows) => {
 
             tabulator.on('dataFiltered', (filters, rows) => {
                console.log(filters)
 
 
                 const summary = document.getElementById('filter-summary');
 
                 const summary = document.getElementById('filter-summary');
 
                 const filter = filters[0];
 
                 const filter = filters[0];
                  
+
 
 +
                 // Set result counter
 +
                document.getElementById('result-count').textContent = rows.length;
 +
 
 
                 // Exit if filter object/type doesn't exist (happens after Tabulator's own filter reset).
 
                 // Exit if filter object/type doesn't exist (happens after Tabulator's own filter reset).
                 if (!(filter && filter.type)) { summary.textContent = 'No filter'; return; }
+
                 if (!(filter && filter.type)) { summary.textContent = 'No filter. Showing all results.'; return; }
  
 
                 // Update filter text.
 
                 // Update filter text.
 
                 if (
 
                 if (
 
                     !filter.type.functions &&
 
                     !filter.type.functions &&
                     !filter.type.dataSources
+
                     !filter.type.dataSources &&
                 ) { summary.textContent = 'No filter'; }
+
                    !filter.type.businessModel &&
 +
                    !filter.type.usedByDmo &&
 +
                    !filter.type.hasUC
 +
                 ) { summary.textContent = 'No filter. Showing all results.'; }
 
                 else {
 
                 else {
 
                     let summaryHtml = '<table>';
 
                     let summaryHtml = '<table>';
 
                     if (filter.type.functions) {
 
                     if (filter.type.functions) {
                         summaryHtml += '<tr><td><strong>Functions</strong></td><td>'
+
                         summaryHtml += '<tr><td><strong>Functions</strong></td><td>';
                             + (filter.type.functions.length > 0 ? filter.type.functions.join(', ') : 'none')
+
                        if (filter.type.functions.size === 0) {
                            + '</td></tr>';
+
                             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) {
 
                     if (filter.type.dataSources) {
Line 515: Line 733:
 
                             + (filter.type.dataSources.length > 0 ? filter.type.dataSources.join(', ') : 'none')
 
                             + (filter.type.dataSources.length > 0 ? filter.type.dataSources.join(', ') : 'none')
 
                             + '</td></tr>';
 
                             + '</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>';
 
                     summaryHtml += '</table>';
Line 522: Line 751:
 
                 const markImages = () => {
 
                 const markImages = () => {
 
                     const selectedSources = filter.type.dataSources;
 
                     const selectedSources = filter.type.dataSources;
                     const selectedFunctions = filter.type.functions;
+
                     const selectedFunctions = filter.type.functions ? Array.from(filter.type.functions.keys()) : undefined;
  
 
                     // Mark data source images
 
                     // Mark data source images
Line 539: Line 768:
 
                     tabulator.off('renderComplete', markImages);
 
                     tabulator.off('renderComplete', markImages);
 
                 }
 
                 }
                 tabulator.on('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"]');
 +
 +
                if (event.target === category) {
 +
                    // Selecting/deselecting the category checks/unchecks all subfunctions.
 +
                    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;
 +
                }
 +
 +
                applyFilters();
 +
            }, { passive: true });
 
             document.getElementById('data-source-filter').addEventListener('change', event => {
 
             document.getElementById('data-source-filter').addEventListener('change', event => {
 
                 applyFilters();
 
                 applyFilters();
 
             }, { passive: true });
 
             }, { passive: true });
             document.getElementById('functions-filter').addEventListener('change', event => {
+
             document.getElementById('business-model-filter').addEventListener('change', event => {
 +
                applyFilters();
 +
            }, { passive: true });
 +
            document.getElementById('bool-filters').addEventListener('change', event => {
 
                 applyFilters();
 
                 applyFilters();
 
             }, { passive: true });
 
             }, { passive: true });
 +
 +
            // Listen for clicks on filter toggles
 +
            document.querySelectorAll('.filter-wrapper .filter-toggle').forEach(el => {
 +
                const wrapper = el.closest('.filter-wrapper');
 +
                el.addEventListener('click', event => void wrapper.classList.toggle('open'));
 +
            });
 +
 +
            // Fix bug where the table is truncated to zero height despite having visible rows.
 +
            tabulator.on('renderComplete', function () {
 +
                // TODO: Check the bugfix for a possible infinite event loop.
 +
                // This will help detect it, in case it happens.
 +
                // console.log('Table height bugfix: render complete.');
 +
 +
                try {
 +
                    const holderHeight = tabulator.rowManager.element.offsetHeight;
 +
                    const tableHeight = tabulator.rowManager.tableElement.offsetHeight;
 +
                    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
 +
 
         });
 
         });
  
Line 575: Line 850:
 
                 filterPane.classList.contains('open') &&
 
                 filterPane.classList.contains('open') &&
 
                 !filterPane.contains(event.target) &&
 
                 !filterPane.contains(event.target) &&
                 event.target !== document.querySelector('#filter-bar .large-button')
+
                 event.target !== document.querySelector('#filter-bar .large-button.open-filters')
 
             ) { filterPane.classList.remove('open'); }
 
             ) { filterPane.classList.remove('open'); }
 
         }, { passive: true });
 
         }, { 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">
Line 596: Line 886:
 
                 </g>
 
                 </g>
 
             </svg>
 
             </svg>
             <span>Technologies</span>
+
             <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>
  
            <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="filter-bar">
 
         <div id="filter-bar">
 
             <div style="display: flex; justify-content: space-between;">
 
             <div style="display: flex; justify-content: space-between;">
 
                 <div style="flex: 1 1;">
 
                 <div style="flex: 1 1;">
                     <h2 style="margin-bottom: 1rem;">Applied Filters</h2>
+
                     <h2 style="margin-bottom: 1rem;">Selected Filters</h2>
                     <div id="filter-summary">No filter</div>
+
                     <div id="filter-summary">No filter. Showing all results.</div>
 
                 </div>
 
                 </div>
                 <div style="flex: 0 0;">
+
                 <div>
                     <button class="large-button" type="button" onclick="toggleFilter()">Filters</button>
+
                     <button class="large-button open-filters" type="button" onclick="toggleFilter()">Open
 +
                        Filters</button>
 
                 </div>
 
                 </div>
 
             </div>
 
             </div>
       
+
 
 +
            <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 2.5rem;">
 +
                <h2 style="margin-bottom: 0;">Results: <span id="result-count"></span></h2>
 +
                <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>
 +
 
 
             <div id="dct-filters">
 
             <div id="dct-filters">
 
                 <h2 style="display: flex; justify-content: space-between;">
 
                 <h2 style="display: flex; justify-content: space-between;">
 
                     <div>
 
                     <div>
                         <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" viewBox="0 0 96 96">
+
                         <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden"
 +
                            viewBox="0 0 96 96">
 
                             <defs>
 
                             <defs>
 
                                 <clipPath id="b">
 
                                 <clipPath id="b">
Line 627: Line 936:
 
                             <g clip-path="url(#b)" transform="translate(-592 -312)">
 
                             <g clip-path="url(#b)" transform="translate(-592 -312)">
 
                                 <path
 
                                 <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" />
+
                                    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>
 
                             </g>
 
                         </svg>
 
                         </svg>
Line 638: Line 947:
 
                 </div>
 
                 </div>
 
                 <div class="filter-wrapper">
 
                 <div class="filter-wrapper">
                     <h3>Functions</h3>
+
                     <h4 class="filter-toggle">Functions <div class="plus icon"></div>
                     <div class="filter-button-wrapper">
+
                     </h4>
                        <button type="button" onclick="selectAll('#functions-filter')">Select all</button> |
+
                    <div class="filter-container">
                         <button type="button" onclick="deselectAll('#functions-filter')">Deselect all</button>
+
                        <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 class="filter-content" id="functions-filter"></div>
 
 
                 </div>
 
                 </div>
 
                 <div class="filter-wrapper">
 
                 <div class="filter-wrapper">
                     <h3>Supported Platforms</h3>
+
                     <h4 class="filter-toggle">Pricing <div class="plus icon"></div>
                     <div class="filter-button-wrapper">
+
                    </h4>
                         <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
+
                     <div class="filter-container">
                        <button type="button" onclick="deselectAll('#data-source-filter')">Deselect all</button>
+
                        <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>
 +
                <!-- <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 class="filter-content" id="data-source-filter"></div>
 
 
                 </div>
 
                 </div>
 
             </div>
 
             </div>

Revision as of 15:30, 19 December 2023

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