mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-24 00:50:28 +00:00
Checkbox widget: list and filter modes (#6561)
* Docs for CheckboxWidget list and filter modes
This documents the `listField` and `filter` attributes.
* Tests for checkbox widget list mode
* Implement checkbox list mode
* WIP on implementing filter attr for checkboxes
* Improve CheckboxWidget documentation
* Refactor checkbox tests: move function to top
The big findNodeOfType function belongs at the top of the describe
block, so that the checkbox tests are more compact and easy to read.
* Move checkbox widget tests to end of file
The checkbox widget tests are long and involved, so we'll move them to
the end of the file so they aren't a huge block of code you need to read
past to find the next test.
* Improve formatting of CheckboxWidget docs
The \define() calls that are short enough to fit on one line should be
put on one line, for readability. The ones that are quite long have been
kept on multiple lines, for readability.
* Added more passing tests for checkbox widget
* Add some failing tests for checkbox widget
The filter mode where neither checked nor unchecked is specified (in
which case an empty filter result means false and a non-empty result
means true) is not working yet.
* Make failing tests pass
* Uncomment (and improve) test for field mode
We're now ready to start working on making this test pass. (There was
also one small mistake in the test, which this commit corrects).
* All tests now passing
* No indeterminate checkboxes in simple modes
The simple checkbox modes (field and index) should not produce
indeterminate checkboxes. That should be reserved for the advanced modes
(list and filter).
* Minor improvement to unit tests
* Allow indeterminate checkboxes in list and filter modes
This change may require some tweaks to the unit tests to be able to test
it properly.
* Slightly easier to read tests
* Two more tests for list mode
* Greatly simplify unit test code
Turns out there's no need to jump through Object.getPrototypeOf hoops.
* Minor simplification of unit test
* Add tests for indeterminate in list & filter modes
With this, the set of tests is complete.
* More tests to specify list mode behavior
* Unfocus tests so all tests run
* Update docs to say "new in 5.2.3" insetad of 5.2.2
* Move checkbox widget tests into their own file
The test-widget.js file was getting too long with all the checkbox
tests added, so we'll move the checkbox tests into their own file.
* Add checkbox widget tests for index mode
This commit also adds tests for index list mode (with a listIndex
attribute that will parallel the listField attribute) but leaves them
commented out because they don't pass yet: the code that implements the
listIndex attribute hasn't been written yet).
* Add listIndex attribute to checkbox widget
* Remove code that lets checkboxes be indeterminate
This reverts commit 6afcb151be
. We will
add this code back in a later PR.
* Remove indeterminate tests for checkbox widget
We're currently not allowing indeterminate checkboxes, so there's no
need for the tests that check for them.
* Document listIndex attribute of CheckboxWidget
* adds class tc-checkbox-checked when checked
* equivalent to #2182 (RadioWidget)
* also applies `tc-checkbox` to checkboxes by default, always
* Move macro definitions inside example text
Since the wikitext-example-without-html macro creates a new parsing
context, it's safe to have macro definitions inside it. That makes these
examples a lot easier to write, and to read.
* Remove all mention of indeterminate checkboxes
Also improve the documentation a little bit: mention what happens in
list mode if neither checked nor unchecked is specified.
* Move filter mode to bottom of checkbox docs
The `filter` attribute should be under both `listField` and `listIndex`
rather than being between them. The documentation for filter mode should
similarly be after the `listIndex` documentation.
* Improve docs for `class` attr of checkbox widget
This brings the wording of the `class` attribute more in line with how
it's worded in the RadioWidget docs.
* Fix bug with list tiddlers
If neither checked nor unchecked was specified, then the behavior should
be "empty = false, non-empty = true". But if *both* are specified yet
neither is found, then the checkbox should be unchecked (false). It had
been falling through to the "non-empty = true" behavior, which was wrong.
* Improve listIndex example of checkbox widgets
* Remove unused function from test-widget.js
Co-authored-by: Tobias Beer <beertobias@gmail.com>
This commit is contained in:
parent
2ab4e965b2
commit
e28af8d594
@ -35,11 +35,12 @@ CheckboxWidget.prototype.render = function(parent,nextSibling) {
|
||||
this.execute();
|
||||
// Create our elements
|
||||
this.labelDomNode = this.document.createElement("label");
|
||||
this.labelDomNode.setAttribute("class",this.checkboxClass);
|
||||
this.labelDomNode.setAttribute("class","tc-checkbox " + this.checkboxClass);
|
||||
this.inputDomNode = this.document.createElement("input");
|
||||
this.inputDomNode.setAttribute("type","checkbox");
|
||||
if(this.getValue()) {
|
||||
this.inputDomNode.setAttribute("checked","true");
|
||||
$tw.utils.addClass(this.labelDomNode,"tc-checkbox-checked");
|
||||
}
|
||||
if(this.isDisabled === "yes") {
|
||||
this.inputDomNode.setAttribute("disabled",true);
|
||||
@ -59,7 +60,7 @@ CheckboxWidget.prototype.render = function(parent,nextSibling) {
|
||||
|
||||
CheckboxWidget.prototype.getValue = function() {
|
||||
var tiddler = this.wiki.getTiddler(this.checkboxTitle);
|
||||
if(tiddler) {
|
||||
if(tiddler || this.checkboxFilter) {
|
||||
if(this.checkboxTag) {
|
||||
if(this.checkboxInvertTag) {
|
||||
return !tiddler.hasTag(this.checkboxTag);
|
||||
@ -67,12 +68,17 @@ CheckboxWidget.prototype.getValue = function() {
|
||||
return tiddler.hasTag(this.checkboxTag);
|
||||
}
|
||||
}
|
||||
if(this.checkboxField) {
|
||||
if(this.checkboxField || this.checkboxIndex) {
|
||||
// Same logic applies to fields and indexes
|
||||
var value;
|
||||
if($tw.utils.hop(tiddler.fields,this.checkboxField)) {
|
||||
value = tiddler.fields[this.checkboxField] || "";
|
||||
if(this.checkboxField) {
|
||||
if($tw.utils.hop(tiddler.fields,this.checkboxField)) {
|
||||
value = tiddler.fields[this.checkboxField] || "";
|
||||
} else {
|
||||
value = this.checkboxDefault || "";
|
||||
}
|
||||
} else {
|
||||
value = this.checkboxDefault || "";
|
||||
value = this.wiki.extractTiddlerDataItem(tiddler,this.checkboxIndex,this.checkboxDefault || "");
|
||||
}
|
||||
if(value === this.checkboxChecked) {
|
||||
return true;
|
||||
@ -80,15 +86,46 @@ CheckboxWidget.prototype.getValue = function() {
|
||||
if(value === this.checkboxUnchecked) {
|
||||
return false;
|
||||
}
|
||||
// Neither value found: were both specified?
|
||||
if(this.checkboxChecked && !this.checkboxUnchecked) {
|
||||
return false; // Absence of checked value
|
||||
}
|
||||
if(this.checkboxUnchecked && !this.checkboxChecked) {
|
||||
return true; // Absence of unchecked value
|
||||
}
|
||||
}
|
||||
if(this.checkboxIndex) {
|
||||
var value = this.wiki.extractTiddlerDataItem(tiddler,this.checkboxIndex,this.checkboxDefault || "");
|
||||
if(value === this.checkboxChecked) {
|
||||
if(this.checkboxListField || this.checkboxListIndex || this.checkboxFilter) {
|
||||
// Same logic applies to lists and filters
|
||||
var list;
|
||||
if(this.checkboxListField) {
|
||||
if($tw.utils.hop(tiddler.fields,this.checkboxListField)) {
|
||||
list = tiddler.getFieldList(this.checkboxListField);
|
||||
} else {
|
||||
list = $tw.utils.parseStringArray(this.checkboxDefault || "") || [];
|
||||
}
|
||||
} else if (this.checkboxListIndex) {
|
||||
list = $tw.utils.parseStringArray(this.wiki.extractTiddlerDataItem(tiddler,this.checkboxListIndex,this.checkboxDefault || "")) || [];
|
||||
} else {
|
||||
list = this.wiki.filterTiddlers(this.checkboxFilter,this) || [];
|
||||
}
|
||||
if(list.indexOf(this.checkboxChecked) !== -1) {
|
||||
return true;
|
||||
}
|
||||
if(value === this.checkboxUnchecked) {
|
||||
if(list.indexOf(this.checkboxUnchecked) !== -1) {
|
||||
return false;
|
||||
}
|
||||
// Neither one present
|
||||
if(this.checkboxChecked && !this.checkboxUnchecked) {
|
||||
return false; // Absence of checked value
|
||||
}
|
||||
if(this.checkboxUnchecked && !this.checkboxChecked) {
|
||||
return true; // Absence of unchecked value
|
||||
}
|
||||
if(this.checkboxChecked && this.checkboxUnchecked) {
|
||||
return false; // Both specified but neither found: default to false
|
||||
}
|
||||
// Neither specified, so empty list is false, non-empty is true
|
||||
return !!list.length;
|
||||
}
|
||||
} else {
|
||||
if(this.checkboxTag) {
|
||||
@ -114,7 +151,8 @@ CheckboxWidget.prototype.handleChangeEvent = function(event) {
|
||||
hasChanged = false,
|
||||
tagCheck = false,
|
||||
hasTag = tiddler && tiddler.hasTag(this.checkboxTag),
|
||||
value = checked ? this.checkboxChecked : this.checkboxUnchecked;
|
||||
value = checked ? this.checkboxChecked : this.checkboxUnchecked,
|
||||
notValue = checked ? this.checkboxUnchecked : this.checkboxChecked;
|
||||
if(this.checkboxTag && this.checkboxInvertTag === "yes") {
|
||||
tagCheck = hasTag === checked;
|
||||
} else {
|
||||
@ -148,9 +186,51 @@ CheckboxWidget.prototype.handleChangeEvent = function(event) {
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
// Set the list field (or index) if specified
|
||||
if(this.checkboxListField || this.checkboxListIndex) {
|
||||
var listContents, oldPos, newPos;
|
||||
if(this.checkboxListField) {
|
||||
listContents = tiddler.getFieldList(this.checkboxListField);
|
||||
} else {
|
||||
listContents = $tw.utils.parseStringArray(this.wiki.extractTiddlerDataItem(this.checkboxTitle,this.checkboxListIndex) || "") || [];
|
||||
}
|
||||
oldPos = notValue ? listContents.indexOf(notValue) : -1;
|
||||
newPos = value ? listContents.indexOf(value) : -1;
|
||||
if(oldPos === -1 && newPos !== -1) {
|
||||
// old value absent, new value present: no change needed
|
||||
} else if(oldPos === -1) {
|
||||
// neither one was present
|
||||
if(value) {
|
||||
listContents.push(value);
|
||||
hasChanged = true;
|
||||
} else {
|
||||
// value unspecified? then leave list unchanged
|
||||
}
|
||||
} else if(newPos === -1) {
|
||||
// old value present, new value absent
|
||||
if(value) {
|
||||
listContents[oldPos] = value;
|
||||
hasChanged = true;
|
||||
} else {
|
||||
listContents.splice(oldPos, 1)
|
||||
hasChanged = true;
|
||||
}
|
||||
} else {
|
||||
// both were present: just remove the old one, leave new alone
|
||||
listContents.splice(oldPos, 1)
|
||||
hasChanged = true;
|
||||
}
|
||||
if(this.checkboxListField) {
|
||||
newFields[this.checkboxListField] = $tw.utils.stringifyList(listContents);
|
||||
}
|
||||
// The listIndex case will be handled in the if(hasChanged) block below
|
||||
}
|
||||
if(hasChanged) {
|
||||
if(this.checkboxIndex) {
|
||||
this.wiki.setText(this.checkboxTitle,"",this.checkboxIndex,value);
|
||||
} else if(this.checkboxListIndex) {
|
||||
var listIndexValue = (listContents && listContents.length) ? $tw.utils.stringifyList(listContents) : undefined;
|
||||
this.wiki.setText(this.checkboxTitle,"",this.checkboxListIndex,listIndexValue);
|
||||
} else {
|
||||
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),fallbackFields,tiddler,newFields,this.wiki.getModificationFields()));
|
||||
}
|
||||
@ -179,6 +259,9 @@ CheckboxWidget.prototype.execute = function() {
|
||||
this.checkboxTag = this.getAttribute("tag");
|
||||
this.checkboxField = this.getAttribute("field");
|
||||
this.checkboxIndex = this.getAttribute("index");
|
||||
this.checkboxListField = this.getAttribute("listField");
|
||||
this.checkboxListIndex = this.getAttribute("listIndex");
|
||||
this.checkboxFilter = this.getAttribute("filter");
|
||||
this.checkboxChecked = this.getAttribute("checked");
|
||||
this.checkboxUnchecked = this.getAttribute("unchecked");
|
||||
this.checkboxDefault = this.getAttribute("default");
|
||||
@ -194,14 +277,20 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
|
||||
*/
|
||||
CheckboxWidget.prototype.refresh = function(changedTiddlers) {
|
||||
var changedAttributes = this.computeAttributes();
|
||||
if(changedAttributes.tiddler || changedAttributes.tag || changedAttributes.invertTag || changedAttributes.field || changedAttributes.index || changedAttributes.checked || changedAttributes.unchecked || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.disabled) {
|
||||
if(changedAttributes.tiddler || changedAttributes.tag || changedAttributes.invertTag || changedAttributes.field || changedAttributes.index || changedAttributes.listField || changedAttributes.filter || changedAttributes.checked || changedAttributes.unchecked || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.disabled) {
|
||||
this.refreshSelf();
|
||||
return true;
|
||||
} else {
|
||||
var refreshed = false;
|
||||
if(changedTiddlers[this.checkboxTitle]) {
|
||||
this.inputDomNode.checked = this.getValue();
|
||||
var isChecked = this.getValue();
|
||||
this.inputDomNode.checked = isChecked;
|
||||
refreshed = true;
|
||||
if(isChecked) {
|
||||
$tw.utils.addClass(this.labelDomNode,"tc-checkbox-checked");
|
||||
} else {
|
||||
$tw.utils.removeClass(this.labelDomNode,"tc-checkbox-checked");
|
||||
}
|
||||
}
|
||||
return this.refreshChildren(changedTiddlers) || refreshed;
|
||||
}
|
||||
|
438
editions/test/tiddlers/tests/test-checkbox-widget.js
Normal file
438
editions/test/tiddlers/tests/test-checkbox-widget.js
Normal file
@ -0,0 +1,438 @@
|
||||
/*\
|
||||
title: testcheckbox-widget.js
|
||||
type: application/javascript
|
||||
tags: [[$:/tags/test-spec]]
|
||||
|
||||
Tests the checkbox widget thoroughly.
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
describe("Checkbox widget", function() {
|
||||
|
||||
var widget = require("$:/core/modules/widgets/widget.js");
|
||||
|
||||
function createWidgetNode(parseTreeNode,wiki) {
|
||||
return new widget.widget(parseTreeNode,{
|
||||
wiki: wiki,
|
||||
document: $tw.fakeDocument
|
||||
});
|
||||
}
|
||||
|
||||
function parseText(text,wiki,options) {
|
||||
var parser = wiki.parseText("text/vnd.tiddlywiki",text,options);
|
||||
return parser ? {type: "widget", children: parser.tree} : undefined;
|
||||
}
|
||||
|
||||
function renderWidgetNode(widgetNode) {
|
||||
$tw.fakeDocument.setSequenceNumber(0);
|
||||
var wrapper = $tw.fakeDocument.createElement("div");
|
||||
widgetNode.render(wrapper,null);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Find a particular type of node from inside the widget tree
|
||||
// Less brittle than wrapper.children[0].children[0] if the parse
|
||||
// tree ever changes in the future
|
||||
function findNodeOfType(targetType, currentNode) {
|
||||
if(currentNode.parseTreeNode && currentNode.parseTreeNode.type === targetType) {
|
||||
return currentNode;
|
||||
} else if(currentNode.children && currentNode.children.length) {
|
||||
var child, result, i;
|
||||
for (i = 0; i < currentNode.children.length; i++) {
|
||||
child = currentNode.children[i];
|
||||
result = findNodeOfType(targetType, child);
|
||||
if(result) return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/*
|
||||
* Test data for checkbox widget tests
|
||||
*/
|
||||
|
||||
const fieldModeTests = [
|
||||
{
|
||||
testName: "field mode checked",
|
||||
tiddlers: [{title: "TiddlerOne", text: "Jolly Old World", expand: "yes"}],
|
||||
widgetText: "<$checkbox tiddler='TiddlerOne' field='expand' checked='yes' />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "TiddlerOne": { expand: undefined } }
|
||||
},
|
||||
{
|
||||
testName: "field mode unchecked",
|
||||
tiddlers: [{title: "TiddlerOne", text: "Jolly Old World", expand: "no"}],
|
||||
widgetText: "<$checkbox tiddler='TiddlerOne' field='expand' unchecked='no' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "TiddlerOne": { expand: undefined } }
|
||||
},
|
||||
{
|
||||
testName: "field mode toggle",
|
||||
tiddlers: [{title: "TiddlerOne", text: "Jolly Old World", expand: "no"}],
|
||||
widgetText: "<$checkbox tiddler='TiddlerOne' field='expand' checked='yes' unchecked='no' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "TiddlerOne": { expand: "yes" } }
|
||||
},
|
||||
];
|
||||
|
||||
const indexModeTests = fieldModeTests.map(data => {
|
||||
const newData = {...data};
|
||||
const newName = data.testName.replace('field mode', 'index mode');
|
||||
const newTiddlers = data.tiddlers.map(tiddler => {
|
||||
return {title: tiddler.title, type: "application/x-tiddler-dictionary", text: `one: a\nexpand: ${tiddler.expand}\ntwo: b`}
|
||||
});
|
||||
const newWidgetText = data.widgetText.replace("field='expand'", "index='expand'");
|
||||
const newChange = {};
|
||||
for (const key of Object.keys(data.expectedChange)) {
|
||||
const oldChange = data.expectedChange[key];
|
||||
if (oldChange.expand) {
|
||||
newChange[key] = { text: `one: a\nexpand: ${oldChange.expand}\ntwo: b` }
|
||||
} else {
|
||||
// In index tiddlers, the "expand" field gets completely removed, not turned into "expand: (undefined)"
|
||||
newChange[key] = { text: `one: a\ntwo: b` }
|
||||
}
|
||||
}
|
||||
newData.testName = newName;
|
||||
newData.tiddlers = newTiddlers;
|
||||
newData.widgetText = newWidgetText;
|
||||
newData.expectedChange = newChange;
|
||||
return newData;
|
||||
});
|
||||
|
||||
const listModeTests = [
|
||||
{
|
||||
testName: "list mode add",
|
||||
tiddlers: [{title: "Colors", colors: "orange yellow"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' checked='green' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode remove",
|
||||
tiddlers: [{title: "Colors", colors: "green orange yellow"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' checked='green' />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode remove inverted",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' unchecked='red' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode remove in middle position",
|
||||
tiddlers: [{title: "Colors", colors: "orange green yellow"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' checked='green' />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode remove in middle position inverted",
|
||||
tiddlers: [{title: "Colors", colors: "orange red yellow"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' unchecked='red' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode remove in final position",
|
||||
tiddlers: [{title: "Colors", colors: "orange yellow green"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' checked='green' />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode remove in final position inverted",
|
||||
tiddlers: [{title: "Colors", colors: "orange yellow red"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' unchecked='red' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode toggle",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' unchecked='red' checked='green' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "green orange yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode toggle in middle position",
|
||||
tiddlers: [{title: "Colors", colors: "orange red yellow"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' unchecked='red' checked='green' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange green yellow" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode remove in final position",
|
||||
tiddlers: [{title: "Colors", colors: "orange yellow red"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' unchecked='red' checked='green' />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode neither checked nor unchecked specified: field value remains unchanged",
|
||||
tiddlers: [{title: "Colors", colors: "orange yellow red"}],
|
||||
widgetText: "<$checkbox tiddler='Colors' listField='colors' />",
|
||||
startsOutChecked: true,
|
||||
finalValue: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow red" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode neither checked nor unchecked specified, but actions specified to change field value",
|
||||
tiddlers: [{title: "ExampleTiddler", someField: "yes"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='ExampleTiddler' $field='someField' $filter='yes'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='ExampleTiddler' $field='someField' $filter='-yes'/>\n" +
|
||||
"<$checkbox tiddler='ExampleTiddler' listField='someField' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "ExampleTiddler": { someField: "" } }
|
||||
},
|
||||
{
|
||||
testName: "list mode neither checked nor unchecked specified, means field value is treated as empty=false, nonempty=true",
|
||||
tiddlers: [{title: "ExampleTiddler", someField: "yes"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='ExampleTiddler' $field='someField' $filter='yes -no'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='ExampleTiddler' $field='someField' $filter='-yes no'/>\n" +
|
||||
"<$checkbox tiddler='ExampleTiddler' listField='someField' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
finalValue: true, // "no" is considered true when neither `checked` nor `unchecked` is specified
|
||||
expectedChange: { "ExampleTiddler": { someField: "no" } }
|
||||
},
|
||||
];
|
||||
|
||||
const indexListModeTests = listModeTests.map(data => {
|
||||
const newData = {...data};
|
||||
const newName = data.testName.replace('list mode', 'index list mode');
|
||||
const newTiddlers = data.tiddlers.map(tiddler => {
|
||||
if (tiddler.hasOwnProperty('colors')) {
|
||||
return {title: tiddler.title, type: "application/x-tiddler-dictionary", text: `one: a\ncolors: ${tiddler.colors}\ntwo: b`}
|
||||
} else if (tiddler.hasOwnProperty('someField')) {
|
||||
return {title: tiddler.title, type: "application/x-tiddler-dictionary", text: `one: a\nsomeField: ${tiddler.someField}\ntwo: b`}
|
||||
}
|
||||
});
|
||||
const newWidgetText = data.widgetText.replace("listField='colors'", "listIndex='colors'").replace(/\$field/g, '$index').replace("listField='someField'", "listIndex='someField'");
|
||||
const newChange = {};
|
||||
for (const key of Object.keys(data.expectedChange)) {
|
||||
const oldChange = data.expectedChange[key];
|
||||
if (oldChange.colors) {
|
||||
newChange[key] = { text: `one: a\ncolors: ${oldChange.colors}\ntwo: b` }
|
||||
} else if (oldChange.someField !== undefined) {
|
||||
newChange[key] = { text: `one: a\nsomeField: ${oldChange.someField}\ntwo: b` }
|
||||
} else {
|
||||
// In index tiddlers, fields with value undefined get completely removed
|
||||
newChange[key] = { text: `one: a\ntwo: b` }
|
||||
}
|
||||
}
|
||||
newData.testName = newName;
|
||||
newData.tiddlers = newTiddlers;
|
||||
newData.widgetText = newWidgetText;
|
||||
newData.expectedChange = newChange;
|
||||
return newData;
|
||||
});
|
||||
|
||||
const filterModeTests = [
|
||||
{
|
||||
testName: "filter mode false -> true",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' unchecked='red' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' unchecked='red' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow red" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode no default false -> true",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' unchecked='red' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode no default true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' unchecked='red' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow red" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only checked specified false -> true",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only checked specified true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow red" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only checked specified no default false -> true",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only checked specified no default true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checked='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow red" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only unchecked specified false -> true",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' unchecked='red' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only unchecked specified true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' unchecked='red' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow red" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only unchecked specified no default false -> true",
|
||||
tiddlers: [{title: "Colors", colors: "red orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' unchecked='red' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "orange yellow green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode only unchecked specified no default true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green orange yellow"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-red green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='red -green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' unchecked='red' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "orange yellow red" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode neither checked nor unchecked specified false -> true",
|
||||
tiddlers: [{title: "Colors"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode neither checked nor unchecked specified true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' default='green' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode neither checked nor unchecked no default specified false -> true",
|
||||
tiddlers: [{title: "Colors", colors: ""}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: false,
|
||||
expectedChange: { "Colors": { colors: "green" } }
|
||||
},
|
||||
{
|
||||
testName: "filter mode neither checked nor unchecked no default specified true -> false",
|
||||
tiddlers: [{title: "Colors", colors: "green"}],
|
||||
widgetText: "\\define checkActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='green'/>\n" +
|
||||
"\\define uncheckActions() <$action-listops $tiddler='Colors' $field='colors' $subfilter='-green'/>\n" +
|
||||
"<$checkbox filter='[list[Colors!!colors]]' checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> />",
|
||||
startsOutChecked: true,
|
||||
expectedChange: { "Colors": { colors: "" } }
|
||||
},
|
||||
];
|
||||
|
||||
const checkboxTestData = fieldModeTests.concat(
|
||||
indexModeTests,
|
||||
listModeTests,
|
||||
indexListModeTests,
|
||||
filterModeTests,
|
||||
);
|
||||
|
||||
/*
|
||||
* Checkbox widget tests using the test data above
|
||||
*/
|
||||
for (const data of checkboxTestData) {
|
||||
it('checkbox widget test: ' + data.testName, function() {
|
||||
// Setup
|
||||
|
||||
var wiki = new $tw.Wiki();
|
||||
wiki.addTiddlers(data.tiddlers);
|
||||
var widgetNode = createWidgetNode(parseText(data.widgetText,wiki),wiki);
|
||||
var wrapper = renderWidgetNode(widgetNode);
|
||||
|
||||
// Check initial state
|
||||
|
||||
const widget = findNodeOfType('checkbox', widgetNode);
|
||||
// Verify that the widget is or is not checked as expected
|
||||
expect(widget.getValue()).toBe(data.startsOutChecked);
|
||||
|
||||
// Fake an event that toggles the checkbox
|
||||
|
||||
// fakedom elmenets don't have a "checked" property. so we fake it because
|
||||
// Checkbox.prototype.handleChangeEvent looks at the "checked" DOM property
|
||||
widget.inputDomNode.checked = !!widget.inputDomNode.attributes.checked;
|
||||
// Now simulate checking the box
|
||||
widget.inputDomNode.checked = !widget.inputDomNode.checked;
|
||||
widget.handleChangeEvent(null);
|
||||
|
||||
// Check state again: in most tests, checkbox should be inverse of what it was
|
||||
const finalValue = data.hasOwnProperty('finalValue') ? data.finalValue : !data.startsOutChecked;
|
||||
expect(widget.getValue()).toBe(finalValue);
|
||||
|
||||
// Check that tiddler(s) has/have gone through expected change(s)
|
||||
for (const key of Object.keys(data.expectedChange)) {
|
||||
const tiddler = wiki.getTiddler(key);
|
||||
const change = data.expectedChange[key];
|
||||
for (const fieldName of Object.keys(change)) {
|
||||
const expectedValue = change[fieldName];
|
||||
const fieldValue = tiddler.fields[fieldName];
|
||||
expect(fieldValue).toBe(expectedValue);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -1,7 +1,8 @@
|
||||
caption: checkbox
|
||||
created: 20131024141900000
|
||||
modified: 20211009121239806
|
||||
modified: 20220402023600000
|
||||
tags: Widgets TriggeringWidgets
|
||||
colors: red orange yellow blue
|
||||
title: CheckboxWidget
|
||||
type: text/vnd.tiddlywiki
|
||||
|
||||
@ -21,11 +22,14 @@ The content of the `<$checkbox>` widget is displayed within an HTML `<label>` el
|
||||
|tag |The name of the tag to which the checkbox is bound |
|
||||
|invertTag |When set to ''yes'', flips the tag binding logic so that the absence of the tag causes the checkbox to be checked |
|
||||
|field |The name of the field to which the checkbox is bound |
|
||||
|listField |<<.from-version "5.2.3">> The name of the field that contains the list to which the checkbox is bound |
|
||||
|index|<<.from-version "5.1.14">> The index of the //tiddler//, a [[DataTiddler|DataTiddlers]], to which the checkbox is bound<<.tip "be sure to set the //tiddler// correctly">>|
|
||||
|listIndex |<<.from-version "5.2.3">> Like <<.attr index>>, but treats the value as a list the same way that <<.attr listField>> does |
|
||||
|filter |<<.from-version "5.2.3">> A filter whose output determines the checked state of the checkbox |
|
||||
|checked |The value of the field corresponding to the checkbox being checked |
|
||||
|unchecked |The value of the field corresponding to the checkbox being unchecked |
|
||||
|default |The default value to use if the field is not defined |
|
||||
|class |The class that will be assigned to the label element |
|
||||
|class |The class that will be assigned to the label element <$macrocall $name=".tip" _="""<<.from-version "5.2.3">> `tc-checkbox` is always applied by default, as well as `tc-checkbox-checked` when checked"""/> |
|
||||
|actions |<<.from-version "5.1.14">> A string containing ActionWidgets to be triggered when the status of the checkbox changes (whether it is checked or unchecked) |
|
||||
|uncheckactions |<<.from-version "5.1.16">> A string containing ActionWidgets to be triggered when the checkbox is unchecked |
|
||||
|checkactions |<<.from-version "5.1.20">> A string containing ActionWidgets to be triggered when the checkbox is checked |
|
||||
@ -45,7 +49,19 @@ Using the checkbox widget in field mode requires the ''field'' attribute to spec
|
||||
|
||||
This example creates a checkbox that is checked if the field ''status'' is equal to ''open'' and unchecked if the field is equal to ''closed''. If the field is undefined then it defaults to ''closed'', meaning that the checkbox will be unchecked if the ''status'' field is missing.
|
||||
|
||||
<<wikitext-example-without-html """<$checkbox field="status" checked="open" unchecked="closed" default="closed"> Is it open?</$checkbox><br>''status:'' {{!!status}}""">>
|
||||
<<wikitext-example-without-html """<$checkbox field="status" checked="open" unchecked="closed" default="closed"> Is it open?</$checkbox><br />''status:'' {{!!status}}""">>
|
||||
|
||||
!! List Mode
|
||||
|
||||
Using the checkbox widget in list mode requires the ''listField'' attribute to specify the name of a field containing a list. The ''checked'' attribute specifies the value that should be present or absent in the list when the checkbox is checked or unchecked respectively. If ''checked'' is absent (or empty) but ''unchecked'' is present, then the logic will be inverted: the checkbox will be checked when the "unchecked" value is missing from the list, and unchecked when the "unchecked" value is found in the list. If both ''checked'' and ''unchecked'' are present, the checkbox will work like a toggle, replacing the ''checked'' value with the ''unchecked'' value and vice-versa. Finally, if neither ''checked'' nor ''unchecked'' is specified, the checkbox will be checked if the field has anything in it, but unchecked if the field is missing or empty. (This is rarely useful; most of the time you'll want to specify ''checked'' or ''unchecked'' or both.)
|
||||
|
||||
The ''default'' attribute is used as a fallback for the checkbox state if the field is not defined.
|
||||
|
||||
This example creates a checkbox that is checked if the list field named ''colors'' contains ''green'' and unchecked if the field contains ''red''. If the field is undefined, or if neither ''green'' nor ''red'' appears in the field, then it defaults to ''green'', meaning that the checkbox will be checked.
|
||||
|
||||
<<wikitext-example-without-html """<$checkbox listField="colors" checked="green" unchecked="red" default="green"> Is it green?</$checkbox><br />''colors:'' {{!!colors}}""">>
|
||||
|
||||
Try editing the ''colors'' field of this tiddler to see how the example changes.
|
||||
|
||||
!! Index Mode
|
||||
|
||||
@ -54,3 +70,33 @@ To use the checkbox widget in index mode set the ''index'' attribute to the inde
|
||||
The example below creates a checkbox that is checked if the index by the name of this tiddler in the tiddler ExampleData is equal to ''selected'' and unchecked if the index is an empty string. If the index is undefined then it defaults to an empty string, meaning the checkbox will be unchecked if the index is missing.
|
||||
|
||||
<$macrocall $name="wikitext-example-without-html" src="""<$checkbox tiddler="ExampleData" index=<<currentTiddler>> checked="selected" unchecked="" default=""> Selected?</$checkbox>"""/>
|
||||
|
||||
!! Index List Mode
|
||||
|
||||
Using the checkbox widget in index list mode requires the ''listIndex'' attribute to specify the the index of a [[DataTiddler|DataTiddlers]] containing a list. The ''checked'' attribute specifies the value that should be present or absent in the list when the checkbox is checked or unchecked respectively. If ''checked'' is absent (or empty) but ''unchecked'' is present, then the logic will be inverted: the checkbox will be checked when the "unchecked" value is missing from the list, and unchecked when the "unchecked" value is found in the list. If both ''checked'' and ''unchecked'' are present, the checkbox will work like a toggle, replacing the ''checked'' value with the ''unchecked'' value and vice-versa. Finally, if neither ''checked'' nor ''unchecked'' is specified, the checkbox will be checked if the field has anything in it, but unchecked if the field is missing or empty. (This is rarely useful; most of the time you'll want to specify ''checked'' or ''unchecked'' or both.)
|
||||
|
||||
The ''default'' attribute is used as a fallback for the checkbox state if the index is undefined.
|
||||
|
||||
The example below creates three checkboxes that each control a different value in an index field of the ExampleData tiddler.
|
||||
|
||||
<$macrocall $name="wikitext-example-without-html" src="""
|
||||
<$set name=indexName filter="[<currentTiddler>addsuffix[ Colors]]" >
|
||||
<$checkbox tiddler="ExampleData" listIndex=<<indexName>> checked="green" unchecked="red" default="red"> Green or red?</$checkbox> <br/>
|
||||
<$checkbox tiddler="ExampleData" listIndex=<<indexName>> checked="yellow" unchecked="blue" default="blue"> Yellow or blue?</$checkbox> <br/>
|
||||
<$checkbox tiddler="ExampleData" listIndex=<<indexName>> checked="orange" unchecked="purple" default="purple"> Orange or purple?</$checkbox> <br />
|
||||
Colors list: {{{ [[ExampleData]getindex<indexName>] }}}
|
||||
</$set>
|
||||
"""/>
|
||||
|
||||
!! Filter Mode
|
||||
|
||||
Using the checkbox widget in filter mode requires the ''filter'' attribute to contain a filter whose output will determine the checked state of the checkbox. In filter mode, checking the checkbox will not automatically make changes to any field of any tiddler. Instead, you can use the ''actions'' attribute (or ''checkactions'' and ''uncheckactions'') to specify what should happen when the checkbox is toggled. It is your responsibility to make sure the actions cause changes to the tiddlers or fields that the filter results depend on, so that the checkbox becomes properly checked or unchecked after the actions have triggered.
|
||||
|
||||
If the filter returns an empty result, the checkbox will be unchecked. Otherwise, if the filter result is non-empty, the checkbox will be checked. However, if either the ''checked'' or ''unchecked'' attributes (or both) are specified, then their values will be looked for in the filter result, instead of considering any non-empty value to mean "checked".
|
||||
|
||||
This example creates the same checkbox as in the list mode example, selecting between ''red'' and ''green'' in the ''colors'' list field, but using filters and actions to make the change.
|
||||
|
||||
<<wikitext-example-without-html """\define checkActions() <$action-listops $field="colors" $subfilter="-red green"/>
|
||||
\define uncheckActions() <$action-listops $field="colors" $subfilter="red -green"/>
|
||||
<$checkbox filter="[list[!!colors]]" checked="green" unchecked="red" default="red" checkactions=<<checkActions>> uncheckactions=<<uncheckActions>> > Is "green" in colors?</$checkbox><br />''colors:'' {{!!colors}}
|
||||
""">>
|
||||
|
Loading…
Reference in New Issue
Block a user