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
(146 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: start;
        }
        .filter-content.loose {
            gap: .5em .5em;
         }
         }


Line 211: Line 216:
             filter: grayscale(1);
             filter: grayscale(1);
         }
         }
         .func-img {
         .func-img {
             width: 1.5rem;
             width: 1.5rem;
Line 235: 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 269: Line 282:
         * @property {FuncData} functions
         * @property {FuncData} functions
         * @property {string} usedByDmo
         * @property {string} usedByDmo
        * @property {string} archived
        * @property {string} hasUC
         * @property {string} logo
         * @property {string} logo
         */
         */
Line 289: Line 304:
         // This object defines the sort order of function categories and matches them to icons.
         // This object defines the sort order of function categories and matches them to icons.
         const fnImages = {
         const fnImages = {
             'Search and Monitor':   '/index.php/Special:FilePath/File:Func_search.svg',
             'Search and Monitor': '/index.php/Special:FilePath/File:Func_search.svg',
             'Post and Schedule':   '/index.php/Special:FilePath/File:Func_post.svg',
             'Post and Schedule': '/index.php/Special:FilePath/File:Func_post.svg',
             'Analysis':             '/index.php/Special:FilePath/File:Func_analysis.svg',
             'Analysis': '/index.php/Special:FilePath/File:Func_analysis.svg',
             'Metrics':             '/index.php/Special:FilePath/File:Func_metrics.svg',
             'Metrics': '/index.php/Special:FilePath/File:Func_metrics.svg',
             'Report':               '/index.php/Special:FilePath/File:Func_report.svg',
             'Report': '/index.php/Special:FilePath/File:Func_report.svg',
             'Collaboration':       '/index.php/Special:FilePath/File:Func_collaboration.svg',
             'Collaboration': '/index.php/Special:FilePath/File:Func_collaboration.svg',
             'Interoperability':     '/index.php/Special:FilePath/File:Func_interoperability.svg',
             'Interoperability': '/index.php/Special:FilePath/File:Func_interoperability.svg',
             'Meta':                 '/index.php/Special:FilePath/File:Func_meta.svg',
             'Meta': '/index.php/Special:FilePath/File:Func_meta.svg',
         };
         };


Line 324: Line 339:
         async function getDcts() {
         async function getDcts() {
             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());
       
 
             const sortOrder = Object.keys(fnImages);
             const sortOrder = Object.keys(fnImages);
             Object.entries(functionsQueryResponse.query.results)
             Object.entries(functionsQueryResponse.query.results)
Line 337: Line 352:
                         {
                         {
                             [DESC_KEY]: results.printouts[DESC_KEY][0],
                             [DESC_KEY]: results.printouts[DESC_KEY][0],
                             [FUNC_KEY]: results.printouts[FUNC_KEY].map(func => removePrefix(func.fulltext))
                             [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());
Line 370: 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();
                 dct.functions = new Map();
Line 390: Line 413:
         /**
         /**
         * @param {DCT} dct
         * @param {DCT} dct
        * @param {Partial<DCT>} filterState
         */
         */
         function dctFilter(dct, filterState) {
         function dctFilter(dct, filterState) {
Line 396: Line 418:
             // 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.


             // TEMPORARY!!!
             let functionsCheck = true;  // automatically pass functions check if the filter missing
             const dctFunctions = Array.from(dct.functions.values()).map(val => val.functions).flat()
             if (filterState.functions) {
            // TEMPORARY!!!
                // If filterState has a category but subfunctions array is empty, we only care about the category.
            // TODO: REWORK
                const emptyCategories = [], nonemptyCategories = [];
            const functionsCheck = filterState.functions
                Array.from(filterState.functions).forEach(([key, subs]) => {
                ? dctFunctions.some(func => filterState.functions.includes(func))
                    if (subs.length > 0) { nonemptyCategories.push(key); } else { emptyCategories.push(key); }
                 : true;
                });
 
                // Empty categories should only be checked by their name
                const checkEmpty = emptyCategories.every(cat => dct.functions.has(cat));
 
                // Nonempty categories should only be checked for presence of their subfunctions. Category itself is irrelevant.
                const checkNonempty = nonemptyCategories.every(cat => {
                    const selectedSubs = filterState.functions.get(cat);
                    const dctSubs = dct.functions.get(cat);
                    return dctSubs && dctSubs[FUNC_KEY] && selectedSubs.every(sub => dctSubs[FUNC_KEY].includes(sub));
                });
 
                functionsCheck = checkEmpty && checkNonempty;
                // TODO: Empty should no longer exist, once every category is fully listed in the filter.
                 // TODO: The filtering of functions therefore should be reduced to nonempty only.
            }
 
             const sourcesCheck = filterState.dataSources
             const sourcesCheck = filterState.dataSources
                 ? dct.dataSources.some(source => filterState.dataSources.includes(source))
                 ? filterState.dataSources.every(source => dct.dataSources.includes(source))
                 : true;
                 : true;
             const businessModelCheck = filterState.businessModel
             const businessModelCheck = filterState.businessModel
Line 410: 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 424: 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 454: Line 510:
             Array.from(functionsData).forEach(([fnCat, fnInfo], index) => {
             Array.from(functionsData).forEach(([fnCat, fnInfo], index) => {
                 const identifier = 'func-filter-' + escapeAttr(fnCat);
                 const identifier = 'func-filter-' + escapeAttr(fnCat);
                 funcFilterHtml +=
                 funcFilterHtml +=  
                     `<div class="func-filter-block">
                     `<div class="func-filter-block">
                         <div>
                         <div>
                             <input type="checkbox" checked id="${identifier}" value="${fnCat}" class="func-cat">
                             <input type="checkbox" id="${identifier}" value="${fnCat}" class="func-cat">
                             <label for="${identifier}"><img src="${fnImages[fnCat]}"> ${fnCat}</label>
                             <label for="${identifier}" title="${fnInfo[DESC_KEY]}"><img src="${fnImages[fnCat]}"> ${fnCat}</label>
                         </div>`;
                         </div>`;


                 // add subfunctions  
                 // add subfunctions  
                 if (index < 4) {
                 funcFilterHtml += '<div class="subfunc-filter-block">';
                    funcFilterHtml += '<div class="subfunc-filter-block">';
                for (const func of fnInfo.functions) {
                    for (const func of fnInfo.functions) {
                    const subfuncId = 'subfunc-filter-' + escapeAttr(func);
                        const subfuncId = 'subfunc-filter-' + escapeAttr(func);
                    funcFilterHtml +=
                        funcFilterHtml +=
                        `<div>
                            `<div>
                            <input type="checkbox" id="${subfuncId}" value="${func}">
                                <input type="checkbox" checked id="${subfuncId}" value="${func}">
                            <label for="${subfuncId}">${func}</label>
                                <label for="${subfuncId}">${func}</label>
                        </div>`;
                            </div>`;
                    }
                    funcFilterHtml += '</div>';
                 }
                 }
                funcFilterHtml += '</div>';
                 funcFilterHtml += '</div>';
                 funcFilterHtml += '</div>';
             });
             });
Line 498: 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 505: Line 560:


             // TODO: Fetch from server?
             // TODO: Fetch from server?
             const FREE_KEY = 'Freeware', FREE_PLAN_KEY = 'Free edition';
             const FREE_KEY = 'Free', FREE_PLAN_KEY = 'Free & Paid';
             const bModels = [FREE_KEY, FREE_PLAN_KEY, 'Paid 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 536: 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">' + FREE_KEY + '</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_KEY + '</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 559: 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) {
                             return Array.from(cell.getValue().keys())
                             return Array.from(cell.getValue().keys())
                                 .map(fn => `<img class="func-img" src="${fnImages[fn]}" data-value="${fn}" alt="${fn}" title="${fn}">`)
                                 .map(fn => `<img class="func-img"  
                                    src="${fnImages[fn]}"  
                                    data-value="${fn}"  
                                    alt="${fn}"  
                                    title="${fn}\n\n${functionsData.get(fn)[DESC_KEY]}">`
                                )
                                 .join('');
                                 .join('');
                         }
                         }
Line 605: 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 613: Line 701:
                 // Set result counter
                 // Set result counter
                 document.getElementById('result-count').textContent = rows.length;
                 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 622: 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 637: Line 735:
                     }
                     }
                     if (filter.type.businessModel) {
                     if (filter.type.businessModel) {
                         summaryHtml += '<tr><td><strong>License 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 practitioners</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 652: 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 674: Line 773:
             // Listen for changes in filter checkbox state.
             // Listen for changes in filter checkbox state.
             document.getElementById('functions-filter').addEventListener('change', event => {
             document.getElementById('functions-filter').addEventListener('change', event => {
                                // Selecting/deselecting the category checks/unchecks all subfunctions.
                const filterBlock = event.target.closest('.func-filter-block');
                // Picking at least one subfunction activates the category.
                const category = filterBlock.querySelector('input.func-cat');
                 // Picking zero subfunctions deactivates the category.
                const subfunctions = filterBlock.querySelectorAll('.subfunc-filter-block input[type="checkbox"]');
                const filterBlock = event.target.closes('.func-filter-block');
 
                 console.log(filterBlock)
                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 });
Line 687: 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 698: Line 805:


             // Fix bug where the table is truncated to zero height despite having visible rows.
             // Fix bug where the table is truncated to zero height despite having visible rows.
             tabulator.on('renderComplete', function() {
             tabulator.on('renderComplete', function () {
                 // TODO: Check the bugfix for a possible infinite event loop.
                 // TODO: Check the bugfix for a possible infinite event loop.
                 // This will help detect it, in case it happens.
                 // This will help detect it, in case it happens.
                 console.log('Table height bugfix: render complete.');
                 // console.log('Table height bugfix: render complete.');
               
 
                 try {
                 try {
                     const holderHeight = tabulator.rowManager.element.offsetHeight;
                     const holderHeight = tabulator.rowManager.element.offsetHeight;
Line 715: Line 822:
             });
             });
             // End bugfix
             // End bugfix
         });
         });


Line 742: 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 755: 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 779: Line 888:
             <div>
             <div>
                 <div>Technologies</div>
                 <div>Technologies</div>
                 <div style="font-size:small; letter-spacing:.03em; margin-left: .6em;">Social Media and Crowdsourcing Library</div>
                 <div style="font-size:small; letter-spacing:.03em; margin-left: .6em;">Social Media and Crowdsourcing
                    Library</div>
             </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>


             <h2 style="margin-top: 2.5rem; margin-bottom: 0;">Results: <span id="result-count"></span></h2>
             <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 813: 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 824: 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 830: 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 840: 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>License 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 853: 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 861: 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>

Revision as of 14:30, 19 December 2023

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