Widget: CrisisCommunicationList: Difference between revisions

From LINKS Community Center
Jump to: navigation, search
Eschmidt (talk | contribs)
(Created page with "<noinclude>Crisis Communication list widget.<br><span style="color: red; font-weight: bold;">Currently in use – do not modify!</span></noinclude> <includeonly></includeo...")
 
Eschmidt (talk | contribs)
No edit summary
Line 1: Line 1:
<noinclude>Crisis Communication list widget.<br><span style="color: red; font-weight: bold;">Currently in use &ndash; do not modify!</span></noinclude>
<noinclude>Current version of the Crisis Communication Library.<br><strong style="color:red;">Currently in use &ndash; do not modify!</strong>
<includeonly></includeonly>
</noinclude>
<includeonly>
    <link href="/resources/assets/tabulator/dist/css/tabulator.min.css" rel="stylesheet">
    <script type="text/javascript" src="/resources/assets/tabulator/dist/js/tabulator.min.js"></script>
    <style>
        #usecase-list-wrapper {
            font-family: 'Open Sans';
            margin-top: 4em;
        }
 
        #usecase-list-wrapper h1,
        #usecase-list-wrapper h2,
        #usecase-list-wrapper h3,
        #usecase-list-wrapper h4 {
            font-family: 'Raleway';
            font-weight: 300;
            letter-spacing: .06em;
        }
 
        #usecase-list-wrapper h1,
        #usecase-filters h2 {
            color: var(--links-cyan);
            display: flex;
            align-items: center;
        }
 
        #usecase-list-wrapper h1 svg {
            height: 2.5em;
            width: 2.5em;
            fill: var(--links-cyan);
            margin-right: .5em;
        }
 
        #filter-bar {
            position: relative;
            margin: 1em 0 2em 0;
        }
 
        #cc-filters {
            position: absolute;
            top: 0;
            right: 0;
            z-index: 100;
            padding: 2em;
            width: 45vw;
            background: #fff;
            border: 1px solid var(--links-cyan);
            clip-path: inset(0 0 100% 100%);
            box-shadow: -10px 10px 10px 5px rgb(0 0 0 / 10%);
            transition: all 400ms ease-in-out;
        }
 
        #cc-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-cyan);
        }
 
        #cc-filters h2 svg {
            height: 1.5em;
            width: 1.5em;
            margin-right: .5em;
            margin-left: -.2em;
        }
 
        .large-button {
            border: 1px solid var(--links-cyan);
            font-size: 1.5em;
            font-family: 'Open Sans';
            font-weight: 100;
            margin-bottom: 2em;
            padding: 0.3em 0.8em;
            color: var(--links-cyan);
            background: transparent;
            font-variant: small-caps;
            display: inline-block;
            transition: all 200ms ease-in-out;
        }
 
        .large-button:hover {
            background-color: var(--links-cyan);
            color: #fff;
        }
 
        #usecases-list-wrapper h2 {
            margin-bottom: 1em;
        }
 
        .filter-button-wrapper {
            margin-bottom: 1em;
        }
 
        .filter-wrapper button {
            border: 0 none;
            color: var(--links-cyan);
            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 .filter-toggle {
            cursor: pointer;
        }
 
        .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;
        }
 
        .filter-content label {
            display: inline;
        }
 
        #intro {
            font-size: larger;
            margin-bottom: 4em;
        }
 
        #intro ul {
            font-size: initial;
        }
 
        #tabulator-table.tabulator {
            border: 0 none;
            background-color: transparent;
            font-size: 1.2em;
        }
 
        #tabulator-table.tabulator .tabulator-header {
            background-color: transparent;
            border: 0 none;
        }
 
        #tabulator-table.tabulator .tabulator-header .tabulator-col {
            border-bottom: 4px double var(--links-cyan);
            border-right: 0 none;
            background: transparent;
        }
 
        #tabulator-table.tabulator .tabulator-header .tabulator-col-content {
            padding: .5em 0;
        }
 
        #tabulator-table.tabulator .tabulator-header .tabulator-col-sorter {
            right: .5em;
        }
 
        #tabulator-table.tabulator .tabulator-row {
            background-color: transparent;
        }
 
        #tabulator-table.tabulator .tabulator-cell {
            border-right: 0 none;
            border-top: 1px solid var(--links-cyan);
            padding: .5em .5em .5em 0;
        }
 
        #tabulator-table.tabulator .tabulator-row .tabulator-responsive-collapse {
            border: 0 none;
            padding: 0 .5em 2em 0;
        }
 
        #filter-summary {
            margin-right: 1em;
        }
 
        #filter-summary table tr td {
            vertical-align: top;
        }
 
        #filter-summary table tr td:first-of-type {
            padding-right: 10px;
        }
 
 
        #tabulator-table.tabulator .tabulator-row .tabulator-responsive-collapse table {
            font-size: smaller;
        }
 
        #filter-summary table tr td strong,
        #tabulator-table.tabulator .tabulator-row .tabulator-responsive-collapse td>strong {
            font-weight: 300;
        }
 
        #tabulator-table.tabulator .usecase-title a,
        #tabulator-table.tabulator .tabulator-responsive-collapse table a {
            color: var(--links-cyan);
        }
    </style>
    <script>
        'use strict';
 
        /**
        * @typedef {Object} CrisisComm
        * @property {string} type
        * @property {string[]} scenario
        * @property {string[]} language
        * @property {string[]} phases
        */
        let table;  // Tabulator instance
 
        const getQueryUrl = query => '/api.php?action=ask&format=json&query=' + encodeURIComponent(query);
        const escapeAttr = text => text ? text.replace(/\W/g, '-') : text;
 
        const PHASES = ['Before', 'During', 'After'];
 
        const TYPE_PROP = 'Crisis Communication Type';
        const PHASE_PROP = 'Disaster Management Phase';
        const SCENARIO_PROP = 'Event type';
        const LANG_PROP = 'Language';
 
        const crisisCommsQuery = '[[Category:Use Cases]]'
            + '|?' + PHASE_PROP
            + '|?' + TYPE_PROP
            + '|?' + LANG_PROP
            + '|?' + SCENARIO_PROP;
 
        async function getInstances() {
            const response = await fetch(getQueryUrl(crisisCommsQuery)).then(res => res.json());
 
            return Object.keys(response.query.results).map(pageTitle => {
                const result = response.query.results[pageTitle];
 
                /** @type {CrisisComm} **/
                const crisisComm = {};
                crisisComm.title = pageTitle;
                crisisComm.url = result.fullurl;
 
                crisisComm[PHASE_PROP] = result.printouts[PHASE_PROP].map(value => value.fulltext);
                crisisComm[TYPE_PROP] = result.printouts[TYPE_PROP].map(value => value.fulltext);
                crisisComm[SCENARIO_PROP] = result.printouts[SCENARIO_PROP].map(value => value.fulltext);
                crisisComm[LANG_PROP] = result.printouts[LANG_PROP].map(value => value.fulltext);
 
                return crisisComm;
            });
        }
 
        function applyFilters(clear) {
            if (!table) return;
 
            // If clear=true, pass empty object to the filter to disable it.
            if (clear) {
                table.setFilter(crisisCommFilter, {});
                return;
            }
 
            /** @type {CrisisComm} **/
            const filterState = {};
 
            const selectedTypes = Array.from(document.querySelectorAll('#type-filter input[type="checkbox"]:checked'))
                .map(checkbox => checkbox.value);
            if (selectedTypes.length > 0) filterState.type = selectedTypes;
 
            const selectedScenarios = Array.from(document.querySelectorAll('#scenario-filter input[type="checkbox"]:checked'))
                .map(checkbox => checkbox.value);
            if (selectedScenarios.length > 0) filterState.scenario = selectedScenarios;
 
            const selectedPhases = Array.from(document.querySelectorAll('#phases-filter input[type="checkbox"]:checked'))
                .map(checkbox => checkbox.value);
            if (selectedPhases.length > 0) filterState.phases = selectedPhases;
 
            const selectedLang = Array.from(document.querySelectorAll('#language-filter input[type="checkbox"]:checked'))
                .map(checkbox => checkbox.value);
            if (selectedLang.length > 0) filterState.language = selectedLang;
 
            table.setFilter(crisisCommFilter, filterState);
        }
 
        /**
        * @param {CrisisComm} crisisComm
        * @param {CrisisComm} filterState
        */
        function crisisCommFilter(crisisComm, 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.
 
            const typeCheck = filterState.type
                ? filterState.type.every(type => crisisComm[TYPE_PROP].includes(type))
                : true;
            const scenarioCheck = filterState.scenario
                ? filterState.scenario.every(event => crisisComm[SCENARIO_PROP].includes(event))
                : true;
            const phaseCheck = filterState.phases
                ? filterState.phases.every(phase => crisisComm[PHASE_PROP].includes(phase))
                : true;
            const langCheck = filterState.language
                ? filterState.language.every(lang => crisisComm[LANG_PROP].includes(lang))
                : true;
            return typeCheck && scenarioCheck && phaseCheck && langCheck;
        }
 
        Promise.all([getInstances()]).then(data => {
            const crisisComms = data[0];
 
            // Collect values from instances to use in the filter.
            const usedTypes = new Set();
            const usedLanguages = new Set();
            const usedScenarios = new Set();
            for (const crisisComm of crisisComms) {
                crisisComm[TYPE_PROP].forEach(type => usedTypes.add(type));
                crisisComm[LANG_PROP].forEach(lang => usedLanguages.add(lang));
                crisisComm[SCENARIO_PROP].forEach(scenario => usedScenarios.add(scenario));
            }
            const typeOptions = [...usedTypes].sort();
            const languageOptions = [...usedLanguages].sort();
            const scenarioOptions = [...usedScenarios].sort();
 
            // Construct filters.
            const typeHtml = typeOptions.reduce((acc, curr) => {
                const identifier = escapeAttr(curr);
                return acc
                    + '<div><input type="checkbox" id="type-filter-' + identifier
                    + '" value="' + curr + '">'
                    + '<label for="type-filter-' + identifier + '">' + curr + '</label></div>'
            }, '');
            document.getElementById('type-filter').innerHTML = typeHtml;
 
            const scenarioHtml = scenarioOptions.reduce((acc, curr) => {
                const identifier = escapeAttr(curr);
                return acc
                    + '<div><input type="checkbox" id="scenario-filter-' + identifier
                    + '" value="' + curr + '">'
                    + '<label for="scenario-filter-' + identifier + '">' + curr + '</label></div>'
            }, '');
            document.getElementById('scenario-filter').innerHTML = scenarioHtml;
 
            let phaseHtml = PHASES.reduce((acc, curr) => {
                const identifier = escapeAttr(curr);
                return acc
                    + '<div><input type="checkbox" id="phases-filter-' + identifier
                    + '" value="' + curr + '">'
                    + '<label for="phases-filter-' + identifier + '">' + curr + '</label></div>'
            }, '');
            document.getElementById('phases-filter').innerHTML = phaseHtml;
 
            const tabulator = new Tabulator('#tabulator-table', {
                data: crisisComms,
                layout: 'fitDataFill',
                responsiveLayout: 'collapse',
                columns: [
                    {
                        title: 'Title',
                        field: 'title',
                        cssClass: 'usecase-title',
                        formatter: cell => '<a href="' + cell.getData().url + '">' + cell.getValue() + '</a>',
                        minWidth: '800px'
                    },
                    {
                        title: 'Scenario',
                        field: SCENARIO_PROP,
                        formatter: cell => cell.getValue().join(', ') || '&mdash;'
                    },
                    {
                        title: 'Phase',
                        field: PHASE_PROP,
                        formatter: cell => cell.getValue().join(', ')
                    }
 
                ],
                initialSort: [
                    { column: 'title', dir: 'asc' }
                ]
            });
 
            tabulator.on('tableBuilt', () => {
                tabulator.redraw(true);
                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) {
                        const type = filter[TYPE_PROP];
                        if (type) {
                            Object.keys(type).forEach(tp => {
                                const box = document.getElementById('type-filter-' + escapeAttr(tp));
                                box.checked = !!type[tp];
                                box.dispatchEvent(new Event('change', { bubbles: true }));
                            });
                            document.getElementById('type-filter').closest('.filter-wrapper').classList.toggle('open');
                        }
 
                        applyFilters();
                        toggleFilter();
                    }
 
                    // Further actions (e.g. open filter panel, etc.)
                    // ...
                }
 
            });
 
            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.type &&
                    !filter.type.scenario &&
                    !filter.type.language &&
                    !filter.type.phases
                ) { summary.textContent = 'No filter. Showing all results.'; }
                else {
                    let summaryHtml = '<table>';
                    if (filter.type.type) {
                        summaryHtml += '<tr><td><strong>Type</strong></td><td>'
                            + (filter.type.type.length > 0 ? filter.type.type.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    if (filter.type.scenario) {
                        summaryHtml += '<tr><td><strong>Scenario</strong></td><td>'
                            + (filter.type.scenario.length > 0 ? filter.type.scenario.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    if (filter.type.phases) {
                        summaryHtml += '<tr><td><strong>Phases</strong></td><td>'
                            + (filter.type.phases > 0 ? filter.type.phases.join(', ') : 'none')
                            + '</td></tr>';
                        }
                    if (filter.type.language) {
                        summaryHtml += '<tr><td><strong>Language</strong></td><td>'
                            + (filter.type.language.length > 0 ? filter.type.language.join(', ') : 'none')
                            + '</td></tr>';
                    }
                    summaryHtml += '</table>';
                    summary.innerHTML = summaryHtml;
                }
 
            });
 
            // Listen for changes in filter checkbox state.
 
            document.getElementById('type-filter').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });
            document.getElementById('scenario-filter').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });
            document.getElementById('language-filter').addEventListener('change', event => {
                applyFilters();
            }, { passive: true });
            document.getElementById('phases-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'));
            });
 
            // Close filter pane when clicked outside of it.
            document.body.addEventListener('click', event => {
                const filterPane = document.getElementById('cc-filters');
                if (
                    filterPane.classList.contains('open') &&
                    !filterPane.contains(event.target) &&
                    event.target !== document.querySelector('#filter-bar .large-button')
                ) { filterPane.classList.remove('open'); }
            }, { passive: true });
 
            // Fix bug where the table is truncated to zero height despite having visible rows.
            // TODO: Check if this bugfix is still necessary.
            tabulator.on('renderComplete', function () {
                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('#cc-filters input[type="checkbox"]').forEach(checkbox => checkbox.checked = checkbox.defaultChecked);
            applyFilters(true);
        }
 
        function toggleFilter() {
            document.getElementById('cc-filters').classList.toggle('open');
        }
    </script>
 
    <div id="usecase-list-wrapper">
        <h1>
            <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" overflow="hidden" viewBox="0 0 96 96">
                <defs>
                    <clipPath id="uc">
                        <path d="M592 312h96v96h-96z" />
                    </clipPath>
                </defs>
                <g clip-path="url(#uc)" transform="translate(-592 -312)">
                    <path d="m648.783 351.102 1.99-.204 2.359 23.001-1.989.204Z" />
                    <path
                        d="M674 387h-6v-5h-8.093l-3.531-36.291A2.99 2.99 0 0 0 653.39 343h-26.658a2.991 2.991 0 0 0-2.985 2.7L620.1 382H612v5h-6v7h68Zm-48.263-41.1a1 1 0 0 1 .995-.9h26.658a.999.999 0 0 1 1 .9l3.51 36.1h-35.795ZM672 392h-64v-3h6v-5h52v5h6ZM639 326h2v12h-2ZM606 355h10v2h-10ZM664 355h10v2h-10ZM611.293 332.707l1.414-1.414 8 8-1.414 1.414ZM659.293 339.293l8-8 1.414 1.414-8 8Z" />
                </g>
            </svg>
            <div>
                <div>Use Cases</div>
                <div style="font-size:small; letter-spacing:.03em; margin-left: .35em;">Social Media and Crowdsourcing
                    Library</div>
            </div>
        </h1>
 
        <!-- <div id="intro">
            <p>The overall goal of the Social Media and Crowdsourcing (SMCS) Use Cases Library is to collect experiences
                and use cases of how SMCS have been used or can be used in real world. This enables the opportunity to
                give disaster management organisations a concrete indication of how they can use SMCS in practice.</p>
        </div> -->
 
        <!-- FILTERS -->
        <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" 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:Use_Cases" style="font-size: 1.5em; font-variant:small-caps; color: var(--links-cyan)">Add new use case</a></div>
            </div>
 
            <div id="cc-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">Type <div class="plus icon"></div></h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#type-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#type-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="type-filter"></div>
                    </div>
                </div>
 
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Language <div class="plus icon"></div></h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#language-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#language-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="language-filter"></div>
                    </div>
                </div>
 
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Scenario <div class="plus icon"></div></h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#scenario-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#scenario-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="scenario-filter"></div>
                    </div>
                </div>
 
                <div class="filter-wrapper">
                    <h4 class="filter-toggle">Disaster Management Phase <div class="plus icon"></div></h4>
                    <div class="filter-container">
                        <div class="filter-button-wrapper">
                            <button type="button" onclick="selectAll('#phases-filter')">Select all</button> |
                            <button type="button" onclick="deselectAll('#phases-filter')">Clear all</button>
                        </div>
                        <div class="filter-content" id="phases-filter"></div>
                    </div>
                </div>
            </div>
        </div>
 
        <div id="tabulator-table"></div>
    </div>
 
</includeonly>

Revision as of 14:47, 8 September 2023

Current version of the Crisis Communication Library.
Currently in use – do not modify!