import {TOutletChangeData} from '../outlet_manager_controller/outlet_manager_controller' import SyncedBooleanAttributesController from '../synced_boolean_attributes_controller/synced_boolean_attributes_controller' type TOptionKey = string | number type TActiveOptions = {[k: TOptionKey]: boolean} export interface OptionsOutlet extends SyncedBooleanAttributesController { select: (e: Event, updateTo?: TOutletChangeData) => void } export default class OptionsController extends SyncedBooleanAttributesController implements OptionsOutlet { static outlets = SyncedBooleanAttributesController.outlets static targets = ['option'] static values = { ...SyncedBooleanAttributesController.values, activeOptions: Object, // The currently active elements in TActiveOptions format isMulti: {type: Boolean, default: false}, // Allows more than one selection toggleable: {type: Boolean, default: false}, // If true, selecting the same value twice will turn it off. If false, nothing happens } // Targets declare readonly optionTargets: Array // Values declare activeOptionsValue: TActiveOptions declare readonly isMultiValue: boolean declare readonly toggleableValue: boolean select(event: Event, updateTo: TOutletChangeData = {}) { const activeOptions = updateTo.data for (const index in this.optionTargets) { const target = this.optionTargets[index] const wasSelected = target === event.currentTarget const isCurrentlyActive = this.getValueForElement(target) ?? false const optionKey = this.#getElementKey(target) const shouldChangeState = activeOptions !== undefined ? !!activeOptions[optionKey] !== isCurrentlyActive : this.#shouldChangeState(isCurrentlyActive, wasSelected) if (shouldChangeState) { const willBeActive = !isCurrentlyActive this.updateAttributesForElement(target, willBeActive) willBeActive ? this.#activateKey(optionKey) : this.#deactivateKey(optionKey) } } this.sendToOutlets(event, {...updateTo, data: this.activeOptionsValue}) } optionTargetConnected(element: Element) { const key = this.#getElementKey(element) const isActive = this.activeOptionsValue[key] || this.doesElementHaveOnAttrs(element) if (isActive) { this.#activateKey(key) } else { this.#deactivateKey(key) } this.updateAttributesForElement(element, isActive) } #shouldChangeState(isCurrentlyActive: boolean, wasSelected: boolean) { if (!wasSelected && !isCurrentlyActive) { // Not currently on and wasn't selected, no reason to change return false } if (!wasSelected && isCurrentlyActive && this.isMultiValue) { // It wasn't selected and it is on. However, multi value is on so just leave it return false } if (wasSelected && isCurrentlyActive && !this.toggleableValue) { // It was selected and it's on. But toggle isn't on so just leave it return false } // Wasn't selected and it's active but only one can be active // Was selected and it's active but toggling behavior means it should deactivate // Was selected and it's not on, so it just needs to be turned on return true } #activateKey(key: TOptionKey) { this.activeOptionsValue = {...this.activeOptionsValue, [key]: true} } #deactivateKey(key: TOptionKey) { const copy = {...this.activeOptionsValue} delete copy[key] this.activeOptionsValue = copy } #getElementKey(element: Element) { const elementValue = element?.getAttribute('data-option-value') if (elementValue) return elementValue const content = (element as HTMLElement)?.textContent if (content === null) { throw new Error( `${element.tagName} was given as an options target without a data-option-value or textContent. One must be provided to identify the target.`, ) } return content.trim() } getValueForElement(element: Element) { const optionKey = this.#getElementKey(element) return this.activeOptionsValue[optionKey] ?? false } getState() { return this.activeOptionsValue } outletUpdate = this.select }