Difference between revisions of "Widget:DCTList"

From LINKS Community Center
Jump to: navigation, search
 
(205 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 33: 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 40: 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 54: 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 112: Line 118:
 
         }
 
         }
  
         .filter-wrapper .filter-container { display: none; }
+
         .filter-wrapper .filter-toggle {
         .filter-wrapper.open .filter-container { display: block; }
+
            cursor: pointer;
 +
         }
  
         .filter-wrapper h4 {
+
         .filter-wrapper .filter-container {
             display: flex;
+
             display: none;
            justify-content: space-between;
 
            align-items: flex-start;
 
 
         }
 
         }
  
         .filter-wrapper .filter-toggle svg {
+
         .filter-wrapper.open .filter-container {
             width: 1.5em;
+
             display: block;
            height: 1.5em;
 
            fill: var(--links-blue);
 
            cursor: pointer;
 
 
         }
 
         }
  
         .filter-wrapper.open .filter-toggle {
+
         .filter-wrapper.open .plus.icon::after {
             transform: rotate(180deg);
+
             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 162: Line 167:
 
             height: 2em;
 
             height: 2em;
 
             margin: 0 .5em 0 0;
 
             margin: 0 .5em 0 0;
 +
        }
 +
 +
        .subfunc-filter-block {
 +
            font-size: smaller;
 +
            padding-left: 1em;
 
         }
 
         }
  
Line 206: Line 216:
 
             filter: grayscale(1);
 
             filter: grayscale(1);
 
         }
 
         }
 +
 
         .func-img {
 
         .func-img {
 
             width: 1.5rem;
 
             width: 1.5rem;
Line 230: 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 { vertical-align: top; }
 
        #filter-summary table tr td:first-of-type { padding-right: 10px; }
 
       
 
 
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse table {
 
         #dct-tabulator .tabulator-row .tabulator-responsive-collapse table {
 
             font-size: smaller;
 
             font-size: smaller;
Line 247: 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 254: Line 280:
 
         * @property {string[]} dataSources
 
         * @property {string[]} dataSources
 
         * @property {string[]} businessModel
 
         * @property {string[]} businessModel
         * @property {string[]} functions
+
         * @property {FuncData} functions
 
         * @property {string} usedByDmo
 
         * @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 273: 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 298: 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 = 'Business model';
+
             const BUSINESS_PROP = 'Pricing';
 
             const DMO_PROP = 'Used by Practitioners';
 
             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
                          + '|?' + BUSINESS_PROP
+
                + '|?' + DATASRC_PROP
                          + '|?' + DMO_PROP
+
                + '|?' + BUSINESS_PROP
                          + '|?' + allFunctions.join('|?');
+
                + '|?' + 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 347: Line 391:
 
                 dct.businessModel = dctResult.printouts[BUSINESS_PROP].map(bModel => bModel.fulltext);
 
                 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] ? dctResult.printouts[DMO_PROP][0].fulltext : 'Unknown';
+
                 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 361: Line 408:
 
             });
 
             });
  
            console.log(dctList)
+
             return dctList;
 
 
             return { dcts: dctList, funcData: functionsData };
 
 
         }
 
         }
  
 
         /**
 
         /**
 
         * @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;
 
            const functionsCheck = filterState.functions
 
                ? dct.functions.some(func => filterState.functions.includes(func))
 
 
                 : true;
 
                 : true;
 
             const businessModelCheck = filterState.businessModel
 
             const businessModelCheck = filterState.businessModel
Line 383: Line 448:
 
                 : true;
 
                 : true;
 
             const dmUseCheck = filterState.usedByDmo
 
             const dmUseCheck = filterState.usedByDmo
                 ? filterState.usedByDmo.includes(dct.usedByDmo)
+
                 ? filterState.usedByDmo === dct.usedByDmo
 +
                : true;
 +
            const ucCheck = filterState.hasUC
 +
                ? filterState.hasUC === dct.hasUC
 
                 : true;
 
                 : true;
             return sourcesCheck && functionsCheck && businessModelCheck && dmUseCheck;
+
            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 397: Line 469:
 
             }
 
             }
  
            /** @type {Partial<Omit<DCT, 'usedByDmo'> & { usedByDmo: string[]>}} */
 
 
             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');
            filterState.functions = selectedFunctions.length === functionOptions.length ? undefined : selectedFunctions;
