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