UNPKG

@eclipse-scout/core

Version:
428 lines (382 loc) 16.8 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 { Button, CheckBoxField, CloneOptions, CompositeField, DateField, dates, EventHandler, FormField, FormFieldSuppressStatus, HorizontalGrid, HtmlComponent, InitModelOf, LogicalGrid, LogicalGridData, LogicalGridLayout, LogicalGridLayoutConfig, Menu, ObjectIdProvider, ObjectOrChildModel, ObjectOrModel, PropertyChangeEvent, scout, SequenceBoxEventMap, SequenceBoxGridConfig, SequenceBoxModel, StatusOrModel, strings, ValueField, Widget } from '../../../index'; export class SequenceBox extends CompositeField implements SequenceBoxModel { declare model: SequenceBoxModel; declare eventMap: SequenceBoxEventMap; declare self: SequenceBox; layoutConfig: LogicalGridLayoutConfig; fields: FormField[]; htmlBody: HtmlComponent; boxErrorStatus: StatusOrModel; boxTooltipText: string; boxMenus: Menu[]; boxMenusVisible: boolean; protected _lastVisibleField: FormField; protected _isOverwritingStatusFromField: boolean; protected _isErrorStatusOverwritten: boolean; protected _isTooltipTextOverwritten: boolean; protected _isMenusOverwritten: boolean; protected _fieldPropertyChangeHandler: EventHandler<PropertyChangeEvent<any, FormField>>; protected _lastVisibleFieldSuppressStatusHandler: EventHandler<PropertyChangeEvent<FormFieldSuppressStatus>>; constructor() { super(); this._addWidgetProperties('fields'); this._addCloneProperties(['layoutConfig']); this.logicalGrid = scout.create(HorizontalGrid); this.layoutConfig = null; this.fields = []; this._fieldPropertyChangeHandler = this._onFieldPropertyChange.bind(this); this._lastVisibleFieldSuppressStatusHandler = this._onLastVisibleFieldSuppressStatusChange.bind(this); } protected override _init(model: InitModelOf<this>) { super._init(model); this._setLayoutConfig(this.layoutConfig); this._initDateFields(); this.setErrorStatus(this.errorStatus); this.setTooltipText(this.tooltipText); this.setMenus(this.menus); this.setMenusVisible(this.menusVisible); } /** * Initialize all DateFields in this SequenceBox with a meaningful autoDate, except fields which already have an autoDate provided by the model. */ protected _initDateFields() { let dateFields = this._getDateFields(); let newAutoDate: Date = null; for (let i = 0; i < dateFields.length; i++) { let currField = dateFields[i]; if (currField.autoDate) { // is the autoDate already set by the field's model remember to not change this value. currField.hasModelAutoDateSet = true; } if (!currField.hasModelAutoDateSet) { currField.setAutoDate(newAutoDate); } newAutoDate = this._getAutoDateProposal(currField); } } protected override _render() { this.addContainer(this.$parent, 'sequence-box'); this.addLabel(); this.addField(this.$parent.makeDiv()); this.addStatus(); this._handleStatus(); this.htmlBody = HtmlComponent.install(this.$field, this.session); this.htmlBody.setLayout(this._createBodyLayout()); for (let i = 0; i < this.fields.length; i++) { let field = this.fields[i]; field.labelUseUiWidth = true; field.on('propertyChange', this._fieldPropertyChangeHandler); field.render(this.$field); this._modifyLabel(field); // set each children layout data to logical grid data field.setLayoutData(new LogicalGridData(field)); } } protected override _renderProperties() { super._renderProperties(); this._renderLayoutConfig(); } protected _createBodyLayout(): LogicalGridLayout { return new LogicalGridLayout(this, this.layoutConfig); } protected override _remove() { this.fields.forEach(f => f.off('propertyChange', this._fieldPropertyChangeHandler)); if (this._lastVisibleField) { this._lastVisibleField.off('propertyChange:suppressStatus', this._lastVisibleFieldSuppressStatusHandler); } super._remove(); } override invalidateLogicalGrid(invalidateLayout?: boolean) { super.invalidateLogicalGrid(false); if (scout.nvl(invalidateLayout, true) && this.rendered) { this.htmlBody.invalidateLayoutTree(); } } protected override _setLogicalGrid(logicalGrid: LogicalGrid | string) { super._setLogicalGrid(logicalGrid); if (this.logicalGrid) { this.logicalGrid.setGridConfig(new SequenceBoxGridConfig()); } } 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(); } } protected _onFieldPropertyChange(event: PropertyChangeEvent<any, FormField>) { let visibilityChanged = (event.propertyName === 'visible'); if (scout.isOneOf(event.propertyName, ['errorStatus', 'tooltipText', 'visible', 'menus', 'menusVisible'])) { this._handleStatus(visibilityChanged); } else if (event.propertyName === 'value') { this._onFieldValueChange(event as PropertyChangeEvent<any, ValueField<any>>); } else if (event.propertyName === 'focused') { this._updateFocusedFromField(event.source); } } /** * Moves the status relevant properties from the last visible field to the SequenceBox. This makes sure that the fields inside the SequenceBox have the same size. */ protected _handleStatus(visibilityChanged?: boolean) { if (visibilityChanged && this._lastVisibleField) { // if there is a new last visible field, make sure the status is shown on the previously last one this._lastVisibleField.off('propertyChange:suppressStatus', this._lastVisibleFieldSuppressStatusHandler); this._lastVisibleField.setSuppressStatus(null); if (this._lastVisibleField.rendered) { this._lastVisibleField._renderErrorStatus(); this._lastVisibleField._renderTooltipText(); this._lastVisibleField._renderMenus(); } } this._lastVisibleField = this._getLastVisibleField(); if (!this._lastVisibleField) { return; } // Update the SequenceBox with the status relevant flags this._isOverwritingStatusFromField = true; if (this._lastVisibleField.errorStatus) { this.setErrorStatus(this._lastVisibleField.errorStatus); this._isErrorStatusOverwritten = true; } else { this.setErrorStatus(this.boxErrorStatus); this._isErrorStatusOverwritten = false; } if (this._lastVisibleField.hasStatusTooltip()) { this.setTooltipText(this._lastVisibleField.tooltipText); this._isTooltipTextOverwritten = true; } else { this.setTooltipText(this.boxTooltipText); this._isTooltipTextOverwritten = false; } let menuItems = this._lastVisibleField.getContextMenuItems(false); if (menuItems && menuItems.length > 0) { // Change owner to make sure menu won't be destroyed when setMenus is called this._updateBoxMenuOwner(this.fieldStatus); this.setMenus(menuItems); this.setMenusVisible(this._lastVisibleField.menusVisible); this._isMenusOverwritten = true; } else { this._updateBoxMenuOwner(this); this.setMenus(this.boxMenus); this.setMenusVisible(this.boxMenusVisible); this._isMenusOverwritten = false; } this._isOverwritingStatusFromField = false; // Make sure the last field won't display a status (but shows status CSS class) this._lastVisibleField.setSuppressStatus(FormField.SuppressStatus.ICON); this._lastVisibleField.on('propertyChange:suppressStatus', this._lastVisibleFieldSuppressStatusHandler); if (visibilityChanged) { // If the last field got invisible, make sure the new last field does not display a status anymore (now done by the seq box) if (this._lastVisibleField.rendered) { this._lastVisibleField._renderErrorStatus(); this._lastVisibleField._renderTooltipText(); this._lastVisibleField._renderMenus(); } } } /** * Marks the sequence box as "focused" when the given inner field is focused but does not have a visible label. * This allows the focus to be indicated on the sequence box label instead. */ protected _updateFocusedFromField(field: FormField) { if (field.visible && field.focused) { this.setFocused(this._computeSequenceBoxFocused(field)); } else { this.setFocused(false); } } /** * Called when the given inner field gained the focus. Computes whether the sequence box should be marked as "focused" as well. * Normally, we want to do this when the inner field does not have a visible label. */ protected _computeSequenceBoxFocused(focusedField: FormField) { // Special marker classes to override the default behavior if (this.hasCssClass('consider-inner-focus')) { return true; } if (this.hasCssClass('never-consider-inner-focus')) { return false; } // Traverse the list of visible inner fields until we reach the focused field. If we did not encounter a field with a visible // label until that point, mark the sequence box as focused. for (let field of this.fields.filter(f => f.visible)) { if (hasLabel(field)) { return false; } if (field === focusedField) { return true; } } return false; function hasLabel(field: FormField) { // Special marker classes to override the default behavior if (field.hasCssClass('consider-for-outer-focus')) { return false; } if (field.hasCssClass('never-consider-for-outer-focus') || field instanceof Button || field instanceof CheckBoxField) { return true; } if (!field.labelVisible || field.labelPosition === FormField.LabelPosition.ON_FIELD) { return false; } // Consider a label with only a single punctuation character as empty return strings.hasText(field.label) && !/^\s*\p{Punctuation}?\s*$/u.test(field.label); } } protected _onLastVisibleFieldSuppressStatusChange(e: PropertyChangeEvent<FormFieldSuppressStatus>) { // do not change suppressStatus e.preventDefault(); } override setErrorStatus(errorStatus: StatusOrModel) { if (this._isOverwritingStatusFromField && !this._isErrorStatusOverwritten) { // was not overwritten, will be overwritten now -> backup old value this.boxErrorStatus = this.errorStatus; } else if (!this._isOverwritingStatusFromField) { // directly changed on seq box -> update backed-up value this.boxErrorStatus = errorStatus; } if (this._isOverwritingStatusFromField || !this._isErrorStatusOverwritten) { // prevent setting value if directly changed on seq box and is already overwritten super.setErrorStatus(errorStatus); } } override setTooltipText(tooltipText: string) { if (this._isOverwritingStatusFromField && !this._isTooltipTextOverwritten) { // was not overwritten, will be overwritten now -> backup old value this.boxTooltipText = this.tooltipText; } else if (!this._isOverwritingStatusFromField) { // directly changed on seq box -> update backed-up value this.boxTooltipText = tooltipText; } if (this._isOverwritingStatusFromField || !this._isTooltipTextOverwritten) { // prevent setting value if directly changed on seq box and is already overwritten super.setTooltipText(tooltipText); } } override setMenus(menusOrModels: ObjectOrChildModel<Menu>[]) { // ensure menus are real and not just model objects let menus = this._createChildren(menusOrModels); if (this._isOverwritingStatusFromField && !this._isMenusOverwritten) { // was not overwritten, will be overwritten now -> backup old value this.boxMenus = this.menus; } else if (!this._isOverwritingStatusFromField) { // directly changed on seq box -> update backed-up value this.boxMenus = menus; } if (this._isOverwritingStatusFromField || !this._isMenusOverwritten) { // prevent setting value if directly changed on seq box and is already overwritten super.setMenus(menus); } } protected _updateBoxMenuOwner(newOwner: Widget) { this.boxMenus.forEach(menu => menu.setOwner(newOwner)); } override setMenusVisible(menusVisible: boolean) { if (this._isOverwritingStatusFromField && !this._isMenusOverwritten) { // was not overwritten, will be overwritten now -> backup old value this.boxMenusVisible = this.menusVisible; } else if (!this._isOverwritingStatusFromField) { // directly changed on seq box -> update backed-up value this.boxMenusVisible = menusVisible; } if (this._isOverwritingStatusFromField || !this._isMenusOverwritten) { // prevent setting value if directly changed on seq box and is already overwritten super.setMenusVisible(menusVisible); } } protected _getLastVisibleField(): FormField { let visibleFields = this.fields.filter(field => field.visible); if (visibleFields.length === 0) { return; } return visibleFields[visibleFields.length - 1]; } protected _onFieldValueChange(event: PropertyChangeEvent<any, ValueField<any>>) { if (event.source instanceof DateField) { this._onDateFieldValueChange(event as PropertyChangeEvent<Date, DateField>); } } protected _onDateFieldValueChange(event: PropertyChangeEvent<Date, DateField>) { // For a better user experience preselect a meaningful date on all following DateFields in the sequence box. let field = event.source; let dateFields = this._getDateFields(); let newAutoDate = this._getAutoDateProposal(field); for (let i = dateFields.indexOf(field) + 1; i < dateFields.length; i++) { let currField = dateFields[i]; if (!currField.hasModelAutoDateSet) { currField.setAutoDate(newAutoDate); } if (currField.value) { // only update fields in between the current field and the next field with a value set. Otherwise, already set autoDates would be overwritten. break; } } } protected _getDateFields(): (DateField & { hasModelAutoDateSet?: boolean })[] { return this.fields.filter(field => field instanceof DateField) as (DateField & { hasModelAutoDateSet?: boolean })[]; } protected _getAutoDateProposal(field: DateField): Date { let newAutoDate: Date = null; // if it's only a time field, add one hour, otherwise add one day if (field && field.value) { if (!field.hasDate && field.hasTime) { newAutoDate = dates.shiftTime(field.value, 1, 0, 0); } else { newAutoDate = dates.shift(field.value, 0, 0, 1); } } return newAutoDate; } // The new sequence-box sets the label to invisible on the model. protected _modifyLabel(field: FormField) { if (field instanceof CheckBoxField) { field.labelVisible = false; } if (field instanceof DateField) { // The DateField has two inputs ($dateField and $timeField), field.$field refers to the composite which is irrelevant here // In order to support aria-labelledby for date fields also, the individual inputs have to be linked with the label rather than the composite if (field.$dateField) { this._linkWithLabel(field.$dateField); } if (field.$timeField) { this._linkWithLabel(field.$timeField); } } else if (field.$field) { // If $field is set depends on the concrete field e.g. a group box does not have a $field this._linkWithLabel(field.$field); } } setFields(fields: ObjectOrChildModel<FormField>[]) { if (this.rendered) { throw new Error('Setting fields is not supported if sequence box is already rendered.'); } this.setProperty('fields', fields); } getFields(): FormField[] { return this.fields; } override clone(model: SequenceBoxModel, options?: CloneOptions): this { let clone = super.clone(model, options); this._deepCloneProperties(clone, 'fields', options); return clone; } } ObjectIdProvider.uuidPathSkipWidgets.add(SequenceBox);