Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Question bank filter management.
18
 *
19
 * @module     core_question/filter
20
 * @copyright  2021 Tomo Tsuyuki <tomotsuyuki@catalyst-au.net>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import CoreFilter from 'core/datafilter';
25
import Notification from 'core/notification';
26
import Selectors from 'core/datafilter/selectors';
27
import Templates from 'core/templates';
28
import Fragment from 'core/fragment';
1441 ariadna 29
import {getString} from 'core/str';
30
import {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';
1 efrain 31
 
32
/**
33
 * Initialise the question bank filter on the element with the given id.
34
 *
35
 * @param {String} filterRegionId ID of the HTML element containing the filters.
36
 * @param {String} defaultcourseid Course ID for the default course to pass back to the view.
37
 * @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.
38
 * @param {Number} perpage The number of questions to display per page.
1441 ariadna 39
 * @param {Number} bankContextId Context ID of the question bank being filtered.
40
 * @param {Number} quizCmId Course module ID of the quiz as the viewing context.
1 efrain 41
 * @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)
42
 * @param {string} callback Name of the callback for the fragment API (e.g question_data)
43
 * @param {string} view The class name of the question bank view class used for this page.
44
 * @param {Number} cmid If we are in an activitiy, the course module ID.
1441 ariadna 45
 * @param {Object} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.
46
 * @param {Object} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.
1 efrain 47
 */
1441 ariadna 48
export const init = async(
1 efrain 49
    filterRegionId,
50
    defaultcourseid,
51
    defaultcategoryid,
52
    perpage,
1441 ariadna 53
    bankContextId,
54
    quizCmId,
1 efrain 55
    component,
56
    callback,
57
    view,
58
    cmid,
59
    pagevars,
1441 ariadna 60
    extraparams,
1 efrain 61
) => {
62
 
63
    const SELECTORS = {
64
        QUESTION_CONTAINER_ID: '#questionscontainer',
65
        QUESTION_TABLE: '#questionscontainer table',
66
        SORT_LINK: '#questionscontainer div.sorters a',
67
        PAGINATION_LINK: '#questionscontainer a[href].page-link',
68
        LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',
69
        BULK_ACTIONS: '#bulkactionsui-container input',
70
        MENU_ACTIONS: '.menu-action',
71
        EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',
72
        EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',
1441 ariadna 73
        SHOW_ALL_LINK: '[data-filteraction="showall"]',
1 efrain 74
    };
75
 
76
    const filterSet = document.querySelector(`#${filterRegionId}`);
77
 
78
    const viewData = {
1441 ariadna 79
        extraparams: JSON.stringify(extraparams),
1 efrain 80
        cmid,
81
        view,
82
        cat: defaultcategoryid,
83
        courseid: defaultcourseid,
84
        filter: {},
85
        jointype: 0,
86
        qpage: 0,
87
        qperpage: perpage,
88
        sortdata: {},
89
        lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null,
90
    };
91
 
92
    let sortData = {};
93
    const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;
94
    if (defaultSort) {
95
        sortData = JSON.parse(defaultSort);
96
    }
97
 
1441 ariadna 98
    const [
99
        showAllText,
100
        showPerPageText,
101
    ] = await Promise.all([
102
        getString('showall', 'core', ''),
103
        getString('showperpage', 'core', extraparams.defaultqperpage),
104
    ]);
105
 
1 efrain 106
    /**
107
     * Retrieve table data.
108
     *
109
     * @param {Object} filterdata data
110
     * @param {Promise} pendingPromise pending promise
111
     */
112
    const applyFilter = (filterdata, pendingPromise) => {
113
        // Reload the questions based on the specified filters. If no filters are provided,
114
        // use the default category filter condition.
115
        if (filterdata) {
116
            // Main join types.
117
            viewData.jointype = parseInt(filterSet.dataset.filterverb, 10);
118
            delete filterdata.jointype;
119
            // Retrieve filter info.
120
            viewData.filter = filterdata;
121
            if (Object.keys(filterdata).length !== 0) {
122
                if (!isNaN(viewData.jointype)) {
123
                    filterdata.jointype = viewData.jointype;
124
                }
125
            }
126
        }
127
        // Load questions for first page.
128
        viewData.filter = JSON.stringify(filterdata);
129
        viewData.sortdata = JSON.stringify(sortData);
1441 ariadna 130
        viewData.quizcmid = quizCmId;
131
 
132
        const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);
133
        // Clear the contents of the element, then append the loading icon.
134
        questionscontainer.innerHTML = '';
135
        addIconToContainerRemoveOnCompletion(questionscontainer, pendingPromise);
136
 
137
        Fragment.loadFragment(component, callback, bankContextId, viewData)
1 efrain 138
            // Render questions for first page and pagination.
139
            .then((questionhtml, jsfooter) => {
1441 ariadna 140
                updateUrlParams(filterdata);
1 efrain 141
                if (questionhtml === undefined) {
142
                    questionhtml = '';
143
                }
144
                if (jsfooter === undefined) {
145
                    jsfooter = '';
146
                }
147
                Templates.replaceNode(questionscontainer, questionhtml, jsfooter);
148
                // Resolve filter promise.
149
                if (pendingPromise) {
150
                    pendingPromise.resolve();
151
                }
152
                return {questionhtml, jsfooter};
153
            })
154
            .catch(Notification.exception);
155
    };
156
 
157
    // Init core filter processor with apply callback.
158
    const coreFilter = new CoreFilter(filterSet, applyFilter);
159
    coreFilter.activeFilters = {}; // Unset useless courseid filter.
160
    coreFilter.init();
161
 
162
    /**
163
     * Update URL Param based upon the current filter.
164
     *
165
     * @param {Object} filters Active filters.
166
     */
167
    const updateUrlParams = (filters) => {
168
        const url = new URL(location.href);
169
        const filterQuery = JSON.stringify(filters);
170
        url.searchParams.set('filter', filterQuery);
171
        history.pushState(filters, '', url);
172
        const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);
173
        if (editSwitch) {
174
            const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);
175
            const editSwitchUrl = new URL(editSwitchUrlInput.value);
176
            editSwitchUrl.searchParams.set('filter', filterQuery);
177
            editSwitchUrlInput.value = editSwitchUrl;
178
            editSwitch.dataset.pageurl = editSwitchUrl;
179
        }
