From 8dec6741210f8395c6f28cfec7b9e3300bca3eff Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 19 Apr 2022 02:50:03 +0700 Subject: [PATCH] Allow checkboxes to be indeterminate (#6593) * Documentation for indeterminate checkboxes * Unit tests for indeterminate checkboxes * Implement indeterminate checkboxes * Simplify indeterminate checkbox example * Slightly simplify refresh logic for indeterminate That five-line if statement can be turned into a simple assignment. * Use "yes" and "no" for checkbox indeterminate attr This makes the "indeterminate" attribute of the checkbox widget work the same way as other boolean attributes of other widgets. * Fix bug with invertTag attribute One place in the checkbox widget code was checking invertTag for Javascript truthiness rather than the value "yes", which could have produced incorrect results if anyone wrote invertTag="no". Fixed. --- core/modules/widgets/checkbox.js | 30 ++++++-- .../tiddlers/tests/test-checkbox-widget.js | 69 +++++++++++++++++++ .../tiddlers/widgets/CheckboxWidget.tid | 28 ++++++++ 3 files changed, 122 insertions(+), 5 deletions(-) diff --git a/core/modules/widgets/checkbox.js b/core/modules/widgets/checkbox.js index 23ad9fdfd..a81ca837a 100644 --- a/core/modules/widgets/checkbox.js +++ b/core/modules/widgets/checkbox.js @@ -27,6 +27,7 @@ CheckboxWidget.prototype = new Widget(); Render this widget into the DOM */ CheckboxWidget.prototype.render = function(parent,nextSibling) { + var isChecked; // Save the parent dom node this.parentDomNode = parent; // Compute our attributes @@ -38,10 +39,14 @@ CheckboxWidget.prototype.render = function(parent,nextSibling) { this.labelDomNode.setAttribute("class","tc-checkbox " + this.checkboxClass); this.inputDomNode = this.document.createElement("input"); this.inputDomNode.setAttribute("type","checkbox"); - if(this.getValue()) { + isChecked = this.getValue(); + if(isChecked) { this.inputDomNode.setAttribute("checked","true"); $tw.utils.addClass(this.labelDomNode,"tc-checkbox-checked"); } + if(isChecked === undefined && this.checkboxIndeterminate === "yes") { + this.inputDomNode.indeterminate = true; + } if(this.isDisabled === "yes") { this.inputDomNode.setAttribute("disabled",true); } @@ -62,7 +67,7 @@ CheckboxWidget.prototype.getValue = function() { var tiddler = this.wiki.getTiddler(this.checkboxTitle); if(tiddler || this.checkboxFilter) { if(this.checkboxTag) { - if(this.checkboxInvertTag) { + if(this.checkboxInvertTag === "yes") { return !tiddler.hasTag(this.checkboxTag); } else { return tiddler.hasTag(this.checkboxTag); @@ -93,6 +98,14 @@ CheckboxWidget.prototype.getValue = function() { if(this.checkboxUnchecked && !this.checkboxChecked) { return true; // Absence of unchecked value } + if(this.checkboxChecked && this.checkboxUnchecked) { + // Both specified but neither found: indeterminate or false, depending + if(this.checkboxIndeterminate === "yes") { + return undefined; + } else { + return false; + } + } } if(this.checkboxListField || this.checkboxListIndex || this.checkboxFilter) { // Same logic applies to lists and filters @@ -122,7 +135,12 @@ CheckboxWidget.prototype.getValue = function() { return true; // Absence of unchecked value } if(this.checkboxChecked && this.checkboxUnchecked) { - return false; // Both specified but neither found: default to false + // Both specified but neither found: indeterminate or false, depending + if(this.checkboxIndeterminate === "yes") { + return undefined; + } else { + return false; + } } // Neither specified, so empty list is false, non-empty is true return !!list.length; @@ -265,6 +283,7 @@ CheckboxWidget.prototype.execute = function() { this.checkboxChecked = this.getAttribute("checked"); this.checkboxUnchecked = this.getAttribute("unchecked"); this.checkboxDefault = this.getAttribute("default"); + this.checkboxIndeterminate = this.getAttribute("indeterminate","no"); this.checkboxClass = this.getAttribute("class",""); this.checkboxInvertTag = this.getAttribute("invertTag",""); this.isDisabled = this.getAttribute("disabled","no"); @@ -277,14 +296,15 @@ 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.listField || changedAttributes.filter || changedAttributes.checked || changedAttributes.unchecked || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.disabled) { + if(changedAttributes.tiddler || changedAttributes.tag || changedAttributes.invertTag || changedAttributes.field || changedAttributes.index || changedAttributes.listField || changedAttributes.listIndex || changedAttributes.filter || changedAttributes.checked || changedAttributes.unchecked || changedAttributes["default"] || changedAttributes.indeterminate || changedAttributes["class"] || changedAttributes.disabled) { this.refreshSelf(); return true; } else { var refreshed = false; if(changedTiddlers[this.checkboxTitle]) { var isChecked = this.getValue(); - this.inputDomNode.checked = isChecked; + this.inputDomNode.checked = !!isChecked; + this.inputDomNode.indeterminate = (isChecked === undefined); refreshed = true; if(isChecked) { $tw.utils.addClass(this.labelDomNode,"tc-checkbox-checked"); diff --git a/editions/test/tiddlers/tests/test-checkbox-widget.js b/editions/test/tiddlers/tests/test-checkbox-widget.js index 12bd53342..30df75bbd 100644 --- a/editions/test/tiddlers/tests/test-checkbox-widget.js +++ b/editions/test/tiddlers/tests/test-checkbox-widget.js @@ -78,6 +78,21 @@ Tests the checkbox widget thoroughly. startsOutChecked: false, expectedChange: { "TiddlerOne": { expand: "yes" } } }, + { + testName: "field mode indeterminate -> true", + tiddlers: [{title: "TiddlerOne", text: "Jolly Old World", expand: "some other value"}], + widgetText: "<$checkbox tiddler='TiddlerOne' field='expand' indeterminate='yes' checked='yes' unchecked='no' />", + startsOutChecked: undefined, + expectedChange: { "TiddlerOne": { expand: "yes" } } + }, + // true -> indeterminate cannot happen in field mode + { + testName: "field mode not indeterminate", + tiddlers: [{title: "TiddlerOne", text: "Jolly Old World", expand: "some other value"}], + widgetText: "<$checkbox tiddler='TiddlerOne' field='expand' indeterminate='' checked='yes' unchecked='no' />", + startsOutChecked: false, + expectedChange: { "TiddlerOne": { expand: "yes" } } + }, ]; const indexModeTests = fieldModeTests.map(data => { @@ -202,6 +217,21 @@ Tests the checkbox widget thoroughly. finalValue: true, // "no" is considered true when neither `checked` nor `unchecked` is specified expectedChange: { "ExampleTiddler": { someField: "no" } } }, + { + testName: "list mode indeterminate -> true", + tiddlers: [{title: "Colors", colors: "orange"}], + widgetText: "<$checkbox tiddler='Colors' listField='colors' indeterminate='yes' unchecked='red' checked='green' />", + startsOutChecked: undefined, + expectedChange: { "Colors": { colors: "orange green" } } + }, + // true -> indeterminate cannot happen in list mode + { + testName: "list mode not indeterminate", + tiddlers: [{title: "Colors", colors: "orange"}], + widgetText: "<$checkbox tiddler='Colors' listField='colors' unchecked='red' checked='green' />", + startsOutChecked: false, + expectedChange: { "Colors": { colors: "orange green" } } + }, ]; const indexListModeTests = listModeTests.map(data => { @@ -379,6 +409,45 @@ Tests the checkbox widget thoroughly. startsOutChecked: true, expectedChange: { "Colors": { colors: "" } } }, + + { + testName: "filter mode indeterminate -> true", + tiddlers: [{title: "Colors", colors: "orange yellow"}], + 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]]' indeterminate='yes' checked='green' unchecked='red' default='green' checkactions=<> uncheckactions=<> />", + startsOutChecked: undefined, + expectedChange: { "Colors": { colors: "orange yellow green" } } + }, + { + testName: "filter mode true -> indeterminate", + tiddlers: [{title: "Colors", colors: "green orange yellow"}], + 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]]' indeterminate='yes' checked='green' unchecked='red' default='green' checkactions=<> uncheckactions=<> />", + startsOutChecked: true, + finalValue: undefined, + expectedChange: { "Colors": { colors: "orange yellow" } } + }, + { + testName: "filter mode not indeterminate -> true", + tiddlers: [{title: "Colors", colors: "orange yellow"}], + 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]]' checked='green' unchecked='red' default='green' checkactions=<> uncheckactions=<> />", + startsOutChecked: false, + expectedChange: { "Colors": { colors: "orange yellow green" } } + }, + { + testName: "filter mode true -> not indeterminate", + tiddlers: [{title: "Colors", colors: "green orange yellow"}], + 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]]' checked='green' unchecked='red' default='green' checkactions=<> uncheckactions=<> />", + startsOutChecked: true, + finalValue: false, + expectedChange: { "Colors": { colors: "orange yellow" } } + }, ]; const checkboxTestData = fieldModeTests.concat( diff --git a/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid b/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid index e0b019790..8f8d35f65 100644 --- a/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/CheckboxWidget.tid @@ -3,6 +3,8 @@ created: 20131024141900000 modified: 20220402023600000 tags: Widgets TriggeringWidgets colors: red orange yellow blue +fruits: bananas oranges grapes +vegetables: carrots potatoes title: CheckboxWidget type: text/vnd.tiddlywiki @@ -29,6 +31,7 @@ The content of the `<$checkbox>` widget is displayed within an HTML `