+
                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 sourceOptions = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]'));
+
             const selectedSources = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]:checked'))
            const selectedSources = sourceOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
+
                .map(checkbox => checkbox.value);
             filterState.dataSources = selectedSources.length === sourceOptions.length ? undefined : selectedSources;
+
             if (selectedSources.length > 0) filterState.dataSources = selectedSources;
  
             const bmOptions = Array.from(document.querySelectorAll('#business-model-filter input[type="checkbox"]'));
+
             const selectedBModels = Array.from(document.querySelectorAll('#business-model-filter input[type="checkbox"]:checked'))
            const selectedBModels = bmOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
+
                .map(checkbox => checkbox.value);
             filterState.businessModel = selectedBModels.length === bmOptions.length ? undefined : selectedBModels;
+
             if (selectedBModels.length > 0) filterState.businessModel = selectedBModels;
  
             const dmUseOptions = Array.from(document.querySelectorAll('#dm-use-filter input[type="checkbox"]'));
+
             if (document.getElementById('used-by-practitioners').checked) filterState.usedByDmo = 'yes';
             const selectedDmUseOptions = dmUseOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
+
             if (document.getElementById('has-use-case').checked) filterState.hasUC = 'yes';
             filterState.usedByDmo = selectedDmUseOptions.length === dmUseOptions.length ? undefined : selectedDmUseOptions;
+
             if (document.getElementById('show-archived').checked) filterState.showArchived = 'yes';
  
 
             table.setFilter(dctFilter, filterState);
 
             table.setFilter(dctFilter, filterState);
Line 421: 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];
 
  
             // The keys of this object must match function category names EXACTLY.
+
             // Set up functions filter
             // This also defines the sort order of function categories.
+
             let funcFilterHtml = '';
            // TODO: Link images to categories directly.
+
            Array.from(functionsData).forEach(([fnCat, fnInfo], index) => {
            const fnImages = {
+
                 const identifier = 'func-filter-' + escapeAttr(fnCat);
                'Search and Monitor': '/index.php/Special:FilePath/File:Func_search.svg',
+
                 funcFilterHtml +=
                 'Post and Schedule':  '/index.php/Special:FilePath/File:Func_post.svg',
+
                    `<div class="func-filter-block">
                 'Analysis':          '/index.php/Special:FilePath/File:Func_analysis.svg',
+
                        <div>
                'Metrics':            '/index.php/Special:FilePath/File:Func_metrics.svg',
+
                            <input type="checkbox" id="${identifier}" value="${fnCat}" class="func-cat">
                'Report':            '/index.php/Special:FilePath/File:Func_report.svg',
+
                            <label for="${identifier}" title="${fnInfo[DESC_KEY]}"><img src="${fnImages[fnCat]}"> ${fnCat}</label>
                'Collaboration':      '/index.php/Special:FilePath/File:Func_collaboration.svg',
+
                        </div>`;
                'Interoperability':  '/index.php/Special:FilePath/File:Func_interoperability.svg',
 
                'Meta':              '/index.php/Special:FilePath/File:Func_meta.svg',
 
            };
 
  
            // Set up filters.
+
                // add subfunctions
            const functionFilterHtml = Object.keys(fnImages).reduce((acc, funcName) => {
+
                funcFilterHtml += '<div class="subfunc-filter-block">';
                const identifier = escapeAttr(funcName);
+
                for (const func of fnInfo.functions) {
                return acc
+
                    const subfuncId = 'subfunc-filter-' + escapeAttr(func);
                     + '<div><input type="checkbox" checked id="func-filter-' + identifier
+
                     funcFilterHtml +=
                    + '" value="' + funcName + '">'
+
                        `<div>
                    + '<label for="func-filter-' + identifier
+
                            <input type="checkbox" id="${subfuncId}" value="${func}">
                    + '"><img src="' + fnImages[funcName] + '"> ' + funcName + '</label></div>'
+
                            <label for="${subfuncId}">${func}</label>
             }, '');
+
                        </div>`;
             document.getElementById('functions-filter').innerHTML = functionFilterHtml;
+
                }
 +
                funcFilterHtml += '</div>';
 +
 
 +
                funcFilterHtml += '</div>';
 +
             });
 +
             document.getElementById('functions-filter').innerHTML = funcFilterHtml;
  
 +
            // Set up sources filter
 
             const groupedSources = [];
 
             const groupedSources = [];
 
             const sourcesCopy = Array.from(dataSources);
 
             const sourcesCopy = Array.from(dataSources);
Line 468: 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>'
 
                 }, '');
 
                 }, '');
