ÿØÿà JFIF ÿÛ „ ( %"1!%)+...383,7(-.+
-+++--++++---+-+-----+---------------+---+-++7-----ÿÀ ß â" ÿÄ ÿÄ H !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ ÿÄ % !1AQa"23‘ÿÚ ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6
öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ
"Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷ó²·˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%Ìòh´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ÇýÊTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆÑªQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»&
î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$ËÚsäÿ
÷Û #°xŸëí(l
»ý3—¥5m!
rt`†0~'j2(]S¦¦kv,ÚÇl¦øJA£Šƒ
J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉä¢mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg
A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD
é©¤&‡ïDbàÁôMÁ.// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
/**
* Question bank filter management.
*
* @module core_question/filter
* @copyright 2021 Tomo Tsuyuki
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import CoreFilter from 'core/datafilter';
import Notification from 'core/notification';
import Selectors from 'core/datafilter/selectors';
import Templates from 'core/templates';
import Fragment from 'core/fragment';
/**
* Initialise the question bank filter on the element with the given id.
*
* @param {String} filterRegionId ID of the HTML element containing the filters.
* @param {String} defaultcourseid Course ID for the default course to pass back to the view.
* @param {String} defaultcategoryid Question bank category ID for the default course to pass back to the view.
* @param {Number} perpage The number of questions to display per page.
* @param {Number} contextId Context ID of the question bank view.
* @param {string} component Frankenstyle name of the component for the fragment API callback (e.g. core_question)
* @param {string} callback Name of the callback for the fragment API (e.g question_data)
* @param {string} view The class name of the question bank view class used for this page.
* @param {Number} cmid If we are in an activitiy, the course module ID.
* @param {string} pagevars JSON-encoded parameters from passed from the view, including filters and jointype.
* @param {string} extraparams JSON-encoded additional parameters specific to this view class, used for re-rendering the view.
*/
export const init = (
filterRegionId,
defaultcourseid,
defaultcategoryid,
perpage,
contextId,
component,
callback,
view,
cmid,
pagevars,
extraparams
) => {
const SELECTORS = {
QUESTION_CONTAINER_ID: '#questionscontainer',
QUESTION_TABLE: '#questionscontainer table',
SORT_LINK: '#questionscontainer div.sorters a',
PAGINATION_LINK: '#questionscontainer a[href].page-link',
LASTCHANGED_FIELD: '#questionsubmit input[name=lastchanged]',
BULK_ACTIONS: '#bulkactionsui-container input',
MENU_ACTIONS: '.menu-action',
EDIT_SWITCH: '.editmode-switch-form input[name=setmode]',
EDIT_SWITCH_URL: '.editmode-switch-form input[name=pageurl]',
};
const filterSet = document.querySelector(`#${filterRegionId}`);
const viewData = {
extraparams,
cmid,
view,
cat: defaultcategoryid,
courseid: defaultcourseid,
filter: {},
jointype: 0,
qpage: 0,
qperpage: perpage,
sortdata: {},
lastchanged: document.querySelector(SELECTORS.LASTCHANGED_FIELD)?.value ?? null,
};
let sortData = {};
const defaultSort = document.querySelector(SELECTORS.QUESTION_TABLE)?.dataset?.defaultsort;
if (defaultSort) {
sortData = JSON.parse(defaultSort);
}
/**
* Retrieve table data.
*
* @param {Object} filterdata data
* @param {Promise} pendingPromise pending promise
*/
const applyFilter = (filterdata, pendingPromise) => {
// Reload the questions based on the specified filters. If no filters are provided,
// use the default category filter condition.
if (filterdata) {
// Main join types.
viewData.jointype = parseInt(filterSet.dataset.filterverb, 10);
delete filterdata.jointype;
// Retrieve filter info.
viewData.filter = filterdata;
if (Object.keys(filterdata).length !== 0) {
if (!isNaN(viewData.jointype)) {
filterdata.jointype = viewData.jointype;
}
updateUrlParams(filterdata);
}
}
// Load questions for first page.
viewData.filter = JSON.stringify(filterdata);
viewData.sortdata = JSON.stringify(sortData);
Fragment.loadFragment(component, callback, contextId, viewData)
// Render questions for first page and pagination.
.then((questionhtml, jsfooter) => {
const questionscontainer = document.querySelector(SELECTORS.QUESTION_CONTAINER_ID);
if (questionhtml === undefined) {
questionhtml = '';
}
if (jsfooter === undefined) {
jsfooter = '';
}
Templates.replaceNode(questionscontainer, questionhtml, jsfooter);
// Resolve filter promise.
if (pendingPromise) {
pendingPromise.resolve();
}
return {questionhtml, jsfooter};
})
.catch(Notification.exception);
};
// Init core filter processor with apply callback.
const coreFilter = new CoreFilter(filterSet, applyFilter);
coreFilter.activeFilters = {}; // Unset useless courseid filter.
coreFilter.init();
/**
* Update URL Param based upon the current filter.
*
* @param {Object} filters Active filters.
*/
const updateUrlParams = (filters) => {
const url = new URL(location.href);
const filterQuery = JSON.stringify(filters);
url.searchParams.set('filter', filterQuery);
history.pushState(filters, '', url);
const editSwitch = document.querySelector(SELECTORS.EDIT_SWITCH);
if (editSwitch) {
const editSwitchUrlInput = document.querySelector(SELECTORS.EDIT_SWITCH_URL);
const editSwitchUrl = new URL(editSwitchUrlInput.value);
editSwitchUrl.searchParams.set('filter', filterQuery);
editSwitchUrlInput.value = editSwitchUrl;
editSwitch.dataset.pageurl = editSwitchUrl;
}
};
/**
* Cleans URL parameters.
*/
const cleanUrlParams = () => {
const queryString = location.search;
const urlParams = new URLSearchParams(queryString);
if (urlParams.has('cmid')) {
const cleanedUrl = new URL(location.href.replace(location.search, ''));
cleanedUrl.searchParams.set('cmid', urlParams.get('cmid'));
history.pushState({}, '', cleanedUrl);
}
if (urlParams.has('courseid')) {
const cleanedUrl = new URL(location.href.replace(location.search, ''));
cleanedUrl.searchParams.set('courseid', urlParams.get('courseid'));
history.pushState({}, '', cleanedUrl);
}
};
// Add listeners for the sorting, paging and clear actions.
document.addEventListener('click', e => {
const sortableLink = e.target.closest(SELECTORS.SORT_LINK);
const paginationLink = e.target.closest(SELECTORS.PAGINATION_LINK);
const clearLink = e.target.closest(Selectors.filterset.actions.resetFilters);
if (sortableLink) {
e.preventDefault();
const oldSort = sortData;
sortData = {};
sortData[sortableLink.dataset.sortname] = sortableLink.dataset.sortorder;
for (const sortname in oldSort) {
if (sortname !== sortableLink.dataset.sortname) {
sortData[sortname] = oldSort[sortname];
}
}
viewData.qpage = 0;
coreFilter.updateTableFromFilter();
}
if (paginationLink) {
e.preventDefault();
const paginationURL = new URL(paginationLink.getAttribute("href"));
const qpage = paginationURL.searchParams.get('qpage');
if (paginationURL.search !== null) {
viewData.qpage = qpage;
coreFilter.updateTableFromFilter();
}
}
if (clearLink) {
cleanUrlParams();
}
});
// Run apply filter at page load.
pagevars = JSON.parse(pagevars);
let initialFilters;
let jointype = null;
if (pagevars.filter) {
// Load initial filter based on page vars.
initialFilters = pagevars.filter;
if (pagevars.jointype) {
jointype = pagevars.jointype;
}
}
if (Object.entries(initialFilters).length !== 0) {
// Remove the default empty filter row.
const emptyFilterRow = filterSet.querySelector(Selectors.filterset.regions.emptyFilterRow);
if (emptyFilterRow) {
emptyFilterRow.remove();
}
// Add filters.
let rowcount = 0;
for (const urlFilter in initialFilters) {
if (urlFilter === 'jointype') {
jointype = initialFilters[urlFilter];
continue;
}
// Add each filter row.
rowcount += 1;
const filterdata = {
filtertype: urlFilter,
values: initialFilters[urlFilter].values,
jointype: initialFilters[urlFilter].jointype,
filteroptions: initialFilters[urlFilter].filteroptions,
rownum: rowcount
};
coreFilter.addFilterRow(filterdata);
}
coreFilter.filterSet.dataset.filterverb = jointype;
// Since we must filter by category, it does not make sense to allow the top-level "match any" or "match none" conditions,
// as this would exclude the category. Remove those options and disable the select.
const join = coreFilter.filterSet.querySelector(Selectors.filterset.fields.join);
join.querySelectorAll(`option:not([value="${jointype}"])`).forEach((option) => option.remove());
join.disabled = true;
}
};