Proyectos de Subversion Moodle

Rev

| 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';
29
 
30
/**
31
 * Initialise the question bank filter on the element with the given id.
32
 *
33
 * @param {String} filterRegionId ID of the HTML element containing the filters.
34
 * @param {String} defaultcourseid Course ID for the default course to pass back to the view.
35
 * @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.
36
 * @param {Number} perpage The number of questions to display per page.
37
 * @param {Number} contextId Context ID of the question bank view.
38
 * @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)
39
 * @param {string} callback Name of the callback for the fragment API (e.g question_data)
40
 * @param {string} view The class name of the question bank view class used for this page.
41
 * @param {Number} cmid If we are in an activitiy, the course module ID.
42
 * @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.
43
 * @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.
44
 */
45
export const init = (
46
    filterRegionId,
47
    defaultcourseid,
48
    defaultcategoryid,
49
    perpage,
50
    contextId,
51
    component,
52
    callback,
53
    view,
54
    cmid,
55
    pagevars,
56
    extraparams
57
) => {
58
 
59
    const SELECTORS = {
60
        QUESTION_CONTAINER_ID: '#questionscontainer',
61
        QUESTION_TABLE: '#questionscontainer table',
62
        SORT_LINK: '#questionscontainer div.sorters a',
63
        PAGINATION_LINK: '#questionscontainer a[href].page-link',
64
        LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',
65
        BULK_ACTIONS: '#bulkactionsui-container input',
66
        MENU_ACTIONS: '.menu-action',
67
        EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',
68
        EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',
69
    };
70
 
71
    const filterSet = document.querySelector(`#${filterRegionId}`);
72
 
73
    const viewData = {
74
        extraparams,
75
        cmid,
76
        view,
77
        cat: defaultcategoryid,
78
        courseid: defaultcourseid,
79
        filter: {},
80
        jointype: 0,
81
        qpage: 0,
82
        qperpage: perpage,
83
        sortdata: {},
84
        lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null,
85
    };
86
 
87
    let sortData = {};
88
    const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;
89
    if (defaultSort) {
90
        sortData = JSON.parse(defaultSort);
91
    }
92
 
93
    /**
94
     * Retrieve table data.
95
     *
96
     * @param {Object} filterdata data
97
     * @param {Promise} pendingPromise pending promise
98
     */
99
    const applyFilter = (filterdata, pendingPromise) => {
100
        // Reload the questions based on the specified filters. If no filters are provided,
101
        // use the default category filter condition.
102
        if (filterdata) {
103
            // Main join types.
104
            viewData.jointype = parseInt(filterSet.dataset.filterverb, 10);
105
            delete filterdata.jointype;
106
            // Retrieve filter info.
107
            viewData.filter = filterdata;
108
            if (Object.keys(filterdata).length !== 0) {
109
                if (!isNaN(viewData.jointype)) {
110
                    filterdata.jointype = viewData.jointype;
111
                }
112
                updateUrlParams(filterdata);
113
            }
114
        }
115
        // Load questions for first page.
116
        viewData.filter = JSON.stringify(filterdata);
117
        viewData.sortdata = JSON.stringify(sortData);
118
        Fragment.loadFragment(component, callback, contextId, viewData)
119
            // Render questions for first page and pagination.
120
            .then((questionhtml, jsfooter) => {
121
                const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);
122
                if (questionhtml === undefined) {
123
                    questionhtml = '';
124
                }
125
                if (jsfooter === undefined) {
126
                    jsfooter = '';
127
                }
128
                Templates.replaceNode(questionscontainer, questionhtml, jsfooter);
129
                // Resolve filter promise.
130
                if (pendingPromise) {
131
                    pendingPromise.resolve();
132
                }
133
                return {questionhtml, jsfooter};
134
            })
135
            .catch(Notification.exception);
136
    };
137
 
138
    // Init core filter processor with apply callback.
139
    const coreFilter = new CoreFilter(filterSet, applyFilter);
140
    coreFilter.activeFilters = {}; // Unset useless courseid filter.
141
    coreFilter.init();
142
 
143
    /**
144
     * Update URL Param based upon the current filter.
145
     *
146
     * @param {Object} filters Active filters.
147
     */
