MDL-72321 core: Add binary datafilter

This adds a new datafilter type which provides a single binary choice
(for example yes/no).
This commit is contained in:
Nathan Nguyen 2022-09-16 12:54:06 +10:00 committed by Mark Johnson
parent e81a8381a8
commit ef8c11328b
12 changed files with 189 additions and 16 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
define("core/datafilter/filtertypes/binary",["exports","core/datafilter/filtertype","core/datafilter/selectors","core/templates","core/str"],(function(_exports,_filtertype,_selectors,_templates,_str){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=_interopRequireDefault(_filtertype),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates);class _default extends _filtertype.default{constructor(){super(...arguments),_defineProperty(this,"optionOne",void 0),_defineProperty(this,"optionTwo",void 0)}async addValueSelector(initialValues){return[this.optionOne,this.optionTwo]=await this.getTextValues(),this.displayBinarySelection(initialValues)}getTextValues(){return(0,_str.get_strings)([{key:"no"},{key:"yes"}])}async displayBinarySelection(){let initialValues=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const specificFilterSet=this.rootNode.querySelector(_selectors.default.filter.byName(this.filterType)),sourceDataNode=this.getSourceDataForFilter(),context={filtertype:this.filterType,title:sourceDataNode.getAttribute("data-field-title"),required:sourceDataNode.dataset.required,options:[{text:this.optionOne,value:0,selected:0===initialValues[0]},{text:this.optionTwo,value:1,selected:1===initialValues[0]}]};return _templates.default.render("core/datafilter/filtertypes/binary_selector",context).then(((binaryUi,js)=>_templates.default.replaceNodeContents(specificFilterSet.querySelector(_selectors.default.filter.regions.values),binaryUi,js)))}get values(){return this.filterRoot.querySelector('[data-filterfield="'.concat(this.name,'"]')).value}}return _exports.default=_default,_exports.default}));
//# sourceMappingURL=binary.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,6 @@ define("core/datafilter/selectors",["exports"],(function(_exports){Object.define
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const getFilterRegion=region=>'[data-filterregion="'.concat(region,'"]'),getFilterAction=action=>'[data-filteraction="'.concat(action,'"]'),getFilterField=field=>'[data-filterfield="'.concat(field,'"]');var _default={filter:{region:getFilterRegion("filter"),actions:{remove:getFilterAction("remove")},fields:{join:getFilterField("join"),type:getFilterField("type")},regions:{values:getFilterRegion("value")},byName:name=>"".concat(getFilterRegion("filter"),'[data-filter-type="').concat(name,'"]')},filterset:{region:getFilterRegion("actions"),actions:{addRow:getFilterAction("add"),applyFilters:getFilterAction("apply"),resetFilters:getFilterAction("reset")},regions:{filtermatch:getFilterRegion("filtermatch"),filterlist:getFilterRegion("filters"),datasource:getFilterRegion("filtertypedata")},fields:{join:"".concat(getFilterRegion("filtermatch")," ").concat(getFilterField("join"))}},data:{fields:{byName:name=>'[data-field-name="'.concat(name,'"]'),all:"".concat(getFilterRegion("filtertypedata")," [data-field-name]")},typeList:getFilterRegion("filtertypelist"),typeListSelect:"select".concat(getFilterRegion("filtertypelist")),required:"".concat(getFilterRegion("value"),' > [data-required="1"]')}};return _exports.default=_default,_exports.default}));
const getFilterRegion=region=>'[data-filterregion="'.concat(region,'"]'),getFilterAction=action=>'[data-filteraction="'.concat(action,'"]'),getFilterField=field=>'[data-filterfield="'.concat(field,'"]');var _default={filter:{region:getFilterRegion("filter"),actions:{remove:getFilterAction("remove")},fields:{join:getFilterField("join"),type:getFilterField("type")},regions:{values:getFilterRegion("value")},byName:name=>"".concat(getFilterRegion("filter"),'[data-filter-type="').concat(name,'"]')},filterset:{region:getFilterRegion("actions"),actions:{addRow:getFilterAction("add"),applyFilters:getFilterAction("apply"),resetFilters:getFilterAction("reset")},regions:{filtermatch:getFilterRegion("filtermatch"),filterlist:getFilterRegion("filters"),datasource:getFilterRegion("filtertypedata"),emptyFilterRow:"".concat(getFilterRegion("filter"),'[data-filter-type=""]')},fields:{join:"".concat(getFilterRegion("filtermatch")," ").concat(getFilterField("join"))}},data:{fields:{byName:name=>'[data-field-name="'.concat(name,'"]'),all:"".concat(getFilterRegion("filtertypedata")," [data-field-name]")},typeList:getFilterRegion("filtertypelist"),typeListSelect:"select".concat(getFilterRegion("filtertypelist")),required:"".concat(getFilterRegion("value"),' > [data-required="1"]')}};return _exports.default=_default,_exports.default}));
//# sourceMappingURL=selectors.min.js.map

