/* * Copyright (c) 2010, 2026 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 { AbstractLayout, Action, aria, arrays, clipboard, CloneOptions, Column, Device, dragAndDrop, DragAndDropHandler, DragAndDropOptions, DropType, EnumObject, EventHandler, fields, FieldStatus, FormFieldClipboardExportEvent, FormFieldEventMap, FormFieldLayout, FormFieldModel, FormFieldValidationResultProvider, GridData, GroupBox, HierarchyChangeEvent, HtmlComponent, InitModelOf, KeyStrokeContext, LoadingSupport, Menu, menus as menuUtil, ObjectOrChildModel, ObjectOrModel, ObjectOrType, objects, Predicate, PropertyChangeEvent, scout, Status, StatusMenuMapping, StatusOrModel, strings, styles, TableRow, Tooltip, tooltips, TooltipSupport, TreeVisitor, TreeVisitResult, Widget } from '../../index'; import $ from 'jquery'; /** * Base class for all form-fields. */ export class FormField extends Widget implements FormFieldModel { declare model: FormFieldModel; declare eventMap: FormFieldEventMap; declare self: FormField; dropType: DropType; dropMaximumSize: number; empty: boolean; errorStatus: Status; fieldStyle: FormFieldStyle; gridData: GridData; gridDataHints: GridData; mode: FormFieldMode; fieldStatus: FieldStatus; keyStrokes: Action[]; displayText: string; label: string; labelVisible: boolean; labelPosition: FormFieldLabelPosition; labelWidthInPixel: number; labelUseUiWidth: boolean; labelHtmlEnabled: boolean; mandatory: boolean; statusMenuMappings: StatusMenuMapping[]; menus: Menu[]; menusVisible: boolean; defaultMenuTypes: string[]; /** If set to true, the field needs to be saved. This will be computed by {@link computeSaveNeeded}. */ saveNeeded: boolean; checkSaveNeeded: boolean; lifecycleBoundary: boolean; statusPosition: FormFieldStatusPosition; statusVisible: boolean; suppressStatus: FormFieldSuppressStatus; /** If set to true, {@link saveNeeded} will return true as well, even if the value has not been changed. */ touched: boolean; tooltipText: string; font: string; foregroundColor: string; backgroundColor: string; labelFont: string; labelForegroundColor: string; labelBackgroundColor: string; tooltipAnchor: FormFieldTooltipAnchor; onFieldTooltipOptionsCreator: (this: FormField) => InitModelOf; dragAndDropHandler: DragAndDropHandler; validationResultProvider: FormFieldValidationResultProvider; $label: JQuery; /** * Note the difference between $field and $fieldContainer: * - $field points to the input-field (typically a browser-text field) * - $fieldContainer could point to the same input-field or when the field is a composite, * to the parent DIV of that composite. For instance: the multi-line-smartfield is a * composite with an input-field and a DIV showing the additional lines. In that case $field * points to the input-field and $fieldContainer to the parent DIV of the input-field. * This property should be used primarily for layout-functionality. */ $field: JQuery; $clearIcon: JQuery; $fieldContainer: JQuery; $icon: JQuery; $pseudoStatus: JQuery; /** * The status is used for error-status, tooltip-icon and menus. */ $status: JQuery; $mandatory: JQuery; $maskedIndicator: JQuery; protected _menuPropertyChangeHandler: EventHandler>; protected _hierarchyChangeHandler: EventHandler; constructor() { super(); this.dropType = DropType.NONE; this.dropMaximumSize = dragAndDrop.DEFAULT_DROP_MAXIMUM_SIZE; this.empty = true; this.errorStatus = null; this.fieldStyle = FormField.DEFAULT_FIELD_STYLE; this.gridData = null; this.gridDataHints = new GridData(); this.mode = FormField.Mode.DEFAULT; this.keyStrokes = []; this.label = null; this.labelVisible = true; this.labelPosition = FormField.LabelPosition.DEFAULT; this.labelWidthInPixel = 0; this.labelUseUiWidth = false; this.labelHtmlEnabled = false; this.mandatory = false; this.statusMenuMappings = []; this.menus = []; this.menusVisible = true; this.defaultMenuTypes = []; this.saveNeeded = false; this.checkSaveNeeded = true; this.lifecycleBoundary = false; this.statusPosition = FormField.StatusPosition.DEFAULT; this.statusVisible = true; this.suppressStatus = null; this.touched = false; this.tooltipText = null; this.tooltipAnchor = FormField.TooltipAnchor.DEFAULT; this.onFieldTooltipOptionsCreator = null; this.validationResultProvider = this._createValidationResultProvider(); this.$label = null; this.$field = null; this.$fieldContainer = null; this.$icon = null; this.$status = null; this._addWidgetProperties(['keyStrokes', 'menus', 'statusMenuMappings']); this._addCloneProperties(['dropType', 'dropMaximumSize', 'errorStatus', 'fieldStyle', 'gridDataHints', 'gridData', 'label', 'labelVisible', 'labelPosition', 'labelWidthInPixel', 'labelUseUiWidth', 'mandatory', 'mode', 'saveNeeded', 'touched', 'statusVisible', 'statusPosition', 'statusMenuMappings', 'tooltipText', 'tooltipAnchor']); this._menuPropertyChangeHandler = this._onMenuPropertyChange.bind(this); this._hierarchyChangeHandler = this._onHierarchyChange.bind(this); } static FieldStyle = { CLASSIC: 'classic', ALTERNATIVE: 'alternative' } as const; static SuppressStatus = { /** * Suppress status on icon and field (CSS class). */ ALL: 'all', /** * Suppress status on icon, but still show status on field (CSS class). */ ICON: 'icon', /** * Suppress status on field (CSS class), but still show status as icon. */ FIELD: 'field' } as const; /** Global variable to make it easier to adjust the default field style for all fields */ static DEFAULT_FIELD_STYLE = FormField.FieldStyle.ALTERNATIVE; static StatusPosition = { DEFAULT: 'default', TOP: 'top' } as const; static LabelPosition = { DEFAULT: 0, LEFT: 1, ON_FIELD: 2, RIGHT: 3, TOP: 4, BOTTOM: 5 } as const; static TooltipAnchor = { DEFAULT: 'default', ON_FIELD: 'onField' } as const; static LabelWidth = { DEFAULT: 0, UI: -1 } as const; // see org.eclipse.scout.rt.client.ui.form.fields.IFormField.FULL_WIDTH static FULL_WIDTH = 0; static Mode = { DEFAULT: 'default', CELLEDITOR: 'celleditor' } as const; protected override _createKeyStrokeContext(): KeyStrokeContext { return new KeyStrokeContext(); } protected override _createLoadingSupport(): LoadingSupport { return new LoadingSupport({ widget: this }); } protected override _init(model: InitModelOf) { super._init(model); this.resolveConsts([{ property: 'labelPosition', constType: FormField.LabelPosition }]); this.resolveTextKeys(['label', 'tooltipText']); this._setValidationResultProvider(this.validationResultProvider); this._setKeyStrokes(this.keyStrokes); this._setMenus(this.menus); this._setErrorStatus(this.errorStatus); this._setGridDataHints(this.gridDataHints); this._setGridData(this.gridData); this._updateEmpty(); this._watchFieldHierarchy(); this.fieldStatus = this._createFieldStatus(); this.fieldStatus.on('remove', () => { this.$status = null; }); } protected _createFieldStatus(): FieldStatus { return scout.create(FieldStatus, { parent: this, position: this.statusPosition }); } protected override _destroy() { this._unwatchFieldHierarchy(); super._destroy(); } protected override _initProperty(propertyName: string, value: any) { if ('gridDataHints' === propertyName) { this._initGridDataHints(value); } else { super._initProperty(propertyName, value); } } /** * This function extends the default grid data hints of the form field. * The default values for grid data hints are set in the constructor of the FormField and its subclasses. * When the given gridDataHints is a plain object, we extend our default values. When gridDataHints is * already instanceof GridData we overwrite default values completely. */ private _initGridDataHints(gridDataHints: GridData) { if (gridDataHints instanceof GridData) { this.gridDataHints = gridDataHints; } else if (objects.isObject(gridDataHints)) { $.extend(this.gridDataHints, gridDataHints); } else { this.gridDataHints = gridDataHints; } } /** * All subclasses of FormField should implement a _render method. It should call the various add* methods provided by the FormField class. * * A possible _render implementation could look like this. *
   * this.addContainer(this.$parent, 'form-field');
   * this.addLabel();
   * this.addMandatoryIndicator();
   * this.addField(this.$parent.makeDiv('foo', 'bar'));
   * this.addStatus();
   * 
*/ protected override _render() { // Render all the necessary parts of a form field. // Subclasses typically override _render completely and add these parts by themselves this.addContainer(this.$parent); this.addLabel(); this.addMandatoryIndicator(); this.addField(this.$parent.makeDiv()); this.addStatus(); } protected override _renderProperties() { super._renderProperties(); this._renderMandatory(); this._renderTooltipText(); this._renderErrorStatus(); this._renderMenus(); this._renderLabel(); this._renderLabelVisible(); this._renderStatusVisible(); this._renderStatusPosition(); this._renderFont(); this._renderForegroundColor(); this._renderBackgroundColor(); this._renderLabelFont(); this._renderLabelForegroundColor(); this._renderLabelBackgroundColor(); this._renderGridData(); this._renderFieldStyle(); } protected override _remove() { super._remove(); this._removeField(); this._removeStatus(); this._removeLabel(); this._removeIcon(); this.removeMandatoryIndicator(); dragAndDrop.uninstallDragAndDropHandler(this); } /** @see FormFieldModel.fieldStyle */ setFieldStyle(fieldStyle: FormFieldStyle) { this.setProperty('fieldStyle', fieldStyle); } protected _renderFieldStyle() { this._renderFieldStyleInternal(this.$container); this._renderFieldStyleInternal(this.$fieldContainer); this._renderFieldStyleInternal(this.$field); if (this.rendered) { // See _renderLabelPosition why it is necessary to invalidate parent as well. let htmlCompParent = this.htmlComp.getParent(); if (htmlCompParent) { htmlCompParent.invalidateLayoutTree(); } this.invalidateLayoutTree(); } } protected _renderFieldStyleInternal($element: JQuery) { if (!$element) { return; } $element.toggleClass('alternative', this.fieldStyle === FormField.FieldStyle.ALTERNATIVE); } /** @see FormFieldModel.mandatory */ setMandatory(mandatory: boolean) { this.setProperty('mandatory', mandatory); } protected _renderMandatory() { this.$container.toggleClass('mandatory', this.mandatory); aria.required(this.$field, this.mandatory || null); } /** * Override this function to return another error status property. * The default implementation returns the property 'errorStatus'. */ protected _errorStatus(): Status { return this.errorStatus; } /** @see FormFieldModel.errorStatus */ setErrorStatus(errorStatus: StatusOrModel) { this.setProperty('errorStatus', errorStatus); } protected _setErrorStatus(errorStatus: StatusOrModel) { errorStatus = Status.ensure(errorStatus); this._setProperty('errorStatus', errorStatus); } /** * Adds the given (functional) error status to the list of error status. Prefer this function over #setErrorStatus * when you don't want to mess with the internal error states of the field (parsing, validation). */ addErrorStatus(errorStatus: string | Status) { if (typeof errorStatus === 'string') { errorStatus = this._createErrorStatus(errorStatus); } if (!(errorStatus instanceof Status)) { throw new Error('errorStatus is not a Status'); } let status = this._errorStatus(); if (status) { status = status.ensureChildren(); // new instance is required for property change } else { status = Status.ok('Root'); } status.addStatus(errorStatus); this.setErrorStatus(status); } /** * Create an error status with severity {@link Status.Severity.ERROR} containing the given message. * * @param message The message for the error status. * @returns containing the given message. */ protected _createErrorStatus(message: string): Status { return Status.error(message); } /** * Whether the error status is or has the given status type. */ containsStatus(statusType: abstract new() => Status): boolean { if (!this.errorStatus) { return false; } return this.errorStatus.containsStatus(statusType); } /** @see FormFieldModel.suppressStatus */ setSuppressStatus(suppressStatus: FormFieldSuppressStatus) { this.setProperty('suppressStatus', suppressStatus); } protected _renderSuppressStatus() { this._renderErrorStatus(); } /** * @returns Whether or not error status icon is suppressed */ protected _isSuppressStatusIcon(): boolean { return scout.isOneOf(this.suppressStatus, FormField.SuppressStatus.ALL, FormField.SuppressStatus.ICON); } /** * @returns Whether or not error status CSS class is suppressed on field */ protected _isSuppressStatusField(): boolean { return scout.isOneOf(this.suppressStatus, FormField.SuppressStatus.ALL, FormField.SuppressStatus.FIELD); } /** * Removes all status (incl. children) with the given type. */ removeErrorStatus(statusType: new() => Status) { this.removeErrorStatusByPredicate(status => status instanceof statusType); } removeErrorStatusByPredicate(predicate: Predicate) { let status = this._errorStatus(); if (!status) { return; } if (status.containsStatusByPredicate(predicate)) { let newStatus = status.clone(); newStatus.removeAllStatusByPredicate(predicate); // If no other status remains -> clear error status if (newStatus.hasChildren()) { this.setErrorStatus(newStatus); } else { this.clearErrorStatus(); } } } clearErrorStatus() { this.setErrorStatus(null); } /** @internal */ _renderErrorStatus() { this._updateFieldStatus(); this._updateErrorStatusClasses(); this._updateAriaDescAndErrorMessage(); } protected _updateErrorStatusClasses() { this._updateErrorStatusClassesOnElement(this.$container); this._updateErrorStatusClassesOnElement(this.$field); } protected _updateErrorStatusClassesOnElement($element: JQuery) { if (!$element) { return; } // Add error status classes to the given element // this.fieldStatus.status may not always be the same as this._errorStatus(), e.g. tooltip is converted to an info status // -> don't use this.fieldStatus to compute the status classes so tooltip can be distinguished from info-status, see FormField.less let status = this._errorStatus(); if (this._isSuppressStatusField()) { status = null; } FieldStatus.updateHasStatus($element, status); } /** @see FormFieldModel.tooltipText */ setTooltipText(tooltipText: string) { this.setProperty('tooltipText', tooltipText); } /** @internal */ _renderTooltipText() { this._updateTooltip(); } /** @see FormFieldModel.tooltipAnchor */ setTooltipAnchor(tooltipAnchor: FormFieldTooltipAnchor) { this.setProperty('tooltipAnchor', tooltipAnchor); } protected _renderTooltipAnchor() { this._updateTooltip(); } protected _updateTooltip() { let hasTooltipText = this.hasStatusTooltip(); this.$container.toggleClass('has-tooltip', hasTooltipText); this.$field?.toggleClass('has-tooltip', hasTooltipText); this._updateFieldStatus(); this._updateAriaDescAndErrorMessage(); if (this.$fieldContainer) { if (this.hasOnFieldTooltip()) { let creatorFunc = this.onFieldTooltipOptionsCreator || this._createOnFieldTooltipOptions; tooltips.install(this.$fieldContainer, creatorFunc.call(this)); } else { tooltips.uninstall(this.$fieldContainer); } } } protected _updateAriaDescAndErrorMessage() { this._updateAriaDescAndErrorMessageOnElement(this.$field); } protected _updateAriaDescAndErrorMessageOnElement($field: JQuery) { let status = this._errorStatus(); let maskedDescription = this.masked ? this.session.text('YouAreNotAllowedToReadThisData') : null; let errorSeverity = status?.severity === Status.Severity.ERROR; // no status description when severity is ERROR, to make it consistent with the visual behavior let statusMessageOrTooltip = errorSeverity ? null : status?.message || this.tooltipText; let description = strings.join(' ', maskedDescription, statusMessageOrTooltip) || null; if (errorSeverity && this.fieldStatus.tooltip?.$content) { aria.invalid($field, true); aria.linkElementWithErrorMessage($field, this.fieldStatus.tooltip.$content); } else { aria.invalid($field, null); aria.removeErrorMessage($field); } aria.description($field, description); } hasStatusTooltip(): boolean { return this.tooltipAnchor === FormField.TooltipAnchor.DEFAULT && strings.hasText(this.tooltipText); } hasOnFieldTooltip(): boolean { return this.tooltipAnchor === FormField.TooltipAnchor.ON_FIELD && strings.hasText(this.tooltipText); } /** @see FormFieldModel.onFieldTooltipOptionsCreator */ setOnFieldTooltipOptionsCreator(onFieldTooltipOptionsCreator: (this: FormField) => InitModelOf) { this.onFieldTooltipOptionsCreator = onFieldTooltipOptionsCreator; } protected _createOnFieldTooltipOptions(): InitModelOf { return { parent: this, text: this.tooltipText, arrowPosition: 50 }; } protected override _renderVisible() { super._renderVisible(); if (this.rendered) { // Make sure error status is hidden / shown when visibility changes this._renderErrorStatus(); } } /** @see FormFieldModel.label */ setLabel(label: string) { this.setProperty('label', label); } protected _renderLabel() { let label = this.label; if (this.labelPosition === FormField.LabelPosition.ON_FIELD) { this._renderPlaceholder(); if (this.$label) { this.$label.text(''); } aria.label(this.$field, this.label); } else if (this.$label) { this._removePlaceholder(); // Make sure an empty label has the same height as the other labels, especially important for top labels this.$label .contentOrNbsp(this.labelHtmlEnabled, label, 'empty') .toggleClass('top', this.labelPosition === FormField.LabelPosition.TOP); // Invalidate layout if label width depends on its content if (this.labelUseUiWidth || this.labelWidthInPixel === FormField.LabelWidth.UI) { this.invalidateLayoutTree(); } } } /** * Renders an empty label for button-like fields that don't have a regular label but which do want to support the 'labelVisible' * property in order to provide some layout-flexibility. Makes sure the empty label has the same height as the other labels, * which is especially important for top labels. */ protected _renderEmptyLabel() { this.$label .html(' ') .toggleClass('top', this.labelPosition === FormField.LabelPosition.TOP); } protected _renderPlaceholder($field?: JQuery) { $field = scout.nvl($field, this.$field); if ($field) { $field.placeholder(this.label); } } /** * @param $field argument is required by DateField.js, when not set this.$field is used */ protected _removePlaceholder($field?: JQuery) { $field = scout.nvl($field, this.$field); if ($field) { $field.placeholder(''); } } /** @see FormFieldModel.labelVisible */ setLabelVisible(visible: boolean) { this.setProperty('labelVisible', visible); } protected _renderLabelVisible() { let visible = this.labelVisible; this._renderChildVisible(this.$label, visible); this.$container.toggleClass('label-hidden', !visible); if (this.rendered && this.labelPosition === FormField.LabelPosition.TOP) { // See _renderLabelPosition why it is necessary to invalidate parent as well. let htmlCompParent = this.htmlComp.getParent(); if (htmlCompParent) { htmlCompParent.invalidateLayoutTree(); } } } /** @see FormFieldModel.labelWidthInPixel */ setLabelWidthInPixel(labelWidthInPixel: number) { this.setProperty('labelWidthInPixel', labelWidthInPixel); } protected _renderLabelWidthInPixel() { this.invalidateLayoutTree(); } /** @see FormFieldModel.labelUseUiWidth */ setLabelUseUiWidth(labelUseUiWidth: number) { this.setProperty('labelUseUiWidth', labelUseUiWidth); } protected _renderLabelUseUiWidth() { this.invalidateLayoutTree(); } /** @see FormFieldModel.statusVisible */ setStatusVisible(visible: boolean) { this.setProperty('statusVisible', visible); } protected _renderStatusVisible() { this._updateFieldStatus(); } /** @see FormFieldModel.statusPosition */ setStatusPosition(statusPosition: FormFieldStatusPosition) { this.setProperty('statusPosition', statusPosition); } protected _renderStatusPosition() { this._updateFieldStatus(); } /** * The tooltip of the {@link fieldStatus}, if it is shown. */ tooltip(): Tooltip { return this.fieldStatus.tooltip; } protected _updateFieldStatus() { let statusVisible = this._computeStatusVisible(); this.fieldStatus.setPosition(this.statusPosition); this.fieldStatus.setVisible(statusVisible); if (!statusVisible) { this.fieldStatus.setMenus(null); this.fieldStatus.setStatus(null); return; } let menus: Menu[]; let errorStatus = this._errorStatus(); let autoRemove = true; let status: Status = null; if (errorStatus) { // If the field is used as a cell editor in an editable table, then no validation errors should be shown. // (parsing and validation will be handled by the cell/column itself) if (this.mode === FormField.Mode.CELLEDITOR) { return; } status = errorStatus; autoRemove = !status.isError(); menus = this._getMenusForStatus(errorStatus); } else if (this.hasStatusTooltip()) { status = scout.create(Status, { message: this.tooltipText, severity: Status.Severity.INFO }); // If there are menus, show them in the tooltip. But only if there is a tooltipText, don't do it if there is an error status. // Menus make most likely no sense if an error status is displayed menus = this.getContextMenuItems(); } else { menus = this.getContextMenuItems(); } if (!this.menusVisible) { menus = []; } this.fieldStatus.update(status, menus, autoRemove, this._isInitialShowStatus()); } protected _isInitialShowStatus(): boolean { return !!this._errorStatus(); } /** * Computes whether the $status should be visible based on statusVisible, errorStatus and tooltip. * -> errorStatus and tooltip override statusVisible, so $status may be visible event though statusVisible is set to false */ protected _computeStatusVisible(): boolean { let status = this._errorStatus(), statusVisible = this.statusVisible, hasStatus = !!status, hasTooltip = this.hasStatusTooltip(); return !this._isSuppressStatusIcon() && this.visible && (statusVisible || hasStatus || hasTooltip || (this._hasMenus() && this.menusVisible)); } protected _renderChildVisible($child: JQuery, visible: boolean): boolean { if (!$child) { return; } if ($child.isVisible() !== visible) { $child.setVisible(visible); this.invalidateLayoutTree(); return true; } } /** @see FormFieldModel.labelPosition */ setLabelPosition(labelPosition: FormFieldLabelPosition) { this.setProperty('labelPosition', labelPosition); } // Don't include in renderProperties, it is not necessary to execute it initially because the positioning is done by _renderLabel protected _renderLabelPosition() { this._renderLabel(); if (this.rendered) { // Necessary to invalidate parent as well if parent uses the logical grid. // LogicalGridData uses another row height depending on the label position let htmlCompParent = this.htmlComp.getParent(); if (htmlCompParent) { htmlCompParent.invalidateLayoutTree(); } this.invalidateLayoutTree(); } } /** @see FormFieldModel.labelHtmlEnabled */ setLabelHtmlEnabled(labelHtmlEnabled: boolean) { this.setProperty('labelHtmlEnabled', labelHtmlEnabled); } protected _renderLabelHtmlEnabled() { // Render the label again when html enabled changes dynamically this._renderLabel(); } protected override _renderEnabled() { super._renderEnabled(); if (this.$field) { this.$field.setEnabled(this.enabledComputed); } this._installOrUninstallDragAndDropHandler(); } get masked(): boolean { return !this.enabledComputed && this.disabledStyle === Widget.DisabledStyle.MASKED; } protected override _renderDisabledStyle() { this._renderDisabledStyleInternal(this.$container); this._renderDisabledStyleInternal(this.$fieldContainer); this._renderDisabledStyleInternal(this.$field); this._renderDisabledStyleInternal(this.$mandatory); this._renderMaskedIndicator(); } /** @see FormFieldModel.font */ setFont(font: string) { this.setProperty('font', font); } protected _renderFont() { styles.legacyFont(this, this.$field); } /** @see FormFieldModel.foregroundColor */ setForegroundColor(foregroundColor: string) { this.setProperty('foregroundColor', foregroundColor); } protected _renderForegroundColor() { styles.legacyForegroundColor(this, this.$field); } /** @see FormFieldModel.backgroundColor */ setBackgroundColor(backgroundColor: string) { this.setProperty('backgroundColor', backgroundColor); } protected _renderBackgroundColor() { styles.legacyBackgroundColor(this, this.$field); } /** @see FormFieldModel.labelFont */ setLabelFont(labelFont: string) { this.setProperty('labelFont', labelFont); } protected _renderLabelFont() { styles.legacyFont(this, this.$label, 'label'); } /** @see FormFieldModel.labelForegroundColor */ setLabelForegroundColor(labelForegroundColor: string) { this.setProperty('labelForegroundColor', labelForegroundColor); } protected _renderLabelForegroundColor() { styles.legacyForegroundColor(this, this.$label, 'label'); } /** @see FormFieldModel.labelBackgroundColor */ setLabelBackgroundColor(labelBackgroundColor: string) { this.setProperty('labelBackgroundColor', labelBackgroundColor); } protected _renderLabelBackgroundColor() { styles.legacyBackgroundColor(this, this.$label, 'label'); } /** @see FormFieldModel.gridDataHints */ setGridDataHints(gridData: ObjectOrModel) { this.setProperty('gridDataHints', gridData); } protected _setGridDataHints(gridData: ObjectOrModel) { this._setProperty('gridDataHints', GridData.ensure(gridData || new GridData())); } protected _renderGridDataHints() { this.parent.invalidateLogicalGrid(); } /** @internal */ _setGridData(gridData: ObjectOrModel) { this._setProperty('gridData', GridData.ensure(gridData || new GridData())); } protected _renderGridData() { if (this.rendered) { let htmlCompParent = this.htmlComp.getParent(); if (htmlCompParent) { // may be null if $container is detached htmlCompParent.invalidateLayoutTree(); } } } /** @see FormFieldModel.menus */ setMenus(menus: ObjectOrChildModel[]) { this.setProperty('menus', menus); } protected _setMenus(menus: Menu | Menu[]) { menus = arrays.ensure(menus); this.menus.forEach(menu => menu.off('propertyChange', this._menuPropertyChangeHandler)); this.updateKeyStrokes(menus, this.menus); this._setProperty('menus', menus); this.menus.forEach(menu => menu.on('propertyChange', this._menuPropertyChangeHandler)); } insertMenu(menuToInsert: ObjectOrChildModel) { this.insertMenus([menuToInsert]); } insertMenus(menusToInsert: ObjectOrChildModel[]) { menuUtil.insertMenus(this, menusToInsert); } deleteMenu(menuToDelete: Menu) { this.deleteMenus([menuToDelete]); } deleteMenus(menusToDelete: Menu[]) { menuUtil.deleteMenus(this, menusToDelete); } protected _onMenuPropertyChange(event: PropertyChangeEvent) { if (event.propertyName === 'visible' && this.rendered) { this._updateMenus(); } } getContextMenuItems(onlyVisible = true): Menu[] { let currentMenuTypes = this.getCurrentMenuTypes(); if (currentMenuTypes.length) { return menuUtil.filter(this.menus, currentMenuTypes, {onlyVisible, defaultMenuTypes: this.defaultMenuTypes}); } if (onlyVisible) { return this.menus.filter(menu => menu.visible); } return this.menus; } protected _getMenusForStatus(status: Status): Menu[] { return this.statusMenuMappings.filter(mapping => { if (!mapping.menu || !mapping.menu.visible) { return false; } // Show the menus which are mapped to the status code and severity (if set) return (mapping.codes.length === 0 || mapping.codes.indexOf(status.code) > -1) && (mapping.severities.length === 0 || mapping.severities.indexOf(status.severity) > -1); }).map(mapping => mapping.menu); } protected _hasMenus(): boolean { return !!(this.menus && this.getContextMenuItems().length > 0); } /** @internal */ _updateMenus() { if (!this.rendered && !this.rendering) { return; } this._updateFieldStatus(); this.fieldStatus.updateHasMenus(this.$container); } /** @internal */ _renderMenus() { this._updateMenus(); } protected _renderStatusMenuMappings() { this._updateMenus(); } setMenusVisible(menusVisible: boolean) { this.setProperty('menusVisible', menusVisible); } protected _setMenusVisible(menusVisible: boolean) { this._setProperty('menusVisible', menusVisible); } protected _renderMenusVisible() { this._updateMenus(); } getCurrentMenuTypes(): string[] { return this._getCurrentMenuTypes(); } protected _getCurrentMenuTypes(): string[] { return []; } protected _setKeyStrokes(keyStrokes: Action[]) { this.updateKeyStrokes(keyStrokes, this.keyStrokes); this._setProperty('keyStrokes', keyStrokes); } /** * May be overridden to explicitly provide a tooltip $parent * @internal */ _$tooltipParent(): JQuery { // Will be determined by the tooltip itself return undefined; } /** @internal */ _hideStatusMessage() { this.fieldStatus.hideTooltip(); } /** * Sets the focus on this field. If the field is not rendered, the focus will be set as soon as it is rendered. * @returns true if the element could be focused, false if not */ override focus(): boolean { if (!this.rendered) { this.session.layoutValidator.schedulePostValidateFunction(this.focus.bind(this)); return false; } if (!this.enabledComputed) { return false; } return this.session.focusManager.requestFocus(this.get$Focusable()); } override get$Focusable(): JQuery { return this.$field; } protected _onFieldFocus(event: JQuery.FocusEvent) { this.setFocused(true); } protected _onFieldBlur(event: JQuery.BlurEvent) { this.setFocused(false); } /** * When calling this function, the same should happen as when clicking into the field. It is used when the label is clicked.
* The most basic action is focusing the field but this may differ from field to field. */ activate() { if (!this.enabledComputed || !this.rendered) { return; } // Explicitly don't use this.focus() because this.focus uses the focus manager which may be disabled (e.g. on mobile devices) this.get$Focusable()?.focus(); } override get$Scrollable(): JQuery { return this.$field; } getParentGroupBox(): GroupBox { return this.findParent(GroupBox); } getParentField(): FormField { return this.findParent(FormField); } /** * Appends a LABEL element to this.$container and sets the this.$label property. */ addLabel() { this.$label = this.$container.appendElement('