Skip to content

Commit

Permalink
adds support for checked list and select box, with controller
Browse files Browse the repository at this point in the history
  • Loading branch information
johnspackman committed Jun 10, 2021
1 parent 4f1dc70 commit 8d1566d
Show file tree
Hide file tree
Showing 16 changed files with 1,230 additions and 7 deletions.
348 changes: 348 additions & 0 deletions source/class/qx/data/controller/CheckedList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2021-2021 Zenesis Limited https://www.zenesis.com
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* John Spackman (github.com/johnspackman)
************************************************************************ */

/**
* Extension of `qx.data.controller.List` which adds support for `qx.ui.form.CheckedList`
* and `qx.ui.form.CheckedSelectBox`.
*
* The principal is that the underlying `List` controller implementation has a model which
* is the complete array of items that can be selected, and that array is used to populate
* the UI widget (ie as normal).
*
* The `checked` psuedo property in this `CheckedList` controller relates to the checked
* property of the UI widget.
*/
qx.Class.define("qx.data.controller.CheckedList", {
extend: qx.data.controller.List,

/**
* Constructor
*
* @param model {qx.data.Array?null} the model array
* @param widget {qx.ui.core.Widget?null} the widget target
* @param path {String} the path in the model for the caption
*/
construct(model, widget, path) {
this.base(arguments, null, widget, path);
this.setChecked(new qx.data.Array());
if (model)
this.setModel(model);
},

properties: {
checked: {
init: null,
nullable: true,
check: "qx.data.Array",
event: "changeChecked",
apply: "_applyChecked"
},

/**
* The path to the property which holds the information that should be
* shown as a label for a tag for a checked item. This is only needed if
* used with a CheckedSelectBox, and only if live updates of the label
* are required.
*/
checkedLabelPath: {
check: "String",
apply: "__updateTags",
nullable: true
},


/**
* The path to the property which holds the information that should be
* shown as an icon for a tag for a checked item. This is only needed if
* used with a CheckedSelectBox, and only if live updates of the label
* are required.
*/
checkedIconPath: {
check: "String",
apply: "__updateTags",
nullable: true
},


/**
* A map containing the options for the checkedLabel binding. The possible keys
* can be found in the {@link qx.data.SingleValueBinding} documentation.
*/
checkedLabelOptions: {
apply: "__updateTags",
nullable: true
},


/**
* A map containing the options for the checked icon binding. The possible keys
* can be found in the {@link qx.data.SingleValueBinding} documentation.
*/
checkedIconOptions: {
apply: "__updateTags",
nullable: true
}
},

members: {
_applyChecked(value, oldValue) {
if (oldValue)
oldValue.removeListener("change", this.__onCheckedChange, this);
if (value)
value.addListener("change", this.__onCheckedChange, this);
this._updateChecked();
},

/**
* @Override
*/
_createItem() {
var delegate = this.getDelegate();
var item;

// check if a delegate and a create method is set
if (delegate != null && delegate.createItem != null) {
item = delegate.createItem();
} else {
item = new qx.ui.form.CheckBox();
}

// if there is a configure method, invoke it
if (delegate != null && delegate.configureItem != null) {
delegate.configureItem(item);
}

return item;
},

/**
* Event handler for changes to the checked array
*
* @param evt {qx.event.type.Data} the event
*/
__onCheckedChange(evt) {
let data = evt.getData();
if (data.type == "order")
return;
this._updateChecked();
},

/**
* @Override
*/
update() {
this.base(arguments);
this._updateChecked();
},

/**
* @Override
*/
_setFilter(value, old) {
this.base(arguments, value, old);
this.__syncModelChecked = true;
qx.ui.core.queue.Widget.add(this);
},

/**
* @Override
*/
syncWidget() {
this.base(arguments);
if (this.__syncModelChecked) {
this._updateChecked();
}
this.__syncModelChecked = null;
},

/**
* @Override
*/
_applyModel(value, oldValue) {
if (!value || !value.getLength()) {
let checked = this.getChecked();
if (checked)
checked.removeAll();
}
this.base(arguments, value, oldValue);
this._updateChecked();
},

/**
* @Override
*/
_applyTarget(value, oldValue) {
this.base(arguments, value, oldValue);
if (oldValue) {
oldValue.removeListener("changeChecked", this.__onTargetCheckedChange, this);
if (qx.Class.supportsEvent(oldValue.constructor, "attachResultsTag")) {
oldValue.removeListener("attachResultsTag", this.__onTargetAttachResultsTag, this);
oldValue.removeListener("detachResultsTag", this.__onTargetDetachResultsTag, this);
}
}
if (value) {
value.addListener("changeChecked", this.__onTargetCheckedChange, this);
if (qx.Class.supportsEvent(value.constructor, "attachResultsTag")) {
value.addListener("attachResultsTag", this.__onTargetAttachResultsTag, this);
value.addListener("detachResultsTag", this.__onTargetDetachResultsTag, this);
}
}
},

/**
* Event handler for changes in the target widget's `checked` property
*/
__onTargetCheckedChange(evt) {
if (this.__inUpdateChecked)
return;
let target = this.getTarget();
let replacement = [];
target.getChecked().forEach(item => {
let itemModel = item.getModel();
if (itemModel)
replacement.push(itemModel);
});
let checked = this.getChecked();
if (checked)
checked.replace(replacement);
},

/**
* Event handler for changes in the target widget's `attachResults` property
*/
__onTargetAttachResultsTag(evt) {
let { tagWidget, item } = evt.getData();
item.setUserData(this.classname + ".tagWidget", tagWidget);
this.__attachTag(tagWidget, item);
},

/**
* Event handler for changes in the target widget's `detachResults` property
*/
__onTargetDetachResultsTag(evt) {
let { tagWidget, item } = evt.getData();
this.__detachTag(tagWidget, item);
item.setUserData(this.classname + ".tagWidget", null);
},

/**
* Updates all tags in the target widget
*/
__updateTags() {
let target = this.getTarget();
if (!target) {
return;
}
target.getChecked().forEach(item => {
let tagWidget = item.getUserData(this.classname + ".tagWidget");
this.__detachTag(tagWidget, item);
this.__attachTag(tagWidget, item);
});
},

/**
* Attaches a single tag; used to bind to the tag so that live updates to the underlying model are reflected in tag names
*
* @param tagWidget {qx.ui.core.Widget} the widget which is the tag
* @param item {qx.ui.core.Widget} the list item that lists the model item that this tag is for
*/
__attachTag(tagWidget, item) {
let itemModel = item.getModel();
let bindData = { };
if (this.getCheckedLabelPath()) {
bindData.checkedLabelId = itemModel.bind(this.getCheckedLabelPath(), tagWidget, "label", this.getCheckedLabelOptions());
}
if (this.getCheckedIconPath()) {
bindData.checkedIconId = itemModel.bind(this.getCheckedIconPath(), tagWidget, "label", this.getCheckedIconOptions());
}
itemModel.setUserData(this.classname + ".bindData", bindData);
},

/**
* Detaches a single tag, inverse of `__attachTag`
*
* @param tagWidget {qx.ui.core.Widget} the widget which is the tag
* @param item {qx.ui.core.Widget} the list item that lists the model item that this tag is for
*/
__detachTag(tagWidget, item) {
let itemModel = item.getModel();
let bindData = itemModel.getUserData(this.classname + ".bindData");
if (bindData) {
if (bindData.checkedLabelId) {
itemModel.removeBinding(bindData.checkedLabelId);
}
if (bindData.checkedIconId) {
itemModel.removeBinding(bindData.checkedIconId);
}
itemModel.setUserData(this.classname + ".bindData", null);
}
},

/**
* Updates the checked widget items to match the array of checked model items
*/
_updateChecked() {
let target = this.getTarget();
if (!target) {
return;
}

if (this.__inUpdateChecked) {
return;
}
this.__inUpdateChecked = true;
try {
// Maps of the widget item, indexed by the hashcode of the model item
let children = {};
let toUncheck = {};

target.getChildren().forEach(item => {
let itemModel = item.getModel();
if (itemModel) {
let hash = itemModel.toHashCode();
children[hash] = item;
if (item.getValue())
toUncheck[hash] = item;
}
});

let toRemove = [];
let checked = this.getChecked();
if (checked) {
checked.forEach(itemModel => {
let hash = itemModel.toHashCode();
if (itemModel) {
delete toUncheck[hash];
if (children[hash]) {
children[hash].setValue(true);
} else {
toRemove.push(itemModel);
}
}
});
Object.values(toUncheck).forEach(item => item.setValue(false));
toRemove.forEach(item => checked.remove(item));
}
} finally {
this.__inUpdateChecked = false;
}
}


}
});
29 changes: 29 additions & 0 deletions source/class/qx/theme/tangible/Appearance.js
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,35 @@ qx.Theme.define("qx.theme.tangible.Appearance", {
},


/*
---------------------------------------------------------------------------
CHECKED SELECT BOX
---------------------------------------------------------------------------
*/
"checked-selectbox": "selectbox",
"checked-selectbox/allNone": {
include: "button",
style: function(states) {
return {
padding: [ 2, 10 ],
textColor: "text-on-primary"
};
}
},
"checked-selectbox/tag": "tag",
"tag": {
alias: "button",
include: "button",
style: function(states) {
return {
padding: [ 1, 7 ],
margin: 0,
decorator: "tag"
};
}
},


/*
---------------------------------------------------------------------------
COMBO BOX
Expand Down
Loading

0 comments on commit 8d1566d

Please sign in to comment.