281 lines
8.4 KiB
JavaScript
281 lines
8.4 KiB
JavaScript
// Model state manager for checkboxes or radio buttons
|
|
export default (Toolkit,_package)=>class Group {
|
|
|
|
// Instance fields
|
|
#_app; // Managed Toolkit.App
|
|
#_byComponent; // Mapping of values keyed by component
|
|
#_byValue; // Mapping of component sets keyed by value
|
|
#_checked; // Set of checked values
|
|
#_eventListeners; // Active event listeners
|
|
#_type; // Group type, either "checkbox" or "radio"
|
|
|
|
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
static {
|
|
_package.Group = {
|
|
onAction: (g,c)=>g.#_onAction(c)
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
this.#_app = null;
|
|
this.#_byComponent = new Map();
|
|
this.#_byValue = new Map();
|
|
this.#_checked = new Set();
|
|
this.#_eventListeners = null;
|
|
this.#_type = null;
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////// Properties ////////////////////////////////
|
|
|
|
// Number of components in the group
|
|
get size() { return this.#_byComponent.size; }
|
|
|
|
// Array of checked values or singular radio value (null if none)
|
|
get value() {
|
|
let ret = [... this.#_checked];
|
|
return this.#_type == "checkbox" ? ret : ret[0] ?? null;
|
|
}
|
|
|
|
// Specify the current checkbox values or radio value
|
|
set value(value) {
|
|
|
|
// Error checking
|
|
if (this.#_type == null)
|
|
throw new Error("There are no components in the group.");
|
|
|
|
// Update the radio value
|
|
if (this.#_type == "radio") {
|
|
if (value === null)
|
|
this.set(this.value, false);
|
|
this.set(value, true);
|
|
return;
|
|
}
|
|
|
|
// Update the checkbox values
|
|
let checked = new Set(Array.isArray(value) ? value : [ value ]);
|
|
for (value of this.#_byValue.keys())
|
|
this.set(value, checked.has(value));
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
// Component iterator
|
|
[Symbol.iterator]() { return this.components(); }
|
|
|
|
// Add a component to the group
|
|
add(ctrl, value) {
|
|
let size = this.#_byComponent.size;
|
|
|
|
// Error checking
|
|
if (this.#_byComponent.has(ctrl))
|
|
throw new Error("Control is already part of this group.");
|
|
if (!(ctrl instanceof Toolkit.Component))
|
|
throw new Error("Control must be a Toolkit.Component.");
|
|
if (this.#_isOtherGroup(ctrl))
|
|
throw new Error("Control is already part of another group.");
|
|
if (size != 0 && ctrl.app != this.#_app) {
|
|
throw new Error("All controls in the group must belong " +
|
|
"to the same Toolkit.App.");
|
|
}
|
|
|
|
// Determine the group type of the item being added
|
|
let type = null;
|
|
if (ctrl instanceof Toolkit.MenuItem) {
|
|
if (ctrl.type == "checkbox")
|
|
type = "checkbox";
|
|
else if (ctrl.type == "radio")
|
|
type = "radio";
|
|
}
|
|
|
|
// Error checking
|
|
if (type == null) {
|
|
throw new Error("Control must be of a checkbox or " +
|
|
"radio button or variety.");
|
|
}
|
|
if (size != 0 && type != this.#_type) {
|
|
throw new Error("All controls in the group must be of the same " +
|
|
"variety, either checkbox or radio button.");
|
|
}
|
|
|
|
// First component in the group
|
|
if (size == 0) {
|
|
this.#_app = ctrl.app;
|
|
this.#_type = type;
|
|
}
|
|
|
|
// Register the component
|
|
this.#_byComponent.set(ctrl, value);
|
|
if (ctrl instanceof Toolkit.MenuItem)
|
|
_package.MenuItem.setGroup(ctrl, this);
|
|
|
|
// Register the value, add the component to the value's list
|
|
if (!this.#_byValue.has(value))
|
|
this.#_byValue.set(value, new Set());
|
|
this.#_byValue.get(value).add(ctrl);
|
|
if (ctrl instanceof Toolkit.MenuItem)
|
|
_package.MenuItem.setChecked(ctrl, this.#_checked.has(value));
|
|
}
|
|
|
|
// Register an event listener
|
|
addEventListener(type, listener) {
|
|
|
|
// Input validation
|
|
type = String(type);
|
|
if (!(listener instanceof Function))
|
|
throw new TypeError("listener must be a function.");
|
|
|
|
// The event listener is already registered
|
|
if (this.#_eventListeners?.get(type)?.includes(listener))
|
|
return;
|
|
|
|
// Establish a set for the listener type
|
|
this.#_eventListeners ??= new Map();
|
|
if (!this.#_eventListeners.has(type)) {
|
|
let listeners = [];
|
|
listeners.inner = new Map();
|
|
this.#_eventListeners.set(type, listeners);
|
|
}
|
|
|
|
// Register the listener
|
|
let listeners = this.#_eventListeners.get(type);
|
|
let inner = e=>{
|
|
e[Toolkit.group ] = this;
|
|
e[Toolkit.target] = Toolkit.component(e.target);
|
|
listener(e);
|
|
};
|
|
listeners.push(listener);
|
|
listeners.inner.set(listener, inner);
|
|
}
|
|
|
|
// Component iterator
|
|
*components() {
|
|
let ret = [... this.#_byComponent.keys()];
|
|
for (let ctrl of ret)
|
|
yield ctrl;
|
|
}
|
|
|
|
// Determine whether a model value is currently checked
|
|
is(value) {
|
|
return this.#_checked.has(value);
|
|
}
|
|
|
|
// Remove a component from the group
|
|
remove(ctrl) {
|
|
|
|
// Error checking
|
|
if (!this.#_byComponent.has(ctrl))
|
|
return;
|
|
|
|
// Working variables
|
|
let value = this.#_byComponent.get(ctrl);
|
|
let components = this.#_byValue .get(value);
|
|
|
|
// Unregister the component
|
|
this.#_byComponent.delete(ctrl);
|
|
|
|
// No components remain
|
|
if (this.#_byComponent.size == 0) {
|
|
this.#_app = null;
|
|
this.#_type = null;
|
|
}
|
|
|
|
// Un-register the value
|
|
components.delete(ctrl);
|
|
if (components.size == 0) {
|
|
this.#_checked.delete(value);
|
|
this.#_byValue.delete(value);
|
|
}
|
|
|
|
// Detach the component from the group
|
|
if (ctrl instanceof MenuItem)
|
|
_package.MenuItem.setGroup(ctrl, null);
|
|
}
|
|
|
|
// Specify whether a model value is currently checked
|
|
set(value, checked) {
|
|
|
|
// Error checking
|
|
if (!this.#_byValue.has(value))
|
|
return;
|
|
|
|
// Checked state is not changing
|
|
checked = !!checked;
|
|
if (this.#_checked.has(value) == checked)
|
|
return;
|
|
|
|
// Un-check the previous radio value
|
|
if (this.#_type == "radio" && this.#_checked.size == 1) {
|
|
let checked = [... this.#_checked][0];
|
|
if (checked != value)
|
|
this.set(checked, false);
|
|
}
|
|
|
|
// Update components
|
|
for (let ctrl of this.#_byValue.get(value)) {
|
|
if (ctrl instanceof Toolkit.MenuItem)
|
|
_package.MenuItem.setChecked(ctrl, checked);
|
|
}
|
|
|
|
// Update model
|
|
this.#_checked[checked ? "add" : "delete"](value);
|
|
}
|
|
|
|
// Value iterator
|
|
*values() {
|
|
if (this.#_byComponent.size == 0)
|
|
return;
|
|
let ret = this.values;
|
|
if (this.#_type != "checkbox")
|
|
ret = [ ret ];
|
|
for (let value of ret)
|
|
yield value;
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Package Methods /////////////////////////////
|
|
|
|
// Control was activated by the user
|
|
#_onAction(ctrl) {
|
|
this.set(this.#_byComponent.get(ctrl),
|
|
this.type == "radio" ? true : !this.#_checked.has(ctrl));
|
|
if (!this.#_eventListeners.has("action"))
|
|
return;
|
|
let listeners = this.#_eventListeners.get("action");
|
|
for (let listener of listeners) {
|
|
listener = listeners.inner.get(listener);
|
|
let e = new Event("group", { bubbles: true, cancelable: true });
|
|
Object.defineProperties(e, { target: { value: ctrl.element } });
|
|
listener(e);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Private Methods /////////////////////////////
|
|
|
|
// Generate a custom event object
|
|
#_emit(type, ctrl, properties) {
|
|
let e = new Event(type, { bubbles: true, cancelable: true });
|
|
Object.defineProperties(e, { target: { value: ctrl.element } });
|
|
Object.assign(e, properties);
|
|
return e;
|
|
}
|
|
|
|
// Determine whether a component belongs to another Group
|
|
#_isOtherGroup(ctrl) {
|
|
let group = null;
|
|
if (ctrl instanceof Toolkit.MenuItem)
|
|
group = ctrl.group;
|
|
return group != null && group != this;
|
|
}
|
|
|
|
};
|