Widget: CrisisCommunicationList: Difference between revisions

From LINKS Community Center
Jump to: navigation, search
Eschmidt (talk | contribs)
No edit summary
Marterer (talk | contribs)
No edit summary
 
(4 intermediate revisions by the same user not shown)
Line 4: Line 4:
     <link href="/resources/assets/tabulator/dist/css/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/dist/js/tabulator.min.js"></script>
     <script type="text/javascript" src="/resources/assets/tabulator/dist/js/tabulator.min.js"></script>
    <style>
        #cc-list-wrapper {
            font-family: 'Open Sans';
            margin-top: 4em;
        }


        #cc-list-wrapper h1,
    <!-- STYLES BEGIN -->
        #cc-list-wrapper h2,
    <link rel="stylesheet" href="https://assets.links.communitycenter.eu/v2/links/lib?q=cc_list.css">
        #cc-list-wrapper h3,
    <!-- STYLES END -->
        #cc-list-wrapper h4 {
            font-family: 'Raleway';
            font-weight: 300;
            letter-spacing: .06em;
        }


        #cc-list-wrapper h1,
     <!-- SCRIPT BEGIN -->
        #usecase-filters h2 {
    <script type="text/javascript" src="https://assets.links.communitycenter.eu/v2/links/lib?q=cc_list.js"></script>
            color: var(--ccl-color);
    <!-- SCRIPT END -->
            display: flex;
            align-items: center;
        }
 
        #cc-list-wrapper h1 svg {
            height: 2.5em;
            width: 2.5em;
            fill: var(--ccl-color);
            margin-right: .5em;
        }
 
        #cc-list-wrapper h2 {
            margin-bottom: 1em;
        }
 
        #filter-bar {
            position: relative;
            margin: 1em 0 2em 0;
        }
 
        #cc-filters {
            position: absolute;
            top: 0;
            right: 0;
            z-index: 100;
            padding: 2em;
            background: #fff;
            border: 1px solid var(--ccl-color);
            clip-path: inset(0 0 100% 100%);
            box-shadow: -10px 10px 10px 5px rgb(0 0 0 / 10%);
            transition: all 400ms ease-in-out;
        }
 
        @media only screen and (orientation: landscape) {
            #cc-filters {
                width: 45vw;
            }
        }
 
        #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(--ccl-color);
        }
 
        #cc-filters h2 svg {
            height: 1.5em;
            width: 1.5em;
            margin-right: .5em;
            margin-left: -.2em;
        }
 
        .large-button {
            border: 1px solid var(--ccl-color);
            font-size: 1.5em;
            font-family: 'Open Sans';
            font-weight: 100;
            margin-bottom: 2em;
            padding: 0.3em 0.8em;
            color: var(--ccl-color);
            background: transparent;
            font-variant: small-caps;
            display: inline-block;
            transition: all 200ms ease-in-out;
        }
 
        .large-button:hover {
            background-color: var(--ccl-color);
            color: #fff;
        }
 
        .filter-button-wrapper {
            margin-bottom: 1em;
        }
 
        .filter-wrapper button {
            border: 0 none;
            color: var(--ccl-color);
            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(--ccl-color);
            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(--ccl-color);
            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(--ccl-color);
        }
     </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:Crisis Communication]]'
            + '|limit=500'
            + '|?' + 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[TYPE_PROP] = result.printouts[TYPE_PROP][0];
                crisisComm[PHASE_PROP] = result.printouts[PHASE_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
                ? crisisComm[SCENARIO_PROP].some(event => filterState.scenario.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) {
                usedTypes.add(crisisComm[TYPE_PROP]);
                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;
 
            let langHtml = languageOptions.reduce((acc, curr) => {
                const identifier = escapeAttr(curr);
                return acc
                    + '<div><input type="checkbox" id="language-filter-' + identifier
                    + '" value="' + curr + '">'
                    + '<label for="language-filter-' + identifier + '">' + curr + '</label></div>'
            }, '');
            document.getElementById('language-filter').innerHTML = langHtml;
 
            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: '1000px'
                    },
                    {
                        title: 'Scenario',
                        field: SCENARIO_PROP,
                        formatter: cell => cell.getValue().join(', ') || '&mdash;'
                    },
                    {
                        title: 'Phase',
                        field: PHASE_PROP,
                        formatter: cell => cell.getValue().join(', ')
                    },
                    {
                        title: 'Language',
                        field: LANG_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="cc-list-wrapper">
     <div id="cc-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://assets.links.communitycenter.eu/v2/links/lib?q=4-cc_list.svg">
                <path
                    d="M78.6 17.005H17.4a3.436 3.436 0 0 0-3.4 3.4v41.312a3.367 3.367 0 0 0 3.34 3.4H54.8l13.6 13.9v-13.8h10.2a3.436 3.436 0 0 0 3.4-3.4V20.506a3.463 3.463 0 0 0-3.4-3.501ZM80 61.817c-.02.765-.64 1.38-1.4 1.4H66.4v10.9L56.23 63.72l-.59-.6H17.4a1.37 1.37 0 0 1-1.4-1.339V20.406c.02-.765.64-1.38 1.4-1.4h61.2c.8.03 1.43.7 1.4 1.5Z" />
                <path d="M46.88 27h2.25v21h-2.25ZM50 53.25c0 1.105-.9 2-2 2s-2-.895-2-2 .9-2 2-2 2 .895 2 2Z" />
            </svg>
             <div>
             <div>
                 <div>Crisis Communication</div>
                 <div>Crisis Communication</div>
                 <div style="font-size:small; letter-spacing:.03em; margin-left: .35em;">Social Media and Crowdsourcing
                 <div style="font-size:small; letter-spacing:.03em; margin-left: .35em;">Social Media and Crowdsourcing Library</div>
                    Library</div>
             </div>
             </div>
         </h1>
         </h1>
Line 602: Line 27:
                 its effects on people. The purpose of this library is to ease communication with the public via Social Media
                 its effects on people. The purpose of this library is to ease communication with the public via Social Media
                 text messages taken from trustful resources.
                 text messages taken from trustful resources.
       
             </p>
             </p>
             <p>
             <p>
Line 631: Line 55:
                 <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"
                         <img src="https://assets.links.communitycenter.eu/v2/links/lib?q=filters.svg">
                            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>
                         <span>Filters</span>
                     </div>
                     </div>

Latest revision as of 09:50, 2 October 2024

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