shrooms-vb-web/toolkit/Group.js

281 lines
8.4 KiB
JavaScript
Raw Permalink Normal View History

2025-02-18 22:39:36 +00:00
// 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;
}
};