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
(609 intermediate revisions by 3 users not shown)
Line 1: Line 1:
<noinclude>Development verstion of the DCT List.<br><span style="color: red; font-weight: bold;">Not ready for production!</span></noinclude>
<noinclude>DCT list widget.<br><span style="color: red; font-weight: bold;">Currently in use &ndash; do not modify!</span></noinclude>
<includeonly>
<includeonly>
     <link href="https://unpkg.com/tabulator-tables@5.2.7/dist/css/tabulator.min.css" rel="stylesheet">
     <link href="/resources/assets/tabulator/dist/css/tabulator.min.css" rel="stylesheet">
     <script type="text/javascript" src="https://unpkg.com/tabulator-tables@5.2.7/dist/js/tabulator.min.js"></script>
     <script type="text/javascript" src="/resources/assets/tabulator/dist/js/tabulator.min.js"></script>


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


     <script>
     <script>
    'use strict';
        'use strict';
 
        const FUNC_KEY = 'functions';
        const DESC_KEY = 'description';
 
        /** @typedef {Map<string, { [DESC_KEY]: string, [FUNC_KEY]: string[] }>} FuncData */


    function getUrl(title) {
        /** @type FuncData */
        return title ? '/index.php/Special:FilePath/' + title : title;
        const functionsData = new Map();    // Data on functions and function categories
    }


    /**
        /**
    * @param {string[]} imageTitles - Array of image titles, e.g, ['File:Image1.png', 'File:Image2.png']
        * @typedef {Object} DCT
    */
        * @property {string} name
    async function getImages(imageTitles) {
        * @property {string} url
        const imageUrlQuery = '/api.php?action=query&format=json&prop=imageinfo&iiprop=url&titles=' + encodeURIComponent(imageTitles.join('|'));
        * @property {string[]} dataSources
        const imageUrlResponse = await fetch(imageUrlQuery).then(response => response.json());
        * @property {string[]} businessModel
        return Object.getOwnPropertyNames(imageUrlResponse.query.pages).map(pageKey => ({
        * @property {FuncData} functions
            /** @type {string} */
        * @property {string} usedByDmo
            title: imageUrlResponse.query.pages[pageKey].title,
        * @property {string} archived
            /** @type {string} */
        * @property {string} hasUC
            url: imageUrlResponse.query.pages[pageKey].imageinfo[0].url
        * @property {string} logo
        }));
        */
    }


    async function getSources() {
         let table;  // Tabulator instance
         const sourceQuery = '/api.php?action=ask&format=json&query=' + encodeURIComponent('[[Category:Social media platform]]|?IMAGE');
        const sourceResponse = await fetch(sourceQuery).then(response => response.json());


         const sources = Object.getOwnPropertyNames(sourceResponse.query.results).map(platformName => ({
         // This defines how sources are grouped in the filter.
            name: platformName,
        // Any source not listed here will be added to the last group entitled "More platforms".
             /** @type {string} */
        const sourcesLayout = [
             image: getUrl(sourceResponse.query.results[platformName].printouts.IMAGE[0].fulltext.replace('PAGENAME:', ''))
            {
         }));
                title: 'General',
                sources: ['Crowd', 'Web']
             },
             {
                title: 'Platforms',
                sources: ['Facebook', 'Twitter', 'Instagram', 'YouTube']
            }
         ];


         // const imageData = await getImages(sources.map(source => source.image));
         // This object defines the sort order of function categories and matches them to icons.
        // imageData.forEach(img => sources[sources.findIndex(source => source.image === img.title)].image = img.url);
        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',
        };


         // console.log('Sources:', sources);
         // Helpers
         return sources;
        const escapeAttr = text => text ? text.replace(/\W/g, '-') : text;
    }
        const getFilePath = title => title ? '/index.php/Special:FilePath/' + title : title;
        const getQueryUrl = query => '/api.php?action=ask&format=json&query=' + encodeURIComponent(query);
         const removePrefix = str => str.substring(str.indexOf(':') + 1);


    async function getDcts() {
        // Fetches platform / data source information.
        const dctQuery = '[[Category:Disaster Community Technology]]' +
        async function getSources() {
                        '|?Image' +
            const IMG_KEY = 'IMAGE';
                        '|?Data Sources';
            const sourceResponse = await fetch(
        const dctQueryUrl = '/api.php?action=ask&format=json&query=' + encodeURIComponent(dctQuery);
                getQueryUrl('[[Category:Social media platform]]|?' + IMG_KEY)
        const dctResponse = await fetch(dctQueryUrl).then(response => response.json());
            ).then(response => response.json());
        console.log('DCT Response:', dctResponse);


        const dctList = Object.getOwnPropertyNames(dctResponse.query.results).map(dctKey => {
            return Object.keys(sourceResponse.query.results).map(platform => {
            const dct = {};
                const img = sourceResponse.query.results[platform].printouts[IMG_KEY][0];
            dct.name = dctKey;
                return {
            /** @type {string} */
                    name: platform,
            dct.url = dctResponse.query.results[dctKey].fullurl;
                    image: img ? getFilePath(img.fulltext) : undefined
            /** @type {string[]} */
                };
            dct.dataSources = dctResponse.query.results[dctKey].printouts['Data Sources'].map(src => src.fulltext);
             });
            /** @type {string} */
         }
            dct.logo = dctResponse.query.results[dctKey].printouts['Image'][0] ? dctResponse.query.results[dctKey].printouts['Image'][0].fulltext : void 0;
             return dct;
         })


         const logoData = await getImages(dctList.map(dct => dct.logo).filter(logo => !!logo));
         // Fetches DCTs and their functions.
        return dctList;
        async function getDcts() {
    }
            const functionsQuery = '[[Category:Function_category]]'
                + '|?-Subproperty_of=' + FUNC_KEY
                + '|?Has_description=' + DESC_KEY;
            const functionsQueryResponse = await fetch(getQueryUrl(functionsQuery)).then(response => response.json());


    Promise.all([getSources(), getDcts()]).then(data => {
            const sortOrder = Object.keys(fnImages);
        const dataSources = data[0];
            Object.entries(functionsQueryResponse.query.results)
        const dcts = data[1];
                .map(([key, value]) => ([removePrefix(key), value]))
                .sort(([keyA,], [keyB,]) => sortOrder.indexOf(keyA) - sortOrder.indexOf(keyB))
                .forEach(([categoryName, results]) => {
                    functionsData.set(
                        categoryName,
                        {
                            [DESC_KEY]: results.printouts[DESC_KEY][0],
                            [FUNC_KEY]: results.printouts[FUNC_KEY]
                                .map(func => removePrefix(func.fulltext))
                                .filter(func => func !== 'Content processing languages' && func !== 'Supported content types')
                                .sort()
                        }
                    );
                });


        console.log(data)
            const allFunctions = Array.from(functionsData.values(), entry => entry[FUNC_KEY]).flat();


        const table = new Tabulator("#dct-tabulator", {
            const DATASRC_PROP = 'Data Sources';
             layout: 'fitColumns',
            const IMG_PROP = 'Image';
             columns: [
            const BUSINESS_PROP = 'Pricing';
                 {
            const DMO_PROP = 'Used by Practitioners';
                    title: 'Name',
            const UC_PROP = 'Use Cases available';
                    field: 'name',
             const ARCHIVED = 'Is Archived';
                    formatter: function (cell) {
             const dctQuery = '[[Category:Disaster Community Technology]]'
                        const dct = cell.getData();
                // + '[[Is Archived::No]]'
                        return '<a href="' + dct.url + '">' + dct.name + '</a>';
                + '|limit=500'
                + '|?' + ARCHIVED
                + '|?' + IMG_PROP
                + '|?' + DATASRC_PROP
                + '|?' + BUSINESS_PROP
                 + '|?' + DMO_PROP
                + '|?' + UC_PROP
                + '|?' + allFunctions.join('|?');
 
            const dctResponse = await fetch(getQueryUrl(dctQuery)).then(response => response.json());
 
            const dctList = Object.keys(dctResponse.query.results).map(dctName => {
                const dctResult = dctResponse.query.results[dctName];
 
                /** @type {DCT} */
                const dct = {};
                dct.name = dctName;
                dct.url = dctResult.fullurl;
                dct.dataSources = dctResult.printouts[DATASRC_PROP].map(source => source.fulltext).sort();
                dct.businessModel = dctResult.printouts[BUSINESS_PROP].map(bModel => bModel.fulltext);
                dct.logo = dctResult.printouts[IMG_PROP][0] ? getFilePath(dctResult.printouts[IMG_PROP][0].fulltext) : undefined;
                dct.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');
 
                    if (confirmedFunctions.length > 0) {
                        dct.functions.set(funcCategory, { [DESC_KEY]: categoryData[DESC_KEY], [FUNC_KEY]: confirmedFunctions });
                     }
                     }
                 },
                 });
                 {
 
                    title: 'Data Sources',
                 return dct;
                    field: 'dataSources',
            });
                     formatter: function (cell) {
 
                        const output = cell.getValue().reduce((prev, curr) => {
            return dctList;
                            const src = dataSources.find(src => src.name === curr);
        }
                            const url = src ? src.image : '';
 
                            return url ? prev + '<img class="data-source-img" src="' + url + '">' : prev + ' ' + curr;
        /**
                         }, '');
        * @param {DCT} dct
                         return output;
        */
        function dctFilter(dct, filterState) {
            // If filtering property is empty, don't apply the filter (set the check to true).
            // Passing an empty object (as with applyFilters(true)) should result in an unfiltered table.
 
            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
                ? filterState.dataSources.every(source => dct.dataSources.includes(source))
                : true;
            const businessModelCheck = filterState.businessModel
                ? dct.businessModel.some(bm => filterState.businessModel.includes(bm))
                : true;
            const dmUseCheck = filterState.usedByDmo
                ? filterState.usedByDmo === dct.usedByDmo
                : true;
            const ucCheck = filterState.hasUC
                ? filterState.hasUC === dct.hasUC
                : true;
            const archivedCheck = filterState.showArchived
                ? true
                : dct.archived !== 'yes';
 
            return sourcesCheck && functionsCheck && businessModelCheck && dmUseCheck && ucCheck && archivedCheck;
        }
 
        function applyFilters(clear) {
            if (!table) return; // prevent filtering before the table is ready
 
            // If clear=true, pass empty object to the filter to disable it.
            if (clear) {
                table.setFilter(dctFilter, {});
                return;
            }
 
            const filterState = {};
 
            if (document.querySelectorAll('#functions-filter input[type="checkbox"]:checked').length > 0) {
                const functionFilterBlocks = document.querySelectorAll('#functions-filter .func-filter-block');
                const funcOpts = new Map();
                functionFilterBlocks.forEach(block => {
                    const cat = block.querySelector('input.func-cat');
                    if (cat.checked) {
                         funcOpts.set(
                            cat.value,
                            Array.from(block.querySelectorAll('.subfunc-filter-block input[type="checkbox"]:checked'))
                                .map(box => box.value)
                         );
                     }
                     }
                });
                filterState.functions = funcOpts;
            }
            const selectedSources = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]:checked'))
                .map(checkbox => checkbox.value);
            if (selectedSources.length > 0) filterState.dataSources = selectedSources;
            const selectedBModels = Array.from(document.querySelectorAll('#business-model-filter input[type="checkbox"]:checked'))
                .map(checkbox => checkbox.value);
            if (selectedBModels.length > 0) filterState.businessModel = selectedBModels;
            if (document.getElementById('used-by-practitioners').checked) filterState.usedByDmo = 'yes';
            if (document.getElementById('has-use-case').checked) filterState.hasUC = 'yes';
            if (document.getElementById('show-archived').checked) filterState.showArchived = 'yes';
            table.setFilter(dctFilter, filterState);
        }
        // Load data and build page.
        Promise.all([getSources(), getDcts()]).then(data => {
            const [dataSources, dcts] = data;
            // Set up functions filter
            let funcFilterHtml = '';
            Array.from(functionsData).forEach(([fnCat, fnInfo], index) => {
                const identifier = 'func-filter-' + escapeAttr(fnCat);
                funcFilterHtml +=
                    `<div class="func-filter-block">
                        <div>
                            <input type="checkbox" id="${identifier}" value="${fnCat}" class="func-cat">
                            <label for="${identifier}" title="${fnInfo[DESC_KEY]}"><img src="${fnImages[fnCat]}"> ${fnCat}</label>
                        </div>`;
                // add subfunctions
                funcFilterHtml += '<div class="subfunc-filter-block">';
                for (const func of fnInfo.functions) {
                    const subfuncId = 'subfunc-filter-' + escapeAttr(func);
                    funcFilterHtml +=
                        `<div>
                            <input type="checkbox" id="${subfuncId}" value="${func}">
                            <label for="${subfuncId}">${func}</label>
                        </div>`;
                }
                funcFilterHtml += '</div>';
                funcFilterHtml += '</div>';
            });
            document.getElementById('functions-filter').innerHTML = funcFilterHtml;
            // Set up sources filter
            const groupedSources = [];
            const sourcesCopy = Array.from(dataSources);
            for (const layoutGroup of sourcesLayout) {
                const group = [];
                for (const source of layoutGroup.sources) {
                    let idx = sourcesCopy.findIndex(src => src.name === source);
                    if (idx !== -1) { group.push(sourcesCopy.splice(idx, 1)[0]); }
                 }
                 }
             ]
                if (group.length > 0) { groupedSources.push({ title: layoutGroup.title, sources: group }); }
             }
            groupedSources.push({ title: 'More platforms', sources: sourcesCopy });
 
            let dataSourceFilterHtml = '';
            groupedSources.forEach(group => {
                dataSourceFilterHtml += '<div class="filter-group-header">' + group.title + '</div>';
                dataSourceFilterHtml += group.sources.reduce((acc, curr, idx) => {
                    const identifier = escapeAttr(curr.name);
                    return acc +
                        '<div ' + (idx === 0 ? ' class="filter-group-start">' : '>') +
                        '<input type="checkbox" id="filter-' + identifier + '" value="' + curr.name + '">' +
                        '<label for="filter-' + identifier + '"><img src="' + curr.image + '"> ' + curr.name + '</label></div>'
                }, '');
            });
            document.getElementById('data-source-filter').innerHTML = dataSourceFilterHtml;
 
            // TODO: Fetch from server?
            const FREE_KEY = 'Free', FREE_PLAN_KEY = 'Free & Paid';
            const pricing = [FREE_KEY, FREE_PLAN_KEY, 'Paid'];
            let pricingFilterHtml = pricing.reduce((acc, curr) => {
                const identifier = escapeAttr(curr);
                return acc
                    + '<div><input type="checkbox" id="bm-filter-' + identifier
                    + '" value="' + curr + '">'
                    + '<label for="bm-filter-' + identifier + '">' + curr
                    + '</label></div>'
            }, '');
            document.getElementById('business-model-filter').innerHTML = pricingFilterHtml;
 
            // Set up table.
            const tabulator = new Tabulator("#dct-tabulator", {
                layout: 'fitColumns',
                responsiveLayout: 'collapse',
                columns: [
                    {
                        title: 'Name',
                        field: 'name',
                        minWidth: 300, // required for responsiveness when using fitColumns
                        widthGrow: 2,
                        formatter: function (cell) {
                            /** @type {DCT} */
                            const dct = cell.getData();
                            let out = '<a href="' + dct.url + '" translate="no">' + dct.name + '</a><br>';
                            if (dct.archived.toLowerCase() === 'yes') {
                                out += '<small><span class="badge lcc-badge-grey">Archived</span></small> ';
                            }
                            if (dct.businessModel.includes(FREE_KEY)) {
                                out += '<small><span class="badge lcc-badge-green">' + FREE_KEY + '</span></small> ';
                            }
                            if (dct.businessModel.includes(FREE_PLAN_KEY)) {
                                out += '<small><span class="badge lcc-badge-green">' + FREE_PLAN_KEY + '</span></small> ';
                            }
                            if (dct.usedByDmo.toLowerCase() === 'yes') {
                                out += '<small><span class="badge lcc-badge-red">Used by practitioners</span></small> ';
                            }
                            if (dct.hasUC.toLowerCase() === 'yes') {
                                out += '<small><span class="badge lcc-badge-purple">Use case available</span></small> ';
                            }
 
                            return out;
                        }
                    },
                    {
                        title: 'Functions',
                        field: 'functions',
                        minWidth: 200, // required for responsiveness when using fitColumns
                        cssClass: 'functions-cell',
                        formatter: function (cell) {
                            return Array.from(cell.getValue().keys())
                                .map(fn => `<img class="func-img"
                                    src="${fnImages[fn]}"
                                    data-value="${fn}"
                                    alt="${fn}"
                                    title="${fn}\n\n${functionsData.get(fn)[DESC_KEY]}">`
                                )
                                .join('');
                        }
                    },
                    {
                        title: 'Supported Platforms',
                        field: 'dataSources',
                        minWidth: 300, // required for responsiveness when using fitColumns
                        cssClass: 'data-sources-cell',
                        formatter: function (cell) {
                            const val = cell.getValue();
                            let out = '<div>';
 
                            groupedSources.forEach((group, gIndex) => {
                                // if (gIndex === groupedSources.length - 1) { out += '<div class="sources-collapse">'; }
                                out += group.sources.reduce((prev, curr) => {
                                    const idx = val.findIndex(src => src === curr.name);
                                    if (idx === -1) {
                                        return prev;
                                    } else {
                                        return curr.image
                                            ? prev + '<img class="data-source-img" data-value="' + curr.name
                                            + '" src="' + curr.image
                                            + '" alt="' + curr.name
                                            + '" title="' + curr.name + '">'
                                            : prev + ' ' + curr.name;
                                    }
                                }, '');
                                if (gIndex === groupedSources.length - 1) {
                                    // out += '<span class="sources-collapse-toggle"></span>';
                                    // out += '</div>';
                                }
                            });
                            return out + '</div>';
                        }
                    }
                ]
            });
 
            tabulator.on('tableBuilt', () => {
                tabulator.setData(dcts);
                table = tabulator;
 
                // Set up the table if parameter was passed.
                const params = new URLSearchParams(window.location.search);
                const encoded = params.get('do');
 
                if (encoded) {
                    const actions = JSON.parse(decodeURIComponent(atob(encoded)));
 
                    const filter = actions.filter;
                    if (filter) {
                        // Functions filter
                        const functions = filter.functions;
                        if (functions) {
                            Object.keys(functions).forEach(subfun => {
                                const subfunEl = document.getElementById('subfunc-filter-' + escapeAttr(subfun));
                                subfunEl.checked = !!functions[subfun];
                                subfunEl.dispatchEvent(new Event('change', { bubbles: true }));
                            });
 
                            document.getElementById('functions-filter').closest('.filter-wrapper').classList.toggle('open');
                        }
 
                        // Further filters
                        // ...
 
                        applyFilters();
                        toggleFilter();
                    }
 
                    // Further actions (e.g. open filter panel, etc.)
                    // ...
                }
 
                applyFilters();
            });
 
            tabulator.on('dataFiltered', (filters, rows) => {
                const summary = document.getElementById('filter-summary');
                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).
                if (!(filter && filter.type)) { summary.textContent = 'No filter. Showing all results.'; return; }
 
                // Update filter text.
                if (
                    !filter.type.functions &&
                    !filter.type.dataSources &&
                    !filter.type.businessModel &&
                    !filter.type.usedByDmo &&
                    !filter.type.hasUC
                ) { summary.textContent = 'No filter. Showing all results.'; }
                else {
                    let summaryHtml = '<table>';
                    if (filter.type.functions) {
                        summaryHtml += '<tr><td><strong>Functions</strong></td><td>';
                        if (filter.type.functions.size === 0) {
                            summaryHtml += 'none';
                        } else {
                            for (const [cat, subs] of filter.type.functions) {
                                summaryHtml += cat;
                                summaryHtml += subs.length > 0 ? ' <small>(' + subs.join(', ') + ')</small>' : '';
                                summaryHtml += ', ';
                            }
                            summaryHtml = summaryHtml.substring(0, summaryHtml.length - 2);
                        }
                        summaryHtml += '</td></tr>';
                    }
                    if (filter.type.dataSources) {
                        summaryHtml += '<tr><td><strong>Platforms</strong></td><td>'
                            + (filter.type.dataSources.length > 0 ? filter.type.dataSources.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    if (filter.type.businessModel) {
                        summaryHtml += '<tr><td><strong>Pricing</strong></td><td>'
                            + (filter.type.businessModel.length > 0 ? filter.type.businessModel.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    if (filter.type.usedByDmo) {
                        summaryHtml += '<tr><td><strong>Used by practitioners</strong></td><td>Yes</td></tr>';
                    }
                    if (filter.type.hasUC) {
                        summaryHtml += '<tr><td><strong>Use case available</strong></td><td>Yes</td></tr>';
                    }
                    summaryHtml += '</table>';
                    summary.innerHTML = summaryHtml;
                }
 
                const markImages = () => {
                    const selectedSources = filter.type.dataSources;
                    const selectedFunctions = filter.type.functions ? Array.from(filter.type.functions.keys()) : undefined;
 
                    // Mark data source images
                    document.querySelectorAll('.data-sources-cell .data-source-img, .tabulator-responsive-collapse .data-source-img')
                        .forEach(img => {
                            if (!selectedSources || selectedSources.includes(img.dataset.value)) { img.classList.remove('unselected'); }
                            else { img.classList.add('unselected'); }
                        });
 
                    // Mark functions images
                    document.querySelectorAll('.functions-cell .func-img').forEach(img => {
                        if (!selectedFunctions || selectedFunctions.includes(img.dataset.value)) { img.classList.remove('unselected'); }
                        else { img.classList.add('unselected'); }
                    });
 
                    tabulator.off('renderComplete', markImages);
                }
                tabulator.on('renderComplete', markImages); // TODO: Prevent this from running if corresponding filters are not active.
            });
 
            // Listen for changes in filter checkbox state.
            document.getElementById('functions-filter').addEventListener('change', event => {
                const filterBlock = event.target.closest('.func-filter-block');
                const category = filterBlock.querySelector('input.func-cat');
                const subfunctions = filterBlock.querySelectorAll('.subfunc-filter-block input[type="checkbox"]');
 
                if (event.target === category) {
                    // Selecting/deselecting the category checks/unchecks all subfunctions.
                    subfunctions.forEach(checkbox => checkbox.checked = category.checked);
                } else {
                    // If no subfunctions are selected, deactivate the category. Activate otherwise.
                    const checkedSubs = Array.from(subfunctions).filter(sub => sub.checked).length;
                    category.checked = checkedSubs > 0;
                }
 
                applyFilters();
            }, { passive: true });
            document.getElementById('data-source-filter').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });
            document.getElementById('business-model-filter').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });
            document.getElementById('bool-filters').addEventListener('change', event => {
                applyFilters();
            }, { 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
 
         });
         });


         table.on("tableBuilt", () => { table.setData(dcts); });
         function selectAll(context) {
    })
            document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = true);
            applyFilters();
        }


        function deselectAll(context) {
            document.querySelectorAll(context + ' input[type="checkbox"]').forEach(checkbox => checkbox.checked = false);
            applyFilters();
        }
        function clearFilters() {
            document.querySelectorAll('#dct-filters input[type="checkbox').forEach(checkbox => checkbox.checked = checkbox.defaultChecked);
            applyFilters(true);
        }
        function toggleFilter() {
            document.getElementById('dct-filters').classList.toggle('open');
        }
        // Close filter pane when clicked outside of it.
        document.body.addEventListener('click', event => {
            const filterPane = document.getElementById('dct-filters');
            if (
                filterPane.classList.contains('open') &&
                !filterPane.contains(event.target) &&
                event.target !== document.querySelector('#filter-bar .large-button.open-filters')
            ) { filterPane.classList.remove('open'); }
        }, { passive: true });
     </script>
     </script>
    <!-- Icon definitons -->
    <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" style="display:none;">
        <defs>
            <symbol id="chevron-down" viewBox="0 0 96 96">
                <clipPath id="chev">
                    <path d="M592 312h96v96h-96z" />
                </clipPath>
                <g clip-path="url(#chev)" transform="translate(-592 -312)">
                    <path
                        d="m640 370.586-25.293-25.293-1.414 1.414L640 373.414l26.707-26.707-1.414-1.414L640 370.586Z" />
                </g>
            </symbol>
        </defs>
    </svg>
     <div id="dct-list-wrapper">
     <div id="dct-list-wrapper">
        <h1>
            <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" viewBox="0 0 96 96">
                <defs>
                    <clipPath id="a">
                        <path d="M65 334h96v96H65z" />
                    </clipPath>
                </defs>
                <g clip-path="url(#a)" transform="translate(-65 -334)">
                    <path d="m142 395.981.004-35.993H84.001v36.01Zm-55.999-33.993h54.003L140 393.981l-53.999.017Z" />
                    <path
                        d="M81.001 358.987a2.002 2.002 0 0 1 2-2h60.003a2.002 2.002 0 0 1 2 2V400h2v-41.013a4.003 4.003 0 0 0-4-4H83.001a4.003 4.003 0 0 0-4 4V400h2ZM118.003 402.981v2h-10.001v-2H67v1.999a5.006 5.006 0 0 0 5 5.001h82.004a5.006 5.006 0 0 0 5.001-5.001v-1.999Zm36.001 5H72a3.004 3.004 0 0 1-3-3h37.002a1.934 1.934 0 0 0 2 2h10.001a1.934 1.934 0 0 0 2-2h37.002a3.005 3.005 0 0 1-3.001 3Z" />
                    <path
                        d="M113 365c-7.18 0-13 5.82-13 13s5.82 13 13 13 13-5.82 13-13c-.008-7.176-5.824-12.992-13-13Zm10.949 12h-3.518a14.167 14.167 0 0 0-4.176-9.508 11.023 11.023 0 0 1 7.694 9.508ZM112 368.226V377h-4.426c.254-3.771 1.929-7.06 4.426-8.774ZM112 379v8.775c-2.494-1.717-4.17-5.017-4.425-8.775Zm2 8.774V379h4.425c-.255 3.754-1.932 7.056-4.425 8.774ZM114 377v-8.773c2.492 1.718 4.17 5.019 4.425 8.773Zm-4.273-9.502a14.124 14.124 0 0 0-4.158 9.502h-3.518a11.02 11.02 0 0 1 7.676-9.502ZM102.051 379h3.518a14.157 14.157 0 0 0 4.173 9.507 11.022 11.022 0 0 1-7.691-9.507Zm14.205 9.508a14.168 14.168 0 0 0 4.175-9.508h3.518a11.023 11.023 0 0 1-7.693 9.508Z" />
                </g>
            </svg>
            <div>
                <div>Technologies</div>
                <div style="font-size:small; letter-spacing:.03em; margin-left: .6em;">Social Media and Crowdsourcing
                    Library</div>
            </div>
        </h1>
        <div id="dct-intro">
            <p>The overall goal of the Social Media and Crowdsourcing (SMCS) Technologies Library is to face the growing
                heterogeneous use of technologies in disasters and the overwhelming number of technologies on the
                market. It
                gathers and structures information about existing technologies to provide an up-to-date overview and
                thus
                support the selection of suitable technologies.
            </p>
            <p>
                You can use the filters to identify relevant technologies according to your needs and then click on the
                name of
                the technology to get further information.
            </p>
        </div>
        <div id="filter-bar">
            <div style="display: flex; justify-content: space-between;">
                <div style="flex: 1 1;">
                    <h2 style="margin-bottom: 1rem;">Selected Filters</h2>
                    <div id="filter-summary">No filter. Showing all results.</div>
                </div>
                <div>
                    <button class="large-button open-filters" type="button" onclick="toggleFilter()">Open
                        Filters</button>
                </div>
            </div>
            <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">
                <h2 style="display: flex; justify-content: space-between;">
                    <div>
                        <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden"
                            viewBox="0 0 96 96">
                            <defs>
                                <clipPath id="b">
                                    <path d="M592 312h96v96h-96z" />
                                </clipPath>
                            </defs>
                            <g clip-path="url(#b)" transform="translate(-592 -312)">
                                <path
                                    d="M636 356.012v38.011l8-8v-30.011L674 326h-68Zm6.588-1.412-.588.584v30.008l-4 4v-34.008l-.585-.586L610.828 328h58.348Z" />
                            </g>
                        </svg>
                        <span>Filters</span>
                    </div>
                    <a onclick="toggleFilter()" id="close-filter-button">&times;</a>
                </h2>
                <div style="text-align: center;">
                    <button class="large-button" type="button" onclick="clearFilters()">Clear Filters</button>
                </div>
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Functions <div class="plus icon"></div>
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#functions-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#functions-filter')">Clear all</button>
                        </div>
                        <div class="filter-content loose" id="functions-filter"></div>
                    </div>
                </div>
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Supported Platforms <div class="plus icon"></div>
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#data-source-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#data-source-filter')">Clear all</button>
                        </div>
                        <div class="filter-content loose" id="data-source-filter"></div>
                    </div>
                </div>
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Pricing <div class="plus icon"></div>
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#business-model-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#business-model-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="business-model-filter"></div>
                    </div>
                </div>
                <!-- <div class="filter-wrapper">
                    <h4 class="filter-toggle">Used by Practitioners <div class="plus icon"></div>
                    </h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#dm-use-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#dm-use-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="dm-use-filter"></div>
                    </div>
                </div> -->
                <div id="bool-filters"
                    style="border-top: 1px solid var(--links-blue); margin-top: 1em; padding-top: 1em;">
                    <div>
                        <input type="checkbox" id="used-by-practitioners" value="yes">
                        <label for="used-by-practitioners">Used by practitioners</label>
                    </div>
                    <div>
                        <input type="checkbox" id="has-use-case" value="yes">
                        <label for="has-use-case">Use case available</label>
                    </div>
                    <div>
                        <input type="checkbox" id="show-archived" value="yes">
                        <label for="show-archived">Show archived</label>
                    </div>
                </div>
            </div>
        </div>
         <div id="dct-tabulator"></div>
         <div id="dct-tabulator"></div>
     </div>
     </div>
</includeonly>
</includeonly>

Revision as of 14:30, 19 December 2023

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