Line 475: Line 560:
  
 
             // TODO: Fetch from server?
 
             // TODO: Fetch from server?
             const FREE_KEY = 'Freeware', FREE_PLAN_KEY = 'Free plan available';
+
             const FREE_KEY = 'Free', FREE_PLAN_KEY = 'Free & Paid';
             const bModels = [FREE_KEY, FREE_PLAN_KEY, 'Subscription', 'Other'];
+
             const pricing = [FREE_KEY, FREE_PLAN_KEY, 'Paid'];
             let bModelFilterHtml = bModels.reduce((acc, curr) => {
+
             let pricingFilterHtml = pricing.reduce((acc, curr) => {
 
                 const identifier = escapeAttr(curr);
 
                 const identifier = escapeAttr(curr);
 
                 return acc
 
                 return acc
                     + '<div><input type="checkbox" checked id="bm-filter-' + identifier
+
                     + '<div><input type="checkbox" id="bm-filter-' + identifier
 
                     + '" value="' + curr + '">'
 
                     + '" value="' + curr + '">'
 
                     + '<label for="bm-filter-' + identifier + '">' + curr
 
                     + '<label for="bm-filter-' + identifier + '">' + curr
 
                     + '</label></div>'
 
                     + '</label></div>'
 
             }, '');
 
             }, '');
             document.getElementById('business-model-filter').innerHTML = bModelFilterHtml;
+
             document.getElementById('business-model-filter').innerHTML = pricingFilterHtml;
 
 
            const dmUse = ['Yes', 'Yes with Use Case', 'Unknown'];
 
            let dmUseFilterHtml = dmUse.reduce((acc, curr) => {
 
                const identifier = escapeAttr(curr);
 
                return acc
 
                    + '<div><input type="checkbox" checked id="dm-use-filter-' + identifier
 
                    + '" value="' + curr + '">'
 
                    + '<label for="dm-use-filter-' + identifier + '">' + curr + '</label></div>'
 
            }, '');
 
            document.getElementById('dm-use-filter').innerHTML = dmUseFilterHtml;
 
  
 
             // Set up table.
 
             // Set up table.
Line 506: 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();
                             let out = '<a href="' + dct.url + '">' + dct.name + '</a><br>';
