// 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; } };