UNPKG

@eclipse-scout/core

Version:
539 lines (463 loc) 17.1 kB
/* * Copyright (c) 2010, 2025 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import { aria, arrays, CloneOptions, FormField, HorizontalGrid, HtmlComponent, InitModelOf, LoadingSupport, LogicalGrid, LogicalGridData, LogicalGridLayout, LogicalGridLayoutConfig, LookupCall, LookupCallOrModel, LookupResult, LookupRow, ObjectIdProvider, ObjectOrChildModel, ObjectOrModel, objects, PropertyChangeEvent, RadioButton, RadioButtonGroupEventMap, RadioButtonGroupGridConfig, RadioButtonGroupLeftOrUpKeyStroke, RadioButtonGroupModel, RadioButtonGroupRightOrDownKeyStroke, scout, Status, ValueField } from '../../../index'; import $ from 'jquery'; export class RadioButtonGroup<TValue> extends ValueField<TValue> implements RadioButtonGroupModel<TValue> { declare model: RadioButtonGroupModel<TValue>; declare eventMap: RadioButtonGroupEventMap<TValue>; declare self: RadioButtonGroup<any>; layoutConfig: LogicalGridLayoutConfig; fields: FormField[]; radioButtons: RadioButton<TValue>[]; gridColumnCount: number; selectedButton: RadioButton<TValue>; lookupStatus: Status; lookupCall: LookupCall<TValue>; htmlBody: HtmlComponent; $body: JQuery; protected _lookupExecuted: boolean; protected _selectButtonLocked: boolean; protected _lookupInProgress: boolean; protected _currentLookupCall: LookupCall<TValue>; protected _buttonPropertyChangeHandler: (event: PropertyChangeEvent<any, RadioButton<TValue>>) => void; constructor() { super(); this.logicalGrid = scout.create(HorizontalGrid); this.layoutConfig = null; this.fields = []; this.radioButtons = []; this.gridColumnCount = RadioButtonGroup.DEFAULT_GRID_COLUMN_COUNT; this.selectedButton = null; this.$body = null; this.lookupStatus = null; this.lookupCall = null; this._lookupExecuted = false; this._selectButtonLocked = false; this._lookupInProgress = false; this._currentLookupCall = null; this._addWidgetProperties(['fields']); this._addCloneProperties(['lookupCall', 'layoutConfig', 'gridColumnCount']); this._buttonPropertyChangeHandler = this._onButtonPropertyChange.bind(this); } static DEFAULT_GRID_COLUMN_COUNT = -1; static ErrorCode = { NO_DATA: 1 } as const; protected override _init(model: InitModelOf<this>) { super._init(model); this._setLayoutConfig(this.layoutConfig); this._setGridColumnCount(this.gridColumnCount); } protected override _initValue(value: TValue) { if (this.lookupCall) { this._setLookupCall(this.lookupCall); } // must be called before value is set this._setFields(this.fields); super._initValue(value); } protected override _initKeyStrokeContext() { super._initKeyStrokeContext(); this.keyStrokeContext.registerKeyStrokes([ new RadioButtonGroupLeftOrUpKeyStroke(this), new RadioButtonGroupRightOrDownKeyStroke(this) ]); } protected _initButtons() { this.radioButtons = this.fields.filter(formField => formField instanceof RadioButton) as RadioButton<TValue>[]; this.radioButtons.forEach(this._initButton.bind(this)); } protected _initButton(button: RadioButton<TValue>) { if (button.events.count('propertyChange', this._buttonPropertyChangeHandler) === 0) { button.on('propertyChange', this._buttonPropertyChangeHandler); } if (button.selected) { this.setValue(button.radioValue); this.selectButton(button); if (button.focused) { this.setFocused(true); } } } protected override _render() { this.addContainer(this.$parent, 'radiobutton-group'); this.addLabel(); this.addMandatoryIndicator(); this.$body = this.$container.appendDiv('radiobutton-group-body'); aria.role(this.$body, 'radiogroup'); this.htmlBody = HtmlComponent.install(this.$body, this.session); this.htmlBody.setLayout(this._createBodyLayout()); // fields are rendered in _renderFields this.addField(this.$body); this.addStatus(); } protected override _renderProperties() { super._renderProperties(); this._renderFields(); this._renderLayoutConfig(); } protected _createBodyLayout(): LogicalGridLayout { return new LogicalGridLayout(this, this.layoutConfig); } protected override _setLogicalGrid(logicalGrid: LogicalGrid | string) { super._setLogicalGrid(logicalGrid); if (this.logicalGrid) { this.logicalGrid.setGridConfig(new RadioButtonGroupGridConfig()); } } override invalidateLogicalGrid(invalidateLayout?: boolean) { super.invalidateLogicalGrid(false); if (scout.nvl(invalidateLayout, true) && this.rendered) { this.htmlBody.invalidateLayoutTree(); } } setLayoutConfig(layoutConfig: ObjectOrModel<LogicalGridLayoutConfig>) { this.setProperty('layoutConfig', layoutConfig); } protected _setLayoutConfig(layoutConfig: ObjectOrModel<LogicalGridLayoutConfig>) { this._setProperty('layoutConfig', LogicalGridLayoutConfig.ensure(layoutConfig || {}).withSmallHgapDefaults()); LogicalGridLayoutConfig.initHtmlEnvChangeHandler(this, () => this.layoutConfig, layoutConfig => this.setLayoutConfig(layoutConfig)); } protected _renderLayoutConfig() { this.layoutConfig.applyToLayout(this.htmlBody.layout as LogicalGridLayout); if (this.rendered) { this.htmlBody.invalidateLayoutTree(); } } override isClearable(): boolean { return false; } getFields(): FormField[] { return this.fields; } override getFocusableElement(): HTMLElement | JQuery { // The first button may not be focusable because it is not selected and therefore has no tab index -> find the first focusable button return this.session.focusManager.findFirstFocusableElement(this.$container); } setFields(fields: ObjectOrChildModel<FormField>[]) { this.setProperty('fields', fields); } protected _setFields(fields: FormField[]) { this._setProperty('fields', fields); this._initButtons(); } protected _renderFields() { this._ensureLookupCallExecuted(); this.fields.forEach(function(formField) { formField.render(this.$body); // set each children layout data to logical grid data formField.setLayoutData(new LogicalGridData(formField)); this._linkWithLabel(formField.$field); }, this); this._provideTabIndex(); // depends on rendered fields this.invalidateLogicalGrid(); } protected override _renderEnabled() { super._renderEnabled(); this._provideTabIndex(); } /** * Set the selected (or first if none is selected) to tabbable */ protected _provideTabIndex() { let tabSet; this.radioButtons.forEach(function(radioButton) { if (radioButton.enabledComputed && this.enabledComputed && !tabSet) { radioButton.setTabbable(true); tabSet = radioButton; } else if (tabSet && this.enabledComputed && radioButton.enabledComputed && radioButton.selected) { tabSet.setTabbable(false); radioButton.setTabbable(true); tabSet = radioButton; } else { radioButton.setTabbable(false); } }, this); } setGridColumnCount(gridColumnCount: number) { this.setProperty('gridColumnCount', gridColumnCount); } protected _setGridColumnCount(gridColumnCount: number): boolean { if (gridColumnCount < 0) { gridColumnCount = this._calcDefaultGridColumnCount(); } if (gridColumnCount === this.gridColumnCount) { return false; } this._setProperty('gridColumnCount', gridColumnCount); this.invalidateLogicalGrid(); return true; } protected _calcDefaultGridColumnCount(): number { let height = 1, hints = this.gridDataHints; if (hints && hints.h > 1) { height = hints.h; } return Math.ceil(this.fields.length / height); } getButtonForRadioValue(radioValue: TValue): RadioButton<TValue> { if (radioValue === null) { return null; } return arrays.find(this.radioButtons, button => { return objects.equals(button.radioValue, radioValue); }); } /** * Search and then select the button with the corresponding radioValue */ protected override _validateValue(value: TValue): TValue { super._validateValue(value); if (!this.initialized && this.lookupCall) { // lookup call may not be started during field initialization. otherwise lookup prepare listeners cannot be attached. // do not validate now (as there are no buttons yet, because the lookup call has not yet been executed). // validation will be done later again when the lookup call is executed. return value; } let lookupScheduled = this._ensureLookupCallExecuted(); if (lookupScheduled) { // the first lookup was scheduled now: buttons are not yet available, not possible to select one. will be done later as soon as the lookup call is finished. return value; } // only show error if value is not null or undefined let buttonToSelect = this.getButtonForRadioValue(value); if (!buttonToSelect && value !== null && value !== undefined && !this._lookupInProgress) { throw this.session.text('InvalidValueMessageX', value); } return value; } protected override _valueChanged() { super._valueChanged(); // Don't select button during initialization if value is null to not override selected state of a button if (this.value !== null || this.initialized) { this.selectButton(this.getButtonForRadioValue(this.value)); } } selectFirstButton() { this.selectButtonByIndex(0); } selectLastButton() { this.selectButtonByIndex(this.radioButtons.length - 1); } selectButtonByIndex(index: number) { if (this.radioButtons.length && index >= 0 && index < this.radioButtons.length) { this.selectButton(this.radioButtons[index]); } } selectButton(radioButton: RadioButton<TValue>) { if (this.selectedButton === radioButton) { // Already selected return; } if (this._selectButtonLocked) { // Don't execute when triggered by this function to make sure the states are updated before firing the selectButton event return; } this._selectButtonLocked = true; // Deselect previously selected button if (this.selectedButton) { // Do not unset tabbable here, because at least one button has to be tabbable even if the button is deselected this.selectedButton.setSelected(false); } // Select new button if (radioButton) { let tabbableButton = this.getTabbableButton(); let needsFocus = false; if (tabbableButton) { // Only one button in the group should have a tab index -> remove it from the current tabbable button after the new button is tabbable. // If that button is focused the newly selected button needs to gain the focus otherwise the focus would fall back to the body. needsFocus = tabbableButton.isFocused(); } radioButton.setSelected(true); radioButton.setTabbable(true); if (needsFocus) { radioButton.focus(); } if (tabbableButton && tabbableButton !== radioButton) { tabbableButton.setTabbable(false); } } this._selectButtonLocked = false; this.setProperty('selectedButton', radioButton); } getTabbableButton(): RadioButton<TValue> { return arrays.find(this.radioButtons, button => { return button.visible && button.isTabbable(); }); } insertButton(radioButton: RadioButton<TValue>) { let newFields = this.fields.slice(); newFields.push(radioButton); this.setFields(newFields); } protected _onButtonPropertyChange(event: PropertyChangeEvent<any, RadioButton<TValue>>) { if (event.propertyName === 'selected') { let selected = event.newValue; if (selected) { this.setValue(event.source.radioValue); this.selectButton(event.source); } else if (event.source === this.selectedButton) { this.selectButton(null); } } else if (event.propertyName === 'focused') { this.setFocused(event.newValue); } } setLookupCall(lookupCall: LookupCallOrModel<TValue>) { this.setProperty('lookupCall', lookupCall); } protected _setLookupCall(lookupCall: LookupCallOrModel<TValue>) { this._setProperty('lookupCall', LookupCall.ensure(lookupCall, this.session)); this._lookupExecuted = false; if (this.rendered) { this._ensureLookupCallExecuted(); } } /** * @returns true if a lookup call execution has been scheduled now. false otherwise. */ protected _ensureLookupCallExecuted(): boolean { if (!this.lookupCall) { return false; } if (this._lookupExecuted) { return false; } this._lookupByAll(); return true; } protected override _createLoadingSupport(): LoadingSupport { return new LoadingSupport({ widget: this, $container: () => this.$body }); } protected _lookupByAll(): JQuery.Promise<LookupResult<TValue>> { if (!this.lookupCall) { return; } let deferred = $.Deferred(); this._executeLookup(this.lookupCall.cloneForAll(), true) .done(result => { this._lookupByAllDone(result); deferred.resolve(result); }); return deferred.promise(); } /** * A wrapper function around lookup calls used to set the _lookupInProgress flag, and display the state in the UI. */ protected _executeLookup(lookupCall: LookupCall<TValue>, abortExisting: boolean): JQuery.Promise<LookupResult<TValue>> { if (abortExisting && this._currentLookupCall) { this._currentLookupCall.abort(); } this._lookupInProgress = true; this.setLoading(true); this._currentLookupCall = lookupCall; this.trigger('prepareLookupCall', { lookupCall: lookupCall }); return lookupCall .execute() .always(() => { this._lookupInProgress = false; this._lookupExecuted = true; this._currentLookupCall = null; this.setLoading(false); this._clearLookupStatus(); }); } protected _lookupByAllDone(result: LookupResult<TValue>) { try { if (result.exception) { // Oops! Something went wrong while the lookup has been processed. this.setLookupStatus(Status.warning({ message: result.exception })); return; } this._populateRadioButtonGroup(result); } finally { this.trigger('lookupCallDone', { result: result }); } } protected _populateRadioButtonGroup(result: LookupResult<TValue>) { let lookupRows = result.lookupRows; let newFields = this.fields.slice(); lookupRows.forEach(lookupRow => { newFields.push(this._createLookupRowRadioButton(lookupRow)); }); this.setFields(newFields); // because the lookup call is asynchronous, reset the value so that it is revalidated. this.setValue(this.value); // also select the button (the line above does not change the value, therefore _valueChanged is not called) this.selectButton(this.getButtonForRadioValue(this.value)); } protected _clearLookupStatus() { this.setLookupStatus(null); } setLookupStatus(lookupStatus: Status) { this.setProperty('lookupStatus', lookupStatus); if (this.rendered) { this._renderErrorStatus(); } } protected override _errorStatus(): Status { return this.lookupStatus || this.errorStatus; } protected _createLookupRowRadioButton(lookupRow: LookupRow<TValue>): RadioButton<TValue> { let button: InitModelOf<RadioButton<TValue>> = { parent: this, label: lookupRow.text, radioValue: lookupRow.key }; if (lookupRow.iconId) { button.iconId = lookupRow.iconId; } if (lookupRow.tooltipText) { button.tooltipText = lookupRow.tooltipText; } if (lookupRow.backgroundColor) { button.backgroundColor = lookupRow.backgroundColor; } if (lookupRow.foregroundColor) { button.foregroundColor = lookupRow.foregroundColor; } if (lookupRow.font) { button.font = lookupRow.font; } if (lookupRow.enabled === false) { button.enabled = false; } if (lookupRow.active === false) { button.visible = false; } if (lookupRow.cssClass) { button.cssClass = lookupRow.cssClass; } let radioButton = scout.create((RadioButton<TValue>), button); radioButton.lookupRow = lookupRow; return radioButton; } override clone(model: RadioButtonGroupModel<TValue>, options?: CloneOptions): this { let clone = super.clone(model, options); this._deepCloneProperties(clone, 'fields', options); clone._initButtons(); return clone; } } ObjectIdProvider.uuidPathSkipWidgets.add(RadioButtonGroup);