Widget: DCTList: Difference between revisions

From LINKS Community Center
Jump to: navigation, search
Eschmidt (talk | contribs)
No edit summary
Eschmidt (talk | contribs)
No edit summary
(228 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 98: Line 105:


         .filter-button-wrapper {
         .filter-button-wrapper {
             margin: 1.5em 0 1em 0;
             margin-bottom: 1em;
         }
         }


Line 109: 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 141: 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 177: Line 208:
         }
         }


         .bm-icon {
         .data-source-img,
             width: 1.5em;
        .func-img {
             height: 1.5em;
             width: 1.2rem;
             vertical-align: top;
             height: 1.2rem;
             margin-left: .25em;
             margin-right: .4rem;
             fill: #c3192b;
             margin-bottom: .4rem;
             filter: grayscale(1);
         }
         }


        .data-source-img,
         .func-img {
         .func-img {
             width: 2em;
             width: 1.5rem;
             height: 2em;
             height: 1.5rem;
            margin-right: .4em;
            margin-bottom: .4em;
         }
         }


         #dct-tabulator img.unselected {
         #dct-tabulator img.unselected {
            filter: grayscale(1);
             opacity: .15;
             opacity: .25;
         }
         }


Line 213: 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 230: 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 237: 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 256: 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 281: 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 DMO';
             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 330: 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 344: 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) {
            console.log(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 367: Line 448:
                 : true;
                 : true;
             const dmUseCheck = filterState.usedByDmo
             const dmUseCheck = filterState.usedByDmo
                 ? filterState.usedByDmo.includes(dct.usedByDmo)
                 ? filterState.usedByDmo === dct.usedByDmo
                 : true;
                 : true;
             return sourcesCheck && functionsCheck && businessModelCheck && dmUseCheck;
            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 381: 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 405: 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 452: 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?
             // 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
                    + (curr === FREE_KEY ? '<svg class="bm-icon"><use href="#freeware-icon"/></svg>' : '')
                    + (curr === FREE_PLAN_KEY ? '<svg class="bm-icon"><use href="#free-plan-icon"/></svg>' : '')
                     + '</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 492: 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>';
                             let out = '<a href="' + dct.url + '" translate="no">' + dct.name + '</a><br>';
                             if (dct.businessModel.includes('Freeware')) {
                             if (dct.archived.toLowerCase() === 'yes') {
                                 out += '<svg class="bm-icon" title="Freeware"><use href="#freeware-icon"/></svg>';
                                 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 available')) {
                             if (dct.businessModel.includes(FREE_PLAN_KEY)) {
                                 out += '<svg class="bm-icon" title="Free plan available"><use href="#free-plan-icon"/></svg>';
                                 out += '<small><span class="badge lcc-badge-green">' + FREE_PLAN_KEY + '</span></small> ';
                             }
                             }
                             if (dct.usedByDmo.toLowerCase() === 'yes') {
                             if (dct.usedByDmo.toLowerCase() === 'yes') {
                                 out += '<br><small><span class="badge badge-info">Used by DMOs</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 += '<br><small><span class="badge badge-info">Used by DMOs</span></small>';
                                 out += '<small><span class="badge lcc-badge-purple">Use case available</span></small> ';
                                out += ' <small><span class="badge badge-success">Use case</span></small>'
                             }
                             }
                             return out;
                             return out;
                         }
                         }
Line 515: 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 537: 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) => {
Line 559: Line 650:
                                 }
                                 }
                             });
                             });
                             return out;
                             return out + '</div>';
                         }
                         }
                     }
                     }
Line 568: 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 573: Line 698:
                 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. Showing all results.'; return; }
                 if (!(filter && filter.type)) { summary.textContent = 'No filter. Showing all results.'; return; }
