ÿØÿà 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ĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"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; } };