Widget: DCTList: Difference between revisions

From LINKS Community Center
Jump to: navigation, search
Eschmidt (talk | contribs)
No edit summary
Marterer (talk | contribs)
No edit summary
(151 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="/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>
        #dct-list-wrapper {
            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 {
    <!-- STYLES BEGIN -->
            display: block;
<link rel="stylesheet" href="https://api.safety-base.eu/v2/links/lib?q=dct_list.css">
            cursor: pointer;
<!-- STYLES END -->
            font-size: 2.5em;
            line-height: .7em;
            margin-top: -.2em;
            font-weight: 100;
            color: var(--links-blue);
        }


        #dct-filters h2 svg {
    <!-- SCRIPT BEGIN -->
            height: 1.5em;
     <script type="text/javascript" src="https://api.safety-base.eu/v2/links/lib?q=dct_list.js"></script>
            width: 1.5em;
    <!-- SCRIPT END -->
            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-container { display: none; }
        .filter-wrapper.open .filter-container { display: block; }
 
        .filter-wrapper h4 {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
        }
 
        .filter-wrapper .filter-toggle svg {
            width: 1.5em;
            height: 1.5em;
            fill: var(--links-blue);
            cursor: pointer;
        }
 
        .filter-wrapper.open .filter-toggle {
            transform: rotate(180deg);
        }
 
        .filter-content {
            font-size: 1.2em;
            margin-bottom: 4em;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(14em, 1fr));
            gap: 1em 1em;
            align-items: start;
        }
 
        .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>
 
     <script>
        'use strict';
 
        const FUNC_KEY = 'functions';
        const DESC_KEY = 'description';
 
        /** @typedef {Map<string, { [DESC_KEY]: string, [FUNC_KEY]: string[] }>} FuncData */
 
        /** @type FuncData */
        const functionsData = new Map();    // Data on functions and function categories
 
        /**
        * @typedef {Object} DCT
        * @property {string} name
        * @property {string} url
        * @property {string[]} dataSources
        * @property {string[]} businessModel
        * @property {FuncData} functions
        * @property {string} usedByDmo
        * @property {string} logo
        */
 
        let table;  // Tabulator instance
 
        // This defines how sources are grouped in the filter.
        // Any source not listed here will be added to the last group entitled "More platforms".
        const sourcesLayout = [
            {
                title: 'General',
                sources: ['Crowd', 'Web']
            },
            {
                title: 'Platforms',
                sources: ['Facebook', 'Twitter', 'Instagram', 'YouTube']
            }
        ];
 
        // This object defines the sort order of function categories and matches them to icons.
        const fnImages = {
            'Search and Monitor':  '/index.php/Special:FilePath/File:Func_search.svg',
            'Post and Schedule':    '/index.php/Special:FilePath/File:Func_post.svg',
            'Analysis':            '/index.php/Special:FilePath/File:Func_analysis.svg',
            'Metrics':              '/index.php/Special:FilePath/File:Func_metrics.svg',
            'Report':              '/index.php/Special:FilePath/File:Func_report.svg',
            'Collaboration':        '/index.php/Special:FilePath/File:Func_collaboration.svg',
            'Interoperability':    '/index.php/Special:FilePath/File:Func_interoperability.svg',
            'Meta':                '/index.php/Special:FilePath/File:Func_meta.svg',
        };
 
        // Helpers
        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);
 
        // Fetches platform / data source information.
        async function getSources() {
            const IMG_KEY = 'IMAGE';
            const sourceResponse = await fetch(
                getQueryUrl('[[Category:Social media platform]]|?' + IMG_KEY)
            ).then(response => response.json());
 
            return Object.keys(sourceResponse.query.results).map(platform => {
                const img = sourceResponse.query.results[platform].printouts[IMG_KEY][0];
                return {
                    name: platform,
                    image: img ? getFilePath(img.fulltext) : undefined
                };
            });
        }
 
        // Fetches DCTs and their functions.
        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());
       
            const sortOrder = Object.keys(fnImages);
            Object.entries(functionsQueryResponse.query.results)
                .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))
                        }
                    );
                });
 
            // 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 DATASRC_PROP = 'Data Sources';
            const IMG_PROP = 'Image';
            const BUSINESS_PROP = 'Business model';
            const DMO_PROP = 'Used by Practitioners';
            const dctQuery = '[[Category:Disaster Community Technology]]'
                          + '[[Is Archived::No]]'
                          + '|limit=500'
                          + '|?' + IMG_PROP
                          + '|?' + DATASRC_PROP
                          + '|?' + BUSINESS_PROP
                          + '|?' + DMO_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] ? dctResult.printouts[DMO_PROP][0].fulltext : 'Unknown';
 
                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 });
                    }
                });
 
                return dct;
            });
 
            return dctList;
        }
 
        /**
        * @param {DCT} dct
        * @param {Partial<DCT>} filterState
        */
        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.
 
            // TEMPORARY!!!
            const dctFunctions = Array.from(dct.functions.values()).map(val => val.functions).flat()
            // TEMPORARY!!!
            // TODO: REWORK
            const functionsCheck = filterState.functions
                ? dctFunctions.some(func => filterState.functions.includes(func))
                : true;
            const sourcesCheck = filterState.dataSources
                ? dct.dataSources.some(source => filterState.dataSources.includes(source))
                : true;
            const businessModelCheck = filterState.businessModel
                ? dct.businessModel.some(bm => filterState.businessModel.includes(bm))
                : true;
            const dmUseCheck = filterState.usedByDmo
                ? filterState.usedByDmo.includes(dct.usedByDmo)
                : true;
            return sourcesCheck && functionsCheck && businessModelCheck && dmUseCheck;
        }
 
        function applyFilters(clear) {
            if (!table) return;
 
            // If clear=true, pass empty object to the filter to disable it.
            if (clear) {
                table.setFilter(dctFilter, {});
                return;
            }
 
            /** @type {Partial<Omit<DCT, 'usedByDmo'> & { usedByDmo: string[]>}} */
            const filterState = {};
 
            const functionOptions = Array.from(document.querySelectorAll('#functions-filter input[type="checkbox"]'));
            const selectedFunctions = functionOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
            filterState.functions = selectedFunctions.length === functionOptions.length ? undefined : selectedFunctions;
 
            const sourceOptions = Array.from(document.querySelectorAll('#data-source-filter input[type="checkbox"]'));
            const selectedSources = sourceOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
            filterState.dataSources = selectedSources.length === sourceOptions.length ? undefined : selectedSources;
 
            const bmOptions = Array.from(document.querySelectorAll('#business-model-filter input[type="checkbox"]'));
            const selectedBModels = bmOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
            filterState.businessModel = selectedBModels.length === bmOptions.length ? undefined : selectedBModels;
 
            const dmUseOptions = Array.from(document.querySelectorAll('#dm-use-filter input[type="checkbox"]'));
            const selectedDmUseOptions = dmUseOptions.filter(checkbox => checkbox.checked).map(checkbox => checkbox.value);
            filterState.usedByDmo = selectedDmUseOptions.length === dmUseOptions.length ? undefined : selectedDmUseOptions;
 
            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" checked id="${identifier}" value="${fnCat}" class="func-cat">
                            <label for="${identifier}"><img src="${fnImages[fnCat]}"> ${fnCat}</label>
                        </div>`;
 
                // add subfunctions
                if (index < 4) {
                    funcFilterHtml += '<div class="subfunc-filter-block">';
                    for (const func of fnInfo.functions) {
                        const subfuncId = 'subfunc-filter-' + escapeAttr(func);
                        funcFilterHtml +=
                            `<div>
                                <input type="checkbox" checked 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 + '" checked>' +
                        '<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 = 'Freeware', FREE_PLAN_KEY = 'Free edition';
            const bModels = [FREE_KEY, FREE_PLAN_KEY, 'Paid subscription', 'Other'];
            let bModelFilterHtml = bModels.reduce((acc, curr) => {
                const identifier = escapeAttr(curr);
                return acc
                    + '<div><input type="checkbox" checked id="bm-filter-' + identifier
                    + '" value="' + curr + '">'
                    + '<label for="bm-filter-' + identifier + '">' + curr
                    + '</label></div>'
            }, '');
            document.getElementById('business-model-filter').innerHTML = bModelFilterHtml;
 
            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.
            const tabulator = new Tabulator("#dct-tabulator", {
                layout: 'fitColumns',
                responsiveLayout: 'collapse',
                columns: [
                    {
                        title: 'Name',
                        field: 'name',
                        minWidth: 300, // required for responsiveness when using fitColumns
                        formatter: function (cell) {
                            /** @type {DCT} */
                            const dct = cell.getData();
                            let out = '<a href="' + dct.url + '">' + dct.name + '</a><br>';
                            if (dct.businessModel.includes(FREE_KEY)) {
                                out += '<small><span class="badge badge-success">' + FREE_KEY + '</span></small> ';
                            }
                            if (dct.businessModel.includes(FREE_PLAN_KEY)) {
                                out += '<small><span class="badge badge-success">' + FREE_PLAN_KEY + '</span></small> ';
                            }
                            if (dct.usedByDmo.toLowerCase() === 'yes') {
                                out += '<small><span class="badge badge-danger">Used by practitioners</span></small> ';
                            }
                            if (dct.usedByDmo.toLowerCase() === 'yes with use case') {
                                out += '<small><span class="badge badge-danger">Used by practitioners</span></small>';
                                out += ' <small><span class="badge badge-warning">Use case</span></small>'
                            }
                            return out;
                        }
                    },
                    {
                        title: 'Functions',
                        field: 'functions',
                        minWidth: 300, // 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}">`)
                                .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;
            });
 
            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
                ) { summary.textContent = 'No filter. Showing all results.'; }
                else {
                    let summaryHtml = '<table>';
                    if (filter.type.functions) {
                        summaryHtml += '<tr><td><strong>Functions</strong></td><td>'
                            + (filter.type.functions.length > 0 ? filter.type.functions.join(', ') : 'none')
                            + '</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>License model</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>'
                            + (filter.type.usedByDmo.length > 0 ? filter.type.usedByDmo.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    summaryHtml += '</table>';
                    summary.innerHTML = summaryHtml;
                }
 
                const markImages = () => {
                    const selectedSources = filter.type.dataSources;
                    const selectedFunctions = filter.type.functions;
 
                    // 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 subfunctions = filterBlock.querySelectorAll('.subfunc-filter-block input[type="checkbox"]');
                // Selecting/deselecting the category checks/unchecks all subfunctions.
                // Picking at least one subfunction activates the category.
                // Picking zero subfunctions deactivates the category.
                console.log(filterBlock)
                console.log(subfunctions)
                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('dm-use-filter').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
        });
 
        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')
            ) { filterPane.classList.remove('open'); }
        }, { passive: true });
    </script>


     <!-- Icon definitons -->
     <!-- Icon definitons -->
     <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" style="display:none;">
     <!--<img src="https://api.safety-base.eu/v2/links/lib?q=unknown.svg">-->
        <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>
         <h1>
             <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" viewBox="0 0 96 96">
             <img src="https://api.safety-base.eu/v2/links/lib?q=1-dct_list.svg">
                <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>
                 <div>Technologies</div>
                 <div>Technologies</div>