Line 582: 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 597: 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 612: 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 629: 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.
             });
             });


             document.getElementById('data-source-filter').addEventListener('change', event => {
            // 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();
                 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 641: 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 });
            // 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 671: 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 679: Line 858:
     <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" style="display:none;">
     <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" style="display:none;">
         <defs>
         <defs>
            <symbol id="freeware-icon" viewBox="0 0 96 96">
                <clipPath id="fw">
                    <path d="M204 216h96v96h-96z" />
                </clipPath>
                <g clip-path="url(#fw)" transform="translate(-204 -216)">
                    <path
                        d="M237 255c-2.2 0-4-1.8-4-4 0-1.4.8-2.7 1.9-3.4 0 .7.1 1.5.1 2.4 0 1.1.9 2 2 2s2-.9 2-2c0-.9 0-1.7-.1-2.5 1.2.7 2.1 2 2.1 3.5 0 2.2-1.8 4-4 4Zm49.2 17.2-28-28c-.8-.8-1.8-1.2-2.8-1.2h-17.2c-1.1-3.5-3.2-5.3-6.8-6-1.3-.2-2.4-.4-3.5-.6-5.7-.8-6.9-1-6.9-7.4 0-1.1-.9-2-2-2s-2 .9-2 2c0 9.4 3.7 10.4 10.3 11.4 1 .2 2.1.3 3.4.6 1.3.3 2.7.5 3.5 2.6-3 1.2-5.1 4.1-5.1 7.5v18.3c0 1.1.4 2.1 1.2 2.8l28 28c.7.8 1.7 1.1 2.7 1.1 1 0 2-.4 2.8-1.2l22.3-22.3c1.6-1.5 1.6-4.1.1-5.6Z" />
                </g>
            </symbol>
            <symbol id="free-plan-icon" viewBox="0 0 96 96">
                <clipPath id="fp">
                    <path d="M592 312h96v96h-96z" />
                </clipPath>
                <g clip-path="url(#fp)" transform="translate(-592 -312)">
                    <path
                        d="m674.6 368.212-28.334-28.326a3.99 3.99 0 0 0-2.833-1.214h-18.617c-.254 0-.5.015-.752.038a6.974 6.974 0 0 0-5.064-4.143l-6.764-1.352a4.992 4.992 0 0 1-4-4.359l-.515-4.633a1 1 0 1 0-1.988.221l.515 4.633a6.984 6.984 0 0 0 5.592 6.1l6.763 1.352a4.99 4.99 0 0 1 3.472 2.632 8.118 8.118 0 0 0-5.359 7.605v18.512a3.676 3.676 0 0 0 1.214 2.833l28.33 28.327a4.03 4.03 0 0 0 5.767 0l22.573-22.56a4.094 4.094 0 0 0 0-5.666Zm-1.414 4.251-22.567 22.56a2.032 2.032 0 0 1-2.939 0L619.35 366.7l-.05-.05-.05-.044a1.722 1.722 0 0 1-.531-1.328v-18.513a6.116 6.116 0 0 1 3.939-5.692l.492 2.457c-.322.173-.618.39-.88.645A4 4 0 1 0 625.1 343h-.015l-.465-2.324c.065 0 .129-.009.194-.009h18.619c.538.009 1.05.236 1.418.628l28.33 28.327a2.1 2.1 0 0 1 0 2.836Zm-47.691-27.421a2.017 2.017 0 1 1-1.906.667l.164.821a1 1 0 0 0 1.179.781.999.999 0 0 0 .784-1.177Z" />
                </g>
            </symbol>
             <symbol id="chevron-down" viewBox="0 0 96 96">
             <symbol id="chevron-down" viewBox="0 0 96 96">
                 <clipPath id="chev">
                 <clipPath id="chev">
Line 704: 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 726: 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 757: 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 768: Line 947:
                 </div>
                 </div>
                 <div class="filter-wrapper">
                 <div class="filter-wrapper">
                     <h3>Functions <span>+</span></h3>
                     <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">
                             <button type="button" onclick="selectAll('#functions-filter')">Select all</button> |
                             <button type="button" onclick="selectAll('#functions-filter')">Select all</button> |
                             <button type="button" onclick="deselectAll('#functions-filter')">Deselect 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">
                     <h3>Supported Platforms</h3>
                     <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">
                             <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
                             <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
                             <button type="button" onclick="deselectAll('#data-source-filter')">Deselect 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">
                     <h3>Business Model</h3>
                     <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">
                             <button type="button" onclick="selectAll('#business-model-filter')">Select all</button> |
                             <button type="button" onclick="selectAll('#business-model-filter')">Select all</button> |
                             <button type="button" onclick="deselectAll('#business-model-filter')">Deselect all</button>
                             <button type="button" onclick="deselectAll('#business-model-filter')">Clear all</button>
                         </div>
                         </div>
                         <div class="filter-content" id="business-model-filter"></div>
                         <div class="filter-content" id="business-model-filter"></div>
                     </div>
                     </div>
                 </div>
                 </div>
                 <div class="filter-wrapper">
                 <!-- <div class="filter-wrapper">
                     <h3>Use in DM</h3>
                     <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">
                             <button type="button" onclick="selectAll('#dm-use-filter')">Select all</button> |
                             <button type="button" onclick="selectAll('#dm-use-filter')">Select all</button> |
                             <button type="button" onclick="deselectAll('#dm-use-filter')">Deselect all</button>
                             <button type="button" onclick="deselectAll('#dm-use-filter')">Clear all</button>
                         </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>

Revision as of 14:30, 19 December 2023

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