blob: 991ac72c85ec0b2164463cc65ae9155b8a6f7e21 [file] [log] [blame]
/*
* Copyright (c) 2014-2018 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
import {
arrays,
clipboard,
Device,
dragAndDrop,
Event,
fields,
FormFieldLayout,
GridData,
GroupBox,
HtmlComponent,
KeyStrokeContext,
LoadingSupport,
objects,
scout,
Status,
strings,
styles,
tooltips,
Widget
} from '../../index';
import * as $ from 'jquery';
/**
* Abstract class for all form-fields.
* @abstract
*/
export default class FormField extends Widget {
constructor() {
super();
this.dropType = 0;
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.labelHtmlEnabled = false;
this.mandatory = false;
this.statusMenuMappings = [];
this.menus = [];
this.menusVisible = true;
this.preventInitialFocus = false;
this.requiresSave = false;
this.statusPosition = FormField.StatusPosition.DEFAULT;
this.statusVisible = true;
this.suppressStatus = false;
this.touched = false;
this.tooltipText = null;
this.tooltipAnchor = FormField.TooltipAnchor.DEFAULT;
this.onFieldTooltipOptionsCreator = null;
this.suppressStatus = null;
this.$label = null;
/**
* 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 a 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.
*/
this.$field = null;
this.$fieldContainer = null;
this.$icon = null;
/**
* The status is used for error-status, tooltip-icon and menus.
*/
this.$status = null;
/**
* Some browsers don't support copying text from disabled input fields. If such a browser is detected
* and this flag is true (default is false), an overlay DIV is rendered over disabled fields which
* provides a custom copy context menu that opens the ClipboardForm.
*/
this.disabledCopyOverlay = false;
this.$disabledCopyOverlay = null;
this._addWidgetProperties(['keyStrokes', 'menus', 'statusMenuMappings']);
this._addCloneProperties(['dropType', 'dropMaximumSize', 'errorStatus', 'fieldStyle', 'gridDataHints', 'gridData', 'label', 'labelVisible', 'labelPosition',
'labelWidthInPixel', 'mandatory', 'mode', 'preventInitialFocus', 'requiresSave', 'touched', 'statusVisible', 'statusPosition', 'statusMenuMappings',
'tooltipText', 'tooltipAnchor']);
this._menuPropertyChangeHandler = this._onMenuPropertyChange.bind(this);
}
static FieldStyle = {
CLASSIC: 'classic',
ALTERNATIVE: 'alternative'
};
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'
};
/** 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'
};
static LabelPosition = {
DEFAULT: 0,
LEFT: 1,
ON_FIELD: 2,
RIGHT: 3,
TOP: 4
};
static TooltipAnchor = {
DEFAULT: 'default',
ON_FIELD: 'onField'
};
static LabelWidth = {
DEFAULT: 0,
UI: -1
};
// see org.eclipse.scout.rt.client.ui.form.fields.IFormField.FULL_WIDTH
static FULL_WIDTH = 0;
static Mode = {
DEFAULT: 'default',
CELLEDITOR: 'celleditor'
};
static SEVERITY_CSS_CLASSES = 'has-error has-warning has-info has-ok';
/**
* @override
* @returns {KeyStrokeContext}
*/
_createKeyStrokeContext() {
return new KeyStrokeContext();
}
/**
* @override
*/
_createLoadingSupport() {
return new LoadingSupport({
widget: this
});
}
_init(model) {
super._init(model);
this.resolveConsts([{
property: 'labelPosition',
constType: FormField.LabelPosition
}]);
this.resolveTextKeys(['label', 'tooltipText']);
this._setKeyStrokes(this.keyStrokes);
this._setMenus(this.menus);
this._setErrorStatus(this.errorStatus);
this._setGridDataHints(this.gridDataHints);
this._setGridData(this.gridData);
this._updateEmpty();
}
_initProperty(propertyName, value) {
if ('gridDataHints' === propertyName) {
this._initGridDataHints(value);
} else {
super._initProperty(propertyName, value);
}
}
/**
* This function <strong>extends</strong> 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.
* @param gridDataHints
* @private
*/
_initGridDataHints(gridDataHints) {
if (gridDataHints instanceof GridData) {
this.gridDataHints = gridDataHints;
} else if (objects.isPlainObject(gridDataHints)) {
$.extend(this.gridDataHints, gridDataHints);
} else {
this.gridDataHints = gridDataHints;
}
}
/**
* All sub-classes of FormField must implement a _render method. The default implementation
* will throw an Error when _render is called. The _render method should call the various add*
* methods provided by the FormField class. A possible _render implementation could look like this.
*
* <pre>
* this.addContainer(this.$parent, 'form-field');
* this.addLabel();
* this.addField(this.$parent.makeDiv('foo', 'bar'));
* this.addMandatoryIndicator();
* this.addStatus();
* </pre>
*/
_render() {
throw new Error('sub-classes of FormField must implement a _render method');
}
_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._renderPreventInitialFocus();
this._renderFieldStyle();
}
_remove() {
super._remove();
this._removeField();
this._removeStatus();
this._removeLabel();
this._removeIcon();
this.removeMandatoryIndicator();
this._removeDisabledCopyOverlay();
this._uninstallDragAndDropHandler();
}
setFieldStyle(fieldStyle) {
this.setProperty('fieldStyle', fieldStyle);
}
_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.
var htmlCompParent = this.htmlComp.getParent();
if (htmlCompParent) {
htmlCompParent.invalidateLayoutTree();
}
this.invalidateLayoutTree();
}
}
_renderFieldStyleInternal($element) {
if (!$element) {
return;
}
$element.toggleClass('alternative', this.fieldStyle === FormField.FieldStyle.ALTERNATIVE);
}
setMandatory(mandatory) {
this.setProperty('mandatory', mandatory);
}
_renderMandatory() {
this.$container.toggleClass('mandatory', this.mandatory);
}
/**
* Override this function to return another error status property.
* The default implementation returns the property 'errorStatus'.
*
* @return {Status}
*/
_errorStatus() {
return this.errorStatus;
}
setErrorStatus(errorStatus) {
this.setProperty('errorStatus', errorStatus);
}
_setErrorStatus(errorStatus) {
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).
*
* @param errorStatus
*/
addErrorStatus(errorStatus) {
if (!(errorStatus instanceof Status)) {
throw new Error('errorStatus is not a Status');
}
var 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);
}
setSuppressStatus(suppressStatus) {
this.setProperty('suppressStatus', suppressStatus);
}
_renderSuppressStatus() {
this._renderErrorStatus();
}
/**
* @returns {boolean} Whether or not error status icon is suppressed
*/
_isSuppressStatusIcon() {
return scout.isOneOf(this.suppressStatus, FormField.SuppressStatus.ALL, FormField.SuppressStatus.ICON);
}
/**
* @returns {boolean} Whether or not error status CSS class is suppressed on field
*/
_isSuppressStatusField() {
return scout.isOneOf(this.suppressStatus, FormField.SuppressStatus.ALL, FormField.SuppressStatus.FIELD);
}
/**
* Removes all status (incl. children) with the given type.
* @param {object} statusType
*/
removeErrorStatus(statusType) {
this.removeErrorStatusPredicate(function(status) {
return status instanceof statusType;
});
}
removeErrorStatusPredicate(predicate) {
var status = this._errorStatus();
if (!status) {
return;
}
if (status.containsStatusPredicate(predicate)) {
var newStatus = status.clone();
newStatus.removeAllStatusPredicate(predicate);
// If no other status remains -> clear error status
if (newStatus.hasChildren()) {
this.setErrorStatus(newStatus);
} else {
this.clearErrorStatus();
}
}
}
clearErrorStatus() {
this.setErrorStatus(null);
}
_renderErrorStatus() {
var status = this._errorStatus(),
hasStatus = !!status,
statusClass = (hasStatus && !this._isSuppressStatusField()) ? 'has-' + status.cssClass() : '';
this._updateErrorStatusClasses(statusClass, hasStatus);
this._updateFieldStatus();
}
_updateErrorStatusClasses(statusClass, hasStatus) {
this._updateErrorStatusClassesOnElement(this.$container, statusClass, hasStatus);
this._updateErrorStatusClassesOnElement(this.$field, statusClass, hasStatus);
}
_updateErrorStatusClassesOnElement($element, statusClass, hasStatus) {
if (!$element) {
return;
}
$element
.removeClass(FormField.SEVERITY_CSS_CLASSES)
.addClass(statusClass, hasStatus);
}
setTooltipText(tooltipText) {
this.setProperty('tooltipText', tooltipText);
}
_renderTooltipText() {
this._updateTooltip();
}
setTooltipAnchor(tooltipAnchor) {
this.setProperty('tooltipAnchor', tooltipAnchor);
}
_renderTooltipAnchor() {
this._updateTooltip();
}
_updateTooltip() {
var hasTooltipText = this.hasStatusTooltip();
this.$container.toggleClass('has-tooltip', hasTooltipText);
if (this.$field) {
this.$field.toggleClass('has-tooltip', hasTooltipText);
}
this._updateFieldStatus();
if (this.$fieldContainer) {
if (this.hasOnFieldTooltip()) {
var creatorFunc = this.onFieldTooltipOptionsCreator || this._createOnFieldTooltipOptions;
tooltips.install(this.$fieldContainer, creatorFunc.call(this));
} else {
tooltips.uninstall(this.$fieldContainer);
}
}
}
hasStatusTooltip() {
return this.tooltipAnchor === FormField.TooltipAnchor.DEFAULT &&
strings.hasText(this.tooltipText);
}
hasOnFieldTooltip() {
return this.tooltipAnchor === FormField.TooltipAnchor.ON_FIELD &&
strings.hasText(this.tooltipText);
}
setOnFieldTooltipOptionsCreator(onFieldTooltipOptionsCreator) {
this.onFieldTooltipOptionsCreator = onFieldTooltipOptionsCreator;
}
_createOnFieldTooltipOptions() {
return {
parent: this,
text: this.tooltipText,
arrowPosition: 50
};
}
/**
* @override
*/
_renderVisible() {
super._renderVisible();
if (this.rendered) {
// Make sure error status is hidden / shown when visibility changes
this._renderErrorStatus();
}
}
setLabel(label) {
this.setProperty('label', label);
}
_renderLabel() {
var label = this.label;
if (this.labelPosition === FormField.LabelPosition.ON_FIELD) {
this._renderPlaceholder();
if (this.$label) {
this.$label.text('');
}
} 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.
*/
_renderEmptyLabel() {
this.$label
.html('&nbsp;')
.toggleClass('top', this.labelPosition === FormField.LabelPosition.TOP);
}
_renderPlaceholder($field) {
$field = scout.nvl($field, this.$field);
if ($field) {
$field.placeholder(this.label);
}
}
/**
* @param $field (optional) argument is required by DateField.js, when not set this.$field is used
*/
_removePlaceholder($field) {
$field = scout.nvl($field, this.$field);
if ($field) {
$field.placeholder('');
}
}
setLabelVisible(visible) {
this.setProperty('labelVisible', visible);
}
_renderLabelVisible() {
var 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.
var htmlCompParent = this.htmlComp.getParent();
if (htmlCompParent) {
htmlCompParent.invalidateLayoutTree();
}
}
}
setLabelWidthInPixel(labelWidthInPixel) {
this.setProperty('labelWidthInPixel', labelWidthInPixel);
}
_renderLabelWidthInPixel() {
this.invalidateLayoutTree();
}
setStatusVisible(visible) {
this.setProperty('statusVisible', visible);
}
_renderStatusVisible() {
this._updateFieldStatus();
}
setStatusPosition(statusPosition) {
this.setProperty('statusPosition', statusPosition);
}
_renderStatusPosition(statusPosition) {
this._updateFieldStatus();
}
_tooltip() {
if (this.fieldStatus) {
return this.fieldStatus.tooltip;
}
return null;
}
_updateFieldStatus() {
if (!this.fieldStatus) {
return;
}
// compute status
var menus,
errorStatus = this._errorStatus(),
status = null,
statusVisible = this._computeStatusVisible(),
autoRemove = true;
this.fieldStatus.setPosition(this.statusPosition);
this.fieldStatus.setVisible(statusVisible);
if (!statusVisible) {
return;
}
if (errorStatus) {
// If the field is used as a cell editor in a 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.OK
});
// 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._getCurrentMenus();
} else {
// 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._getCurrentMenus();
}
this.fieldStatus.update(status, menus, autoRemove, this._isInitialShowStatus());
}
_isInitialShowStatus() {
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
*/
_computeStatusVisible() {
var status = this._errorStatus(),
statusVisible = this.statusVisible,
hasStatus = !!status,
hasTooltip = this.hasStatusTooltip();
return !this._isSuppressStatusIcon() && this.visible && (statusVisible || hasStatus || hasTooltip || (this._hasMenus() && this.menusVisible));
}
_renderChildVisible($child, visible) {
if (!$child) {
return;
}
if ($child.isVisible() !== visible) {
$child.setVisible(visible);
this.invalidateLayoutTree();
return true;
}
}
setLabelPosition(labelPosition) {
this.setProperty('labelPosition', labelPosition);
}
// Don't include in renderProperties, it is not necessary to execute it initially because the positioning is done by _renderLabel
_renderLabelPosition(position) {
this._renderLabel();
if (this.rendered) {
// Necessary to invalidate parent as well if parent uses the logical grid.
// LogicalGridData uses another row height depending of the label position
var htmlCompParent = this.htmlComp.getParent();
if (htmlCompParent) {
htmlCompParent.invalidateLayoutTree();
}
this.invalidateLayoutTree();
}
}
setLabelHtmlEnabled(labelHtmlEnabled) {
this.setProperty('labelHtmlEnabled', labelHtmlEnabled);
}
_renderLabelHtmlEnabled() {
// Render the label again when html enabled changes dynamically
this._renderLabel();
}
/**
* @override
*/
_renderEnabled() {
super._renderEnabled();
if (this.$field) {
this.$field.setEnabled(this.enabledComputed);
}
this._updateDisabledCopyOverlay();
this._updateDropType();
}
/**
* @override Wigdet.js
*/
_renderDisabledStyle() {
this._renderDisabledStyleInternal(this.$container);
this._renderDisabledStyleInternal(this.$fieldContainer);
this._renderDisabledStyleInternal(this.$field);
this._renderDisabledStyleInternal(this.$mandatory);
}
setFont(font) {
this.setProperty('font', font);
}
_renderFont() {
styles.legacyFont(this, this.$field);
}
setForegroundColor(foregroundColor) {
this.setProperty('foregroundColor', foregroundColor);
}
_renderForegroundColor() {
styles.legacyForegroundColor(this, this.$field);
}
setBackgroundColor(backgroundColor) {
this.setProperty('backgroundColor', backgroundColor);
}
_renderBackgroundColor() {
styles.legacyBackgroundColor(this, this.$field);
}
setLabelFont(labelFont) {
this.setProperty('labelFont', labelFont);
}
_renderLabelFont() {
styles.legacyFont(this, this.$label, 'label');
}
setLabelForegroundColor(labelForegroundColor) {
this.setProperty('labelForegroundColor', labelForegroundColor);
}
_renderLabelForegroundColor() {
styles.legacyForegroundColor(this, this.$label, 'label');
}
setLabelBackgroundColor(labelBackgroundColor) {
this.setProperty('labelBackgroundColor', labelBackgroundColor);
}
_renderLabelBackgroundColor() {
styles.legacyBackgroundColor(this, this.$label, 'label');
}
setGridDataHints(gridData) {
this.setProperty('gridDataHints', gridData);
}
_setGridDataHints(gridData) {
if (!gridData) {
gridData = new GridData();
}
this._setProperty('gridDataHints', GridData.ensure(gridData));
}
_renderGridDataHints() {
this.parent.invalidateLogicalGrid();
}
_setGridData(gridData) {
if (!gridData) {
gridData = new GridData();
}
this._setProperty('gridData', GridData.ensure(gridData));
}
_renderGridData() {
if (this.rendered) {
var htmlCompParent = this.htmlComp.getParent();
if (htmlCompParent) { // may be null if $container is detached
htmlCompParent.invalidateLayoutTree();
}
}
}
setMenus(menus) {
this.setProperty('menus', menus);
}
_setMenus(menus) {
menus = arrays.ensure(menus);
this.menus.forEach(function(menu) {
menu.off('propertyChange', this._menuPropertyChangeHandler);
}, this);
this.updateKeyStrokes(menus, this.menus);
this._setProperty('menus', menus);
this.menus.forEach(function(menu) {
menu.on('propertyChange', this._menuPropertyChangeHandler);
}, this);
}
insertMenu(menuToInsert) {
this.insertMenus([menuToInsert]);
}
insertMenus(menusToInsert) {
menusToInsert = arrays.ensure(menusToInsert);
if (menusToInsert.length === 0) {
return;
}
this.setMenus(this.menus.concat(menusToInsert));
}
deleteMenu(menuToDelete) {
this.deleteMenus([menuToDelete])
}
deleteMenus(menusToDelete) {
menusToDelete = arrays.ensure(menusToDelete);
if (menusToDelete.length === 0) {
return;
}
var menus = this.menus.slice();
arrays.removeAll(menus, menusToDelete);
this.setMenus(menus);
}
_onMenuPropertyChange(event) {
if (event.propertyName === 'visible' && this.rendered) {
this._updateMenus();
}
}
_getCurrentMenus() {
return this.menus.filter(function(menu) {
return menu.visible;
});
}
_getMenusForStatus(status) {
return this.statusMenuMappings.filter(function(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(function(mapping) {
return mapping.menu;
});
}
_hasMenus() {
return !!(this.menus && this._getCurrentMenus().length > 0);
}
_updateMenus() {
this.$container.toggleClass('has-menus', this._hasMenus() && this.menusVisible);
this._updateFieldStatus();
}
_renderMenus() {
this._updateMenus();
}
_renderStatusMenuMappings() {
this._updateMenus();
}
setMenusVisible(menusVisible) {
this.setProperty('menusVisible', menusVisible);
}
/**
* override by TabItem
**/
_setMenusVisible(menusVisible) {
this._setProperty('menusVisible', menusVisible);
}
_renderMenusVisible() {
this._updateMenus();
}
_setKeyStrokes(keyStrokes) {
this.updateKeyStrokes(keyStrokes, this.keyStrokes);
this._setProperty('keyStrokes', keyStrokes);
}
/**
* May be overridden to explicitly provide a tooltip $parent
*/
_$tooltipParent() {
// Will be determined by the tooltip itself
return undefined;
}
_hideStatusMessage() {
if (this.fieldStatus) {
this.fieldStatus.hideTooltip();
}
}
_showContextMenu() {
var menus = this._getCurrentMenus();
if (menus.length === 0) {
// at least one menu item must be visible
return;
}
// Make sure tooltip is closed first
this._hideStatusMessage();
this.contextPopup = scout.create('ContextMenuPopup', {
parent: this,
$anchor: this.$status,
menuItems: menus,
cloneMenuItems: false,
closeOnAnchorMouseDown: false
});
this.contextPopup.open();
}
_hideContextMenu() {
if (this.contextPopup) {
this.contextPopup.close();
this.contextPopup = null;
}
}
_renderPreventInitialFocus() {
this.$container.toggleClass('prevent-initial-focus', !!this.preventInitialFocus);
}
/**
* Sets the focus on this field. If the field is not rendered, the focus will be set as soon as it is rendered.
*
* @override
*/
focus() {
if (!this.rendered) {
this.session.layoutValidator.schedulePostValidateFunction(this.focus.bind(this));
return false;
}
if (!this.enabledComputed) {
return false;
}
var focusableElement = this.getFocusableElement();
if (focusableElement) {
return this.session.focusManager.requestFocus(focusableElement);
}
return false;
}
/**
* This method returns the HtmlElement to be used as initial focus element or when {@link #focus()} is called.
* It can be overridden, in case the FormField needs to return something other than this.$field[0].
*
* @override
*/
getFocusableElement() {
if (this.rendered && this.$field) {
return this.$field[0];
}
return null;
}
_onFieldFocus(event) {
this.setFocused(true);
}
_onFieldBlur() {
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.<br>
* 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)
var focusableElement = this.getFocusableElement();
if (focusableElement) {
$.ensure(focusableElement).focus();
}
}
/**
* @override
*/
get$Scrollable() {
return this.$field;
}
getParentGroupBox() {
var parent = this.parent;
while (parent && !(parent instanceof GroupBox)) {
parent = parent.parent;
}
return parent;
}
getParentField() {
return this.parent;
}
/**
* Appends a LABEL element to this.$container and sets the this.$label property.
*/
addLabel() {
this.$label = this.$container.appendElement('<label>');
tooltips.installForEllipsis(this.$label, {
parent: this
});
// Setting the focus programmatically does not work in a mousedown listener on mobile devices,
// that is why a click listener is used instead
this.$label.on('click', this._onLabelClick.bind(this));
}
_onLabelClick(event) {
if (!strings.hasText(this.label)) {
// Clicking on "invisible" labels should not have any effect since it is confusing
return;
}
this.activate();
}
_removeLabel() {
if (!this.$label) {
return;
}
tooltips.uninstall(this.$label);
this.$label.remove();
this.$label = null;
}
/**
* Links the given element with the label by setting aria-labelledby.<br>
* This allows screen readers to build a catalog of the elements on the screen and their relationships, for example, to read the label when the input is focused.
*/
_linkWithLabel($element) {
if (!this.$label || !$element) {
return;
}
fields.linkElementWithLabel($element, this.$label);
}
_removeIcon() {
if (!this.$icon) {
return;
}
this.$icon.remove();
this.$icon = null;
}
/**
* Appends the given field to the this.$container and sets the property this.$field.
* The $field is used as $fieldContainer as long as you don't explicitly call addFieldContainer before calling addField.
*/
addField($field) {
if (!this.$fieldContainer) {
this.addFieldContainer($field);
}
this.$field = $field;
this._linkWithLabel($field);
this.$field.on('blur', this._onFieldBlur.bind(this))
.on('focus', this._onFieldFocus.bind(this));
}
/**
* Call this method before addField if you'd like to have a different field container than $field.
*/
addFieldContainer($fieldContainer) {
this.$fieldContainer = $fieldContainer
.addClass('field');
// Only append if not already appended or it is not the last element so that append would move it to the end
// This can be important for some widgets, e.g. iframe which would cancel and restart the request on every dom insertion
if (this.$container.has($fieldContainer).length === 0 || $fieldContainer.next().length > 0) {
$fieldContainer.appendTo(this.$container);
}
}
/**
* Removes this.$field and this.$fieldContainer and sets the properties to null.
*/
_removeField() {
if (this.$field) {
this.$field.remove();
this.$field = null;
}
if (this.$fieldContainer) {
this.$fieldContainer.remove();
this.$fieldContainer = null;
}
}
/**
* Appends a SPAN element for form-field status to this.$container and sets the this.$status property.
*/
addStatus() {
if (this.fieldStatus) {
return;
}
this.fieldStatus = scout.create('FieldStatus', {
parent: this,
position: this.statusPosition,
// This will be done by _updateFieldStatus again, but doing it here prevents unnecessary layout invalidations later on
visible: this._computeStatusVisible()
});
this.fieldStatus.render();
this.$status = this.fieldStatus.$container;
this._updateFieldStatus();
}
_removeStatus() {
if (!this.fieldStatus) {
return;
}
this.fieldStatus.remove();
this.$status = null;
this.fieldStatus = null;
}
/**
* Appends a SPAN element to this.$container and sets the this.$pseudoStatus property.
* The purpose of a pseudo status is to consume the space an ordinary status would.
* This makes it possible to make components without a status as width as components with a status.
*/
addPseudoStatus() {
this.$pseudoStatus = this.$container.appendSpan('status');
}
addMandatoryIndicator() {
this.$mandatory = this.$container.appendSpan('mandatory-indicator');
}
removeMandatoryIndicator() {
if (!this.$mandatory) {
return;
}
this.$mandatory.remove();
this.$mandatory = null;
}
/**
* Adds a SPAN element with class 'icon' the the given optional $parent.
* When $parent is not set, the element is added to this.$container.
* @param $parent (optional)
*/
addIcon($parent) {
if (!$parent) {
$parent = this.$container;
}
this.$icon = fields.appendIcon($parent)
.on('mousedown', this._onIconMouseDown.bind(this));
}
_onIconMouseDown(event) {
if (!this.enabledComputed) {
return;
}
this.$field.focus();
}
/**
* Appends a DIV element as form-field container to $parent and sets the this.$container property.
* Applies FormFieldLayout to this.$container (if container does not define another layout).
* Sets this.htmlComp to the HtmlComponent created for this.$container.
*
* @param $parent to which container is appended
* @param cssClass cssClass to add to the new container DIV
* @param layout when layout is undefined, this#_createLayout() is called
*
*/
addContainer($parent, cssClass, layout) {
this.$container = $parent.appendDiv('form-field');
if (cssClass) {
this.$container.addClass(cssClass);
}
var htmlComp = HtmlComponent.install(this.$container, this.session);
htmlComp.setLayout(layout || this._createLayout());
this.htmlComp = htmlComp;
}
/**
* @return {FormFieldLayout} the default layout FormFieldLayout. Override this function if your field needs another layout.
*/
_createLayout() {
return new FormFieldLayout(this);
}
/**
* Updates the "inner alignment" of a field. Usually, the GridData hints only have influence on the
* LogicalGridLayout. However, the properties "horizontalAlignment" and "verticalAlignment" are
* sometimes used differently. Instead of controlling the field alignment in case fillHorizontal/
* fillVertical is false, the developer expects the _contents_ of the field to be aligned correspondingly
* inside the field. Technically, this is not correct, but is supported for legacy and convenience
* reasons for some of the Scout fields. Those who support the behavior may override _renderGridData()
* and call this method. Some CSS classes are then added to the field.
*
* opts:
* useHorizontalAlignment:
* When this option is true, "halign-" classes are added according to gridData.horizontalAlignment.
* useVerticalAlignment:
* When this option is true, "valign-" classes are added according to gridData.verticalAlignment.
* $fieldContainer:
* Specifies the div where the classes should be added. If omitted, this.$fieldContainer is used.
*/
updateInnerAlignment(opts) {
opts = opts || {};
var $fieldContainer = opts.$fieldContainer || this.$fieldContainer;
this._updateElementInnerAlignment(opts, $fieldContainer);
if ($fieldContainer !== this.$container) {
// also set the styles to the container
this._updateElementInnerAlignment(opts, this.$container);
}
}
_updateElementInnerAlignment(opts, $field) {
opts = opts || {};
var useHorizontalAlignment = scout.nvl(opts.useHorizontalAlignment, true);
var useVerticalAlignment = scout.nvl(opts.useVerticalAlignment, true);
if (!$field) {
return;
}
$field.removeClass('has-inner-alignment halign-left halign-center halign-right valign-top valign-middle valign-bottom');
if (useHorizontalAlignment || useVerticalAlignment) {
// Set horizontal and vertical alignment (from gridData)
$field.addClass('has-inner-alignment');
var gridData = this.gridData;
if (this.parent.logicalGrid) {
// If the logical grid is calculated by JS, use the hints instead of the calculated grid data
gridData = this.gridDataHints;
}
if (useHorizontalAlignment) {
var hAlign = gridData.horizontalAlignment;
$field.addClass(hAlign < 0 ? 'halign-left' : (hAlign > 0 ? 'halign-right' : 'halign-center'));
}
if (useVerticalAlignment) {
var vAlign = gridData.verticalAlignment;
$field.addClass(vAlign < 0 ? 'valign-top' : (vAlign > 0 ? 'valign-bottom' : 'valign-middle'));
}
// Alignment might have affected inner elements (e.g. clear icon)
this.invalidateLayout();
}
}
addCellEditorFieldCssClasses($field, opts) {
$field
.addClass('cell-editor-field')
.addClass(Device.get().cssClassForEdge());
if (opts.cssClass) {
$field.addClass(opts.cssClass);
}
}
prepareForCellEdit(opts) {
opts = opts || {};
// remove mandatory and status indicators (popup should 'fill' the whole cell)
if (this.$mandatory) {
this.removeMandatoryIndicator();
}
if (this.$status) {
this.$status.remove();
this.$status = null;
}
if (this.$container) {
this.$container.addClass('cell-editor-form-field');
}
if (this.$field) {
this.addCellEditorFieldCssClasses(this.$field, opts);
}
}
_renderDropType() {
this._updateDropType();
}
_updateDropType() {
if (this.dropType && this.enabledComputed) {
this._installDragAndDropHandler();
} else {
this._uninstallDragAndDropHandler();
}
}
_createDragAndDropHandler() {
return dragAndDrop.handler(this, {
supportedScoutTypes: dragAndDrop.SCOUT_TYPES.FILE_TRANSFER,
dropType: function() {
return this.dropType;
}.bind(this),
dropMaximumSize: function() {
return this.dropMaximumSize;
}.bind(this)
});
}
_installDragAndDropHandler(event) {
if (this.dragAndDropHandler) {
return;
}
this.dragAndDropHandler = this._createDragAndDropHandler();
this.dragAndDropHandler.install(this.$field);
}
_uninstallDragAndDropHandler(event) {
if (!this.dragAndDropHandler) {
return;
}
this.dragAndDropHandler.uninstall();
this.dragAndDropHandler = null;
}
_updateDisabledCopyOverlay() {
if (this.disabledCopyOverlay && !Device.get().supportsCopyFromDisabledInputFields()) {
if (this.enabledComputed) {
this._removeDisabledCopyOverlay();
} else {
this._renderDisabledCopyOverlay();
this.revalidateLayout(); // because bounds of overlay is set in FormFieldLayout
}
}
}
_renderDisabledCopyOverlay() {
if (!this.$disabledCopyOverlay) {
this.$disabledCopyOverlay = this.$container
.appendDiv('disabled-overlay')
.on('contextmenu', this._createCopyContextMenu.bind(this));
}
}
_removeDisabledCopyOverlay() {
if (this.$disabledCopyOverlay) {
this.$disabledCopyOverlay.remove();
this.$disabledCopyOverlay = null;
}
}
_createCopyContextMenu(event) {
if (!this.visible || strings.empty(this.displayText)) {
return;
}
var menu = scout.create('Menu', {
parent: this,
text: this.session.text('ui.Copy'),
inheritAccessibility: false
});
menu.on('action', function(event) {
this.exportToClipboard();
}.bind(this));
var popup = scout.create('ContextMenuPopup', {
parent: this,
menuItems: [menu],
cloneMenuItems: false,
location: {
x: event.pageX,
y: event.pageY
}
});
popup.open();
}
/**
* Visits this field and all child formfields in pre-order (top-down).
*
* @returns {string} the TreeVisitResult, or nothing to continue.
*/
visitFields(visitor) {
return visitor(this);
}
/**
* Visit all parent form fields. The visit stops if the parent is no form field anymore (e.g. a form, desktop or session).
*/
visitParents(visitor) {
var curParent = this.parent;
while (curParent instanceof FormField) {
visitor(curParent);
curParent = curParent.parent;
}
}
markAsSaved() {
this.setProperty('touched', false);
this.updateRequiresSave();
}
touch() {
this.setProperty('touched', true);
this.updateRequiresSave();
}
/**
* Updates the requiresSave property by checking if the field is touched or if computeRequiresSave() returns true.
*/
updateRequiresSave() {
if (!this.initialized) {
return;
}
this.requiresSave = this.touched || this.computeRequiresSave();
}
/**
* Override this function in order to return whether or not this field requires to be saved.
* The default impl. returns false.
*
* @returns {boolean}
*/
computeRequiresSave() {
return false;
}
/**
* @returns {object} which contains 3 properties: valid, validByErrorStatus and validByMandatory
*/
getValidationResult() {
var validByErrorStatus = !this._errorStatus();
var validByMandatory = !this.mandatory || !this.empty;
var valid = validByErrorStatus && validByMandatory;
return {
valid: valid,
validByErrorStatus: validByErrorStatus,
validByMandatory: validByMandatory
};
}
_updateEmpty() {
// NOP
}
requestInput() {
if (this.enabledComputed && this.rendered) {
this.focus();
}
}
clone(model, options) {
var clone = super.clone(model, options);
this._deepCloneProperties(clone, 'menus', options);
return clone;
}
exportToClipboard() {
if (!this.displayText) {
return;
}
var event = new Event({
text: this.displayText
});
this.trigger('clipboardExport', event);
if (!event.defaultPrevented) {
this._exportToClipboard(event.text);
}
}
_exportToClipboard(text) {
clipboard.copyText({
parent: this,
text: text
});
}
}