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 `