180
    };
181
 
182
    /**
183
     * Cleans URL parameters.
184
     */
185
    const cleanUrlParams = () => {
186
        const queryString = location.search;
187
        const urlParams = new URLSearchParams(queryString);
188
        if (urlParams.has('cmid')) {
189
            const cleanedUrl = new URL(location.href.replace(location.search, ''));
190
            cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));
191
            history.pushState({}, '', cleanedUrl);
192
        }
193
 
194
        if (urlParams.has('courseid')) {
195
            const cleanedUrl = new URL(location.href.replace(location.search, ''));
196
            cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));
197
            history.pushState({}, '', cleanedUrl);
198
        }
199
    };
200
 
201
    // Add listeners for the sorting, paging and clear actions.
1441 ariadna 202
    document.querySelector('.questionbankwindow').addEventListener('click', e => {
1 efrain 203
        const sortableLink = e.target.closest(SELECTORS.SORT_LINK);
204
        const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);
205
        const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);
1441 ariadna 206
        const showallLink = e.target.closest(SELECTORS.SHOW_ALL_LINK);
1 efrain 207
        if (sortableLink) {
208
            e.preventDefault();
209
            const oldSort = sortData;
210
            sortData = {};
211
            sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
212
            for (const sortname in oldSort) {
213
                if (sortname !== sortableLink.dataset.sortname) {
214
                    sortData[sortname] = oldSort[sortname];
215
                }
216
            }
217
            viewData.qpage = 0;
1441 ariadna 218
            coreFilter.updateTableFromFilter(false);
1 efrain 219
        }
220
        if (paginationLink) {
221
            e.preventDefault();
222
            const paginationURL = new URL(paginationLink.getAttribute("href"));
223
            const qpage = paginationURL.searchParams.get('qpage');
224
            if (paginationURL.search !== null) {
225
                viewData.qpage = qpage;
1441 ariadna 226
                coreFilter.updateTableFromFilter(false);
1 efrain 227
            }
228
        }
229
        if (clearLink) {
230
            cleanUrlParams();
231
        }
1441 ariadna 232
        if (showallLink) {
233
 
234
            e.preventDefault();
235
 
236
            // Toggle between showing all and going back to the original qperpage.
237
            if (Number(showallLink.dataset.status) === 0) {
238
                viewData.qperpage = extraparams.maxqperpage;
239
                showallLink.dataset.status = 1;
240
                showallLink.innerText = showPerPageText;
241
            } else {
242
                viewData.qperpage = extraparams.defaultqperpage;
243
                showallLink.dataset.status = 0;
244
                showallLink.innerText = showAllText;
245
            }
246
            viewData.qpage = 0;
247
            coreFilter.updateTableFromFilter();
248
        }
1 efrain 249
    });
250
 
251
    // Run apply filter at page load.
252
    let initialFilters;
253
    let jointype = null;
254
    if (pagevars.filter) {
255
        // Load initial filter based on page vars.
256
        initialFilters = pagevars.filter;
257
        if (pagevars.jointype) {
258
            jointype = pagevars.jointype;
259
        }
260
    }
261
 
262
    if (Object.entries(initialFilters).length !== 0) {
263
        // Remove the default empty filter row.
264
        const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);
265
        if (emptyFilterRow) {
266
            emptyFilterRow.remove();
267
        }
268
 
269
        // Add filters.
270
        let rowcount = 0;
271
        for (const urlFilter in initialFilters) {
272
            if (urlFilter === 'jointype') {
273
                jointype = initialFilters[urlFilter];
274
                continue;
275
            }
276
            // Add each filter row.
277
            rowcount += 1;
278
            const filterdata = {
279
                filtertype: urlFilter,
280
                values:  initialFilters[urlFilter].values,
281
                jointype: initialFilters[urlFilter].jointype,
282
                filteroptions: initialFilters[urlFilter].filteroptions,
283
                rownum: rowcount
284
            };
285
            coreFilter.addFilterRow(filterdata);
286
        }
287
        coreFilter.filterSet.dataset.filterverb = jointype;
288
 
289
        // Since we must filter by category, it does not make sense to allow the top-level "match any" or "match none" conditions,
290
        // as this would exclude the category. Remove those options and disable the select.
291
        const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);
292
        join.querySelectorAll(`option:not([value="${jointype}"])`).forEach((option) => option.remove());
293
        join.disabled = true;
294
    }
295
};