Line 785: Line 25:
         </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">
                         <img src="https://www.safetyinnovation.center/lcc/dct_list/img/filters.svg">
                            <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>
                         <span>Filters</span>
                     </div>
                     </div>
                     <a onclick="toggleFilter()" id="close-filter-button">&times;</a>
                     <a onclick="toggleFilter()" id="close-filter-button">&times;</a>
                 </h2>
                 </h2>
                 <div style="text-align: center;">
                 <div style="text-align: center;">
                     <button class="large-button" type="button" onclick="clearFilters()">Clear Filters</button>
                     <button class="large-button" type="button" onclick="clearFilters()">Clear Filters</button>
                 </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 832: Line 74:
                             <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 842: Line 86:
                             <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 855: Line 101:
                     </div>
                     </div>
                 </div>
                 </div>
                 <div class="filter-wrapper">
 
                     <h4>Used by Practitioners <span class="filter-toggle"><svg><use href="#chevron-down"/></svg></span></h4>
                 <!-- <div class="filter-wrapper">
                     <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 863: Line 111:
                         </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>
             </div>
             </div>
         </div>
         </div>
         <div id="dct-tabulator"></div>
         <div id="dct-tabulator"></div>
     </div>
     </div>
</includeonly>
</includeonly>

Revision as of 17:06, 12 June 2024

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