148
    const updateUrlParams = (filters) => {
149
        const url = new URL(location.href);
150
        const filterQuery = JSON.stringify(filters);
151
        url.searchParams.set('filter', filterQuery);
152
        history.pushState(filters, '', url);
153
        const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);
154
        if (editSwitch) {
155
            const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);
156
            const editSwitchUrl = new URL(editSwitchUrlInput.value);
157
            editSwitchUrl.searchParams.set('filter', filterQuery);
158
            editSwitchUrlInput.value = editSwitchUrl;
159
            editSwitch.dataset.pageurl = editSwitchUrl;
160
        }
161
    };
162
 
163
    /**
164
     * Cleans URL parameters.
165
     */
166
    const cleanUrlParams = () => {
167
        const queryString = location.search;
168
        const urlParams = new URLSearchParams(queryString);
169
        if (urlParams.has('cmid')) {
170
            const cleanedUrl = new URL(location.href.replace(location.search, ''));
171
            cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));
172
            history.pushState({}, '', cleanedUrl);
173
        }
174
 
175
        if (urlParams.has('courseid')) {
176
            const cleanedUrl = new URL(location.href.replace(location.search, ''));
177
            cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));
178
            history.pushState({}, '', cleanedUrl);
179
        }
180
    };
181
 
182
    // Add listeners for the sorting, paging and clear actions.
183
    document.addEventListener('click', e => {
184
        const sortableLink = e.target.closest(SELECTORS.SORT_LINK);
185
        const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);
186
        const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);
187
        if (sortableLink) {
188
            e.preventDefault();
189
            const oldSort = sortData;
190
            sortData = {};
191
            sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
192
            for (const sortname in oldSort) {
193
                if (sortname !== sortableLink.dataset.sortname) {
194
                    sortData[sortname] = oldSort[sortname];
195
                }
196
            }
197
            viewData.qpage = 0;
198
            coreFilter.updateTableFromFilter();
199
        }
200
        if (paginationLink) {
201
            e.preventDefault();
202
            const paginationURL = new URL(paginationLink.getAttribute("href"));
203
            const qpage = paginationURL.searchParams.get('qpage');
204
            if (paginationURL.search !== null) {
205
                viewData.qpage = qpage;
206
                coreFilter.updateTableFromFilter();
207
            }
208
        }
209
        if (clearLink) {
210
            cleanUrlParams();
211
        }
212
    });
213
 
214
    // Run apply filter at page load.
215
    pagevars = JSON.parse(pagevars);
216
    let initialFilters;
217
    let jointype = null;
218
    if (pagevars.filter) {
219
        // Load initial filter based on page vars.
220
        initialFilters = pagevars.filter;
221
        if (pagevars.jointype) {
222
            jointype = pagevars.jointype;
223
        }
224
    }
225
 
226
    if (Object.entries(initialFilters).length !== 0) {
227
        // Remove the default empty filter row.
228
        const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);
229
        if (emptyFilterRow) {
230
            emptyFilterRow.remove();
231
        }
232
 
233
        // Add filters.
234
        let rowcount = 0;
235
        for (const urlFilter in initialFilters) {
236
            if (urlFilter === 'jointype') {
237
                jointype = initialFilters[urlFilter];
238
                continue;
239
            }
240
            // Add each filter row.
241
            rowcount += 1;
242
            const filterdata = {
243
                filtertype: urlFilter,
244
                values:  initialFilters[urlFilter].values,
245
                jointype: initialFilters[urlFilter].jointype,
246
                filteroptions: initialFilters[urlFilter].filteroptions,
247
                rownum: rowcount
248
            };
249
            coreFilter.addFilterRow(filterdata);
250
        }
251
        coreFilter.filterSet.dataset.filterverb = jointype;
252
 
253
        // Since we must filter by category, it does not make sense to allow the top-level "match any" or "match none" conditions,
254
        // as this would exclude the category. Remove those options and disable the select.
255
        const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);
256
        join.querySelectorAll(`option:not([value="${jointype}"])`).forEach((option) => option.remove());
257
        join.disabled = true;
258
    }
259
};