+
                             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)) {
 
                             if (dct.businessModel.includes(FREE_KEY)) {
                                 out += '<small><span class="badge badge-success">Freeware</span></small> ';
+
                                 out += '<small><span class="badge lcc-badge-green">' + FREE_KEY + '</span></small> ';
 
                             }
 
                             }
 
                             if (dct.businessModel.includes(FREE_PLAN_KEY)) {
 
                             if (dct.businessModel.includes(FREE_PLAN_KEY)) {
                                 out += '<small><span class="badge badge-success">Free plan</span></small> ';
+
                                 out += '<small><span class="badge lcc-badge-green">' + FREE_PLAN_KEY + '</span></small> ';
 
                             }
 
                             }
 
                             if (dct.usedByDmo.toLowerCase() === 'yes') {
 
                             if (dct.usedByDmo.toLowerCase() === 'yes') {
                                 out += '<small><span class="badge badge-danger">Used by practitioners</span></small> ';
+
                                 out += '<small><span class="badge lcc-badge-red">Used by practitioners</span></small> ';
 
                             }
 
                             }
                             if (dct.usedByDmo.toLowerCase() === 'yes with use case') {
+
                             if (dct.hasUC.toLowerCase() === 'yes') {
                                 out += '<small><span class="badge badge-danger">Used by practitioners</span></small>';
+
                                 out += '<small><span class="badge lcc-badge-purple">Use case available</span></small> ';
                                out += ' <small><span class="badge badge-warning">Use case</span></small>'
 
 
                             }
 
                             }
 +
 
                             return out;
 
                             return out;
 
                         }
 
                         }
Line 529: 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 582: 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();
 
             });
 
             });
  
Line 587: Line 698:
 
                 const summary = document.getElementById('filter-summary');
 
                 const summary = document.getElementById('filter-summary');
 
                 const filter = filters[0];
 
                 const filter = filters[0];
                 const count = rows.length;
+
 
                 console.log('Amount: ' +  count)
+
                 // 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. Showing all results.'; return; }
 
                 if (!(filter && filter.type)) { summary.textContent = 'No filter. Showing all results.'; return; }
Line 598: Line 710:
 
                     !filter.type.dataSources &&
 
                     !filter.type.dataSources &&
 
                     !filter.type.businessModel &&
 
                     !filter.type.businessModel &&
                     !filter.type.usedByDmo
+
                     !filter.type.usedByDmo &&
 +
                    !filter.type.hasUC
 
                 ) { summary.textContent = 'No filter. Showing all results.'; }
 
                 ) { 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 613: Line 735:
 
                     }
 
                     }
 
                     if (filter.type.businessModel) {
 
                     if (filter.type.businessModel) {
                         summaryHtml += '<tr><td><strong>Business model</strong></td><td>'
+
                         summaryHtml += '<tr><td><strong>Pricing</strong></td><td>'
 
                             + (filter.type.businessModel.length > 0 ? filter.type.businessModel.join(', ') : 'none')
 
                             + (filter.type.businessModel.length > 0 ? filter.type.businessModel.join(', ') : 'none')
 
                             + '</td></tr>';
 
                             + '</td></tr>';
 
                     }
 
                     }
 
                     if (filter.type.usedByDmo) {
 
                     if (filter.type.usedByDmo) {
                         summaryHtml += '<tr><td><strong>Used by DMO</strong></td><td>'
+
                         summaryHtml += '<tr><td><strong>Used by practitioners</strong></td><td>Yes</td></tr>';
                            + (filter.type.usedByDmo.length > 0 ? filter.type.usedByDmo.join(', ') : 'none')
+
                    }
                            + '</td></tr>';
+
                    if (filter.type.hasUC) {
 +
                        summaryHtml += '<tr><td><strong>Use case available</strong></td><td>Yes</td></tr>';
 
                     }
 
                     }
 
                     summaryHtml += '</table>';
 
                     summaryHtml += '</table>';
Line 628: 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 645: 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.
 
             // Listen for changes in filter checkbox state.
             document.getElementById('data-source-filter').addEventListener('change', event => {
+
             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();
 
                 applyFilters();
 
             }, { passive: true });
 
             }, { passive: true });
             document.getElementById('functions-filter').addEventListener('change', event => {
+
             document.getElementById('data-source-filter').addEventListener('change', event => {
 
                 applyFilters();
 
                 applyFilters();
 
             }, { passive: true });
 
             }, { passive: true });
Line 658: Line 794:
 
                 applyFilters();
 
                 applyFilters();
 
             }, { passive: true });
 
             }, { passive: true });
             document.getElementById('dm-use-filter').addEventListener('change', event => {
+
             document.getElementById('bool-filters').addEventListener('change', event => {
 
                 applyFilters();
 
                 applyFilters();
 
             }, { passive: true });
 
             }, { passive: true });
Line 667: Line 803:
 
                 el.addEventListener('click', event => void wrapper.classList.toggle('open'));
 
                 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 694: 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 });
Line 707: Line 863:
 
                 </clipPath>
 
                 </clipPath>
 
                 <g clip-path="url(#chev)" transform="translate(-592 -312)">
 
                 <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" />
+
                     <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>
 
                 </g>
 
             </symbol>
 
             </symbol>
Line 729: 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. Showing all results.</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 760: 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 771: Line 947:
 
                 </div>
 
                 </div>
 
                 <div class="filter-wrapper">
 
                 <div class="filter-wrapper">
                     <h4>Functions <span class="filter-toggle"><svg><use href="#chevron-down"/></svg></span></h4>
+
                     <h4 class="filter-toggle">Functions <div class="plus icon"></div>
 +
                    </h4>
 
                     <div class="filter-container">
 
                     <div class="filter-container">
 
                         <div class="filter-button-wrapper">
 
                         <div class="filter-button-wrapper">
Line 777: Line 954:
 
                             <button type="button" onclick="deselectAll('#functions-filter')">Clear all</button>
 
                             <button type="button" onclick="deselectAll('#functions-filter')">Clear all</button>
 
                         </div>
 
                         </div>
                         <div class="filter-content" id="functions-filter"></div>
+
                         <div class="filter-content loose" id="functions-filter"></div>
 
                     </div>
 
                     </div>
 
                 </div>
 
                 </div>
 
                 <div class="filter-wrapper">
 
                 <div class="filter-wrapper">
                     <h4>Supported Platforms <span class="filter-toggle"><svg><use href="#chevron-down"/></svg></span></h4>
+
                     <h4 class="filter-toggle">Supported Platforms <div class="plus icon"></div>
 +
                    </h4>
 
                     <div class="filter-container">
 
                     <div class="filter-container">
 
                         <div class="filter-button-wrapper">
 
                         <div class="filter-button-wrapper">
Line 787: Line 965:
 
                             <button type="button" onclick="deselectAll('#data-source-filter')">Clear all</button>
 
                             <button type="button" onclick="deselectAll('#data-source-filter')">Clear all</button>
 
                         </div>
 
                         </div>
                         <div class="filter-content" id="data-source-filter"></div>
+
                         <div class="filter-content loose" id="data-source-filter"></div>
 
                     </div>
 
                     </div>
 
                 </div>
 
                 </div>
 
                 <div class="filter-wrapper">
 
                 <div class="filter-wrapper">
                     <h4>Business Model <span class="filter-toggle"><svg><use href="#chevron-down"/></svg></span></h4>
+
                     <h4 class="filter-toggle">Pricing <div class="plus icon"></div>
 +
                    </h4>
 
                     <div class="filter-container">
 
                     <div class="filter-container">
 
                         <div class="filter-button-wrapper">
 
                         <div class="filter-button-wrapper">
Line 800: Line 979:
 
                     </div>
 
                     </div>
 
                 </div>
 
                 </div>
                 <div class="filter-wrapper">
+
                 <!-- <div class="filter-wrapper">
                     <h4>Used by Practitioners <span class="filter-toggle"><svg><use href="#chevron-down"/></svg></span></h4>
+
                     <h4 class="filter-toggle">Used by Practitioners <div class="plus icon"></div>
 +
                    </h4>
 
                     <div class="filter-container">
 
                     <div class="filter-container">
 
                         <div class="filter-button-wrapper">
 
                         <div class="filter-button-wrapper">
Line 808: Line 988:
 
                         </div>
 
                         </div>
 
                         <div class="filter-content" id="dm-use-filter"></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>

Latest revision as of 15:30, 19 December 2023

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