View File

@ -1 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../../src/datafilter/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for user filters.\n *\n * @module core/datafilter/selectors\n * @copyright 2020 Michael Hawkins <michaelh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filtermatch: getFilterRegion('filtermatch'),\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n },\n fields: {\n join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n all: `${getFilterRegion('filtertypedata')} [data-field-name]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n typeListSelect: `select${getFilterRegion('filtertypelist')}`,\n required: `${getFilterRegion('value')} > [data-required=\"1\"]`,\n },\n};\n"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filtermatch","filterlist","datasource","data","all","typeList","typeListSelect","required"],"mappings":";;;;;;;;MAuBMA,gBAAkBC,sCAAiCA,aACnDC,gBAAkBC,sCAAiCA,aACnDC,eAAiBC,oCAA+BA,yBAEvC,CACXC,OAAQ,CACJL,OAAQD,gBAAgB,UACxBO,QAAS,CACLC,OAAQN,gBAAgB,WAE5BO,OAAQ,CACJC,KAAMN,eAAe,QACrBO,KAAMP,eAAe,SAEzBQ,QAAS,CACLC,OAAQb,gBAAgB,UAE5Bc,OAAQC,gBAAWf,gBAAgB,wCAA+Be,YAEtEC,UAAW,CACPf,OAAQD,gBAAgB,WACxBO,QAAS,CACLU,OAAQf,gBAAgB,OACxBgB,aAAchB,gBAAgB,SAC9BiB,aAAcjB,gBAAgB,UAElCU,QAAS,CACLQ,YAAapB,gBAAgB,eAC7BqB,WAAYrB,gBAAgB,WAC5BsB,WAAYtB,gBAAgB,mBAEhCS,OAAQ,CACJC,eAASV,gBAAgB,2BAAkBI,eAAe,WAGlEmB,KAAM,CACFd,OAAQ,CACJK,OAAQC,kCAA6BA,WACrCS,cAAQxB,gBAAgB,yCAE5ByB,SAAUzB,gBAAgB,kBAC1B0B,+BAAyB1B,gBAAgB,mBACzC2B,mBAAa3B,gBAAgB"}
{"version":3,"file":"selectors.min.js","sources":["../../src/datafilter/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for user filters.\n *\n * @module core/datafilter/selectors\n * @copyright 2020 Michael Hawkins <michaelh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filtermatch: getFilterRegion('filtermatch'),\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n emptyFilterRow: `${getFilterRegion('filter')}[data-filter-type=\"\"]`,\n },\n fields: {\n join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n all: `${getFilterRegion('filtertypedata')} [data-field-name]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n typeListSelect: `select${getFilterRegion('filtertypelist')}`,\n required: `${getFilterRegion('value')} > [data-required=\"1\"]`,\n },\n};\n"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filtermatch","filterlist","datasource","emptyFilterRow","data","all","typeList","typeListSelect","required"],"mappings":";;;;;;;;MAuBMA,gBAAkBC,sCAAiCA,aACnDC,gBAAkBC,sCAAiCA,aACnDC,eAAiBC,oCAA+BA,yBAEvC,CACXC,OAAQ,CACJL,OAAQD,gBAAgB,UACxBO,QAAS,CACLC,OAAQN,gBAAgB,WAE5BO,OAAQ,CACJC,KAAMN,eAAe,QACrBO,KAAMP,eAAe,SAEzBQ,QAAS,CACLC,OAAQb,gBAAgB,UAE5Bc,OAAQC,gBAAWf,gBAAgB,wCAA+Be,YAEtEC,UAAW,CACPf,OAAQD,gBAAgB,WACxBO,QAAS,CACLU,OAAQf,gBAAgB,OACxBgB,aAAchB,gBAAgB,SAC9BiB,aAAcjB,gBAAgB,UAElCU,QAAS,CACLQ,YAAapB,gBAAgB,eAC7BqB,WAAYrB,gBAAgB,WAC5BsB,WAAYtB,gBAAgB,kBAC5BuB,yBAAmBvB,gBAAgB,oCAEvCS,OAAQ,CACJC,eAASV,gBAAgB,2BAAkBI,eAAe,WAGlEoB,KAAM,CACFf,OAAQ,CACJK,OAAQC,kCAA6BA,WACrCU,cAAQzB,gBAAgB,yCAE5B0B,SAAU1B,gBAAgB,kBAC1B2B,+BAAyB3B,gBAAgB,mBACzC4B,mBAAa5B,gBAAgB"}

View File

@ -110,13 +110,14 @@ export default class {
}
/**
* Add an unselected filter row.
* Add a filter row.
*
* @param {Object} filterdata Optional, data for adding for row with an existing filter.
* @return {Promise}
*/
addFilterRow() {
addFilterRow(filterdata = {}) {
const pendingPromise = new Pending('core/datafilter:addFilterRow');
const rownum = 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
const rownum = filterdata.rownum ?? 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;
return Templates.renderForPromise('core/datafilter/filter_row', {"rownumber": rownum})
.then(({html, js}) => {
const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);
@ -148,6 +149,13 @@ export default class {
.then(result => {
pendingPromise.resolve();
// If an existing filter is passed in, add it. Otherwise, leave the row empty.
if (filterdata.filtertype) {
result.forEach(filter => {
this.addFilter(filter, filterdata.filtertype, filterdata.values,
filterdata.jointype, filterdata.filteroptions);
});
}
return result;
})
.catch(Notification.exception);
@ -171,9 +179,11 @@ export default class {
* @param {HTMLElement} filterRow
* @param {String} filterType
* @param {Array} initialFilterValues The initially selected values for the filter
* @param {String} filterJoin
* @param {Object} filterOptions
* @returns {Filter}
*/
async addFilter(filterRow, filterType, initialFilterValues) {
async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {
// Name the filter on the filter row.
filterRow.dataset.filterType = filterType;
@ -184,13 +194,12 @@ export default class {
if (filterDataNode.dataset.filterTypeClass) {
Filter = await import(filterDataNode.dataset.filterTypeClass);
}
this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues);
this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);
// Disable the select.
const typeField = filterRow.querySelector(Selectors.filter.fields.type);
typeField.value = filterType;
typeField.disabled = 'disabled';
// Update the list of available filter types.
this.updateFiltersOptions();

View File

@ -0,0 +1,111 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Base filter for binary selector ie: (Yes / No).
*
* @module core/datafilter/filtertypes/binary
* @author 2022 Ghaly Marc-Alexandre <marc-alexandreghaly@catalyst-ca.net>
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Filter from 'core/datafilter/filtertype';
import Selectors from 'core/datafilter/selectors';
import Templates from 'core/templates';
import {get_strings as getStrings} from 'core/str';
export default class extends Filter {
/**
* Text string for the first binary option.
*
* This option (and {@see optionTwo}) are set by {@see getTextValues()}. The base class will set default values,
* a subclass can override the method to define its own option.
*
* @type {String}
*/
optionOne;
/**
* Text string for the second binary option.
*
* @type {String}
*/
optionTwo;
/**
* Add the value selector to the filter row.
*
* @param {Array} initialValues The default value for the filter.
*/
async addValueSelector(initialValues) {
[this.optionOne, this.optionTwo] = await this.getTextValues();
return this.displayBinarySelection(initialValues);
}
/**
* Fetch text values for select options.
*
* Subclasses should override this method to set their own options.
*
* @returns {Promise}
*/
getTextValues() {
return getStrings([{key: 'no'}, {key: 'yes'}]);
}
/**
* Renders yes/no select input with proper selection.
*
* @param {Array} initialValues The default value for the filter.
*/
async displayBinarySelection(initialValues = []) {
// We specify a specific filterset in case there are multiple filtering condition - avoiding glitches.
const specificFilterSet = this.rootNode.querySelector(Selectors.filter.byName(this.filterType));
const sourceDataNode = this.getSourceDataForFilter();
const context = {
filtertype: this.filterType,
title: sourceDataNode.getAttribute('data-field-title'),
required: sourceDataNode.dataset.required,
options: [
{
text: this.optionOne,
value: 0,
selected: initialValues[0] === 0,
},
{
text: this.optionTwo,
value: 1,
selected: initialValues[0] === 1,
},
]
};
return Templates.render('core/datafilter/filtertypes/binary_selector', context)
.then((binaryUi, js) => {
return Templates.replaceNodeContents(specificFilterSet.querySelector(Selectors.filter.regions.values), binaryUi, js);
});
}
/**
* Get the list of raw values for this filter type.
*
* @returns {Array}
*/
get values() {
return this.filterRoot.querySelector(`[data-filterfield="${this.name}"]`).value;
}
}

View File

@ -51,6 +51,7 @@ export default {
filtermatch: getFilterRegion('filtermatch'),
filterlist: getFilterRegion('filters'),
datasource: getFilterRegion('filtertypedata'),
emptyFilterRow: `${getFilterRegion('filter')}[data-filter-type=""]`,
},
fields: {
join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,

View File

@ -33,11 +33,11 @@
"rownumber": 1
}
}}
<div data-filterregion="filter">
<div data-filterregion="filter" data-filter-type="">
<fieldset>
<legend class="sr-only">{{#str}}filterrowlegend, core, {{rownumber}}{{/str}}</legend>
<div class="border-radius my-2 p-2 bg-white border d-flex flex-column flex-md-row align-items-md-start">
<div class="d-flex flex-column flex-md-row align-items-md-center">
<div class="border-radius my-2 p-2 bg-white border d-flex flex-column flex-md-row align-items-md-start mr-0 ml-0 row">
<div class="d-flex flex-column flex-md-row align-items-md-center my-1">
<label for="core-filter_row-jointype-{{uniqid}}" class="mr-md-2 mb-md-0">{{#str}}match{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="join" id="core-filter_row-jointype-{{uniqid}}">
<option value="0">{{#str}}none{{/str}}</option>
@ -46,15 +46,15 @@
</select>
</div>
<label class="sr-only pt-2" for="core-filter_row-filtertype-{{uniqid}}">{{#str}}filtertype{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="type" id="core-filter_row-filtertype-{{uniqid}}">
<label class="sr-only pt-2 my-1" for="core-filter_row-filtertype-{{uniqid}}">{{#str}}filtertype{{/str}}</label>
<select class="custom-select my-1 mb-md-0 mr-md-2" data-filterfield="type" id="core-filter_row-filtertype-{{uniqid}}">
<option value="">{{#str}}selectfiltertype{{/str}}</option>
{{#filtertypes}}
<option value="{{name}}">{{title}}</option>
{{/filtertypes}}
</select>
<div data-filterregion="value" class="d-md-block flex-column align-items-start flex-lg-row"></div>
<div data-filterregion="value" class="d-md-block flex-column align-items-start flex-lg-row my-1"></div>
<button data-filteraction="remove" class="ml-auto icon-no-margin icon-size-4 btn text-reset" aria-label="{{#str}}clearfilterrow{{/str}}">
{{#pix}}e/cancel_solid_circle{{/pix}}

View File

@ -0,0 +1,46 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core/datafilter/filtertypes/binary_selector
Moodle template for hidden, questiontext and subcategories filter.
Context variables required for this template:
* filtertype - filter type name for dataset filterfield.
* textvalueone - text value for first selection.
* textvaluetwo - text value for second selection.
* optionone - boolen for first option selection.
* optiontwo - boolean for second option selection.
Example context (json):
{
"title": "Show hidden questions",
"filtertype": "hidden",
"textvalueone": "yes",
"textvaluetwo": "no",
"optionone": false,
"optiontwo": true
}
}}
<div class="d-flex flex-column flex-md-row align-items-md-center" data-required="{{required}}">
<label for="core-filter_row-binary-{{uniqid}}" class="sr-only">{{title}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" name="{{filtertype}}" data-filterfield="{{filtertype}}" id="core-filter_row-binary-{{uniqid}}">
{{#options}}
<option{{#selected}} selected{{/selected}} value="{{value}}">{{text}}</option>
{{/options}}
</select>
</div>

View File

@ -93,6 +93,8 @@ being forced open in all behat tests.
* New `required` field added to core\output\datafilter. This will be output via the data-required property in filter_type.mustache,
any additional filter types will need to output this property as well. Filters with data-required="1" will not be removable from
the list of filters.
* addFilterRow() in core/datafilter now accepts a filterdata object to add a row with a pre-defined filter.
* New "binary" datafilter type added for creating filters with a single yes/no option.
=== 4.2 ===