@eclipse-scout/core
Version:
Eclipse Scout runtime
548 lines (477 loc) • 14.8 kB
text/typescript
/*
* 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 {
AbstractLayout, ActionEventMap, ActionKeyStroke, ActionModel, Alignment, aria, Device, DoubleClickSupport, EnumObject, HtmlComponent, Icon, InitModelOf, KeyStrokeContext, LoadingSupport, NullLayout, scout, TooltipPosition, tooltips,
TooltipSupport, Widget
} from '../index';
import $ from 'jquery';
export type ActionStyle = EnumObject<typeof Action.ActionStyle>;
export type KeyStrokeFirePolicy = EnumObject<typeof Action.KeyStrokeFirePolicy>;
export type ActionTextPosition = EnumObject<typeof Action.TextPosition>;
export class Action extends Widget implements ActionModel {
declare model: ActionModel;
declare eventMap: ActionEventMap;
declare self: Action;
actionStyle: ActionStyle;
compact: boolean;
iconId: string;
horizontalAlignment: Alignment;
keyStroke: string;
keyStrokeFirePolicy: KeyStrokeFirePolicy;
selected: boolean;
preventDoubleClick: boolean;
tabbable: boolean;
actionKeyStroke: ActionKeyStroke;
text: string;
textPosition: ActionTextPosition;
htmlEnabled: boolean;
/**
* May be set to true if the action does not fit into the container and is for example moved into an overflow-menu.
*/
overflown: boolean;
textVisible: boolean;
toggleAction: boolean;
tooltipText: string;
showTooltipWhenSelected: boolean;
tooltipPosition: TooltipPosition;
icon: Icon;
$text: JQuery;
protected _doubleClickSupport: DoubleClickSupport;
protected _compactOrig: boolean;
protected _textVisibleOrig: boolean;
constructor() {
super();
this.actionStyle = Action.ActionStyle.DEFAULT;
this.compact = false;
this.iconId = null;
this.horizontalAlignment = -1;
this.keyStroke = null;
this.keyStrokeFirePolicy = Action.KeyStrokeFirePolicy.ACCESSIBLE_ONLY;
this.selected = false;
this.preventDoubleClick = false;
this.tabbable = false;
this.text = null;
this.textPosition = Action.TextPosition.DEFAULT;
this.htmlEnabled = false;
this.overflown = false;
this.textVisible = true;
this.toggleAction = false;
this.tooltipText = null;
this.showTooltipWhenSelected = true;
this._doubleClickSupport = new DoubleClickSupport();
this._addCloneProperties(['actionStyle', 'horizontalAlignment', 'iconId', 'selected', 'preventDoubleClick', 'tabbable', 'text', 'textPosition', 'htmlEnabled', 'tooltipText', 'toggleAction']);
}
static ActionStyle = {
/**
* Regular look, also used in overflow menus.
*/
DEFAULT: 0,
/**
* Action looks like a button.
*/
BUTTON: 1
} as const;
static TextPosition = {
DEFAULT: 'default',
/**
* The text will be positioned below the icon. It has no effect if no icon is set.
*/
BOTTOM: 'bottom'
} as const;
static KeyStrokeFirePolicy = {
ACCESSIBLE_ONLY: 0,
ALWAYS: 1
} as const;
protected override _createKeyStrokeContext(): KeyStrokeContext {
return new KeyStrokeContext();
}
protected override _createLoadingSupport(): LoadingSupport {
return new LoadingSupport({
widget: this
});
}
protected override _init(model: InitModelOf<this>) {
super._init(model);
this.actionKeyStroke = this._createActionKeyStroke();
this.resolveConsts([{
property: 'actionStyle',
constType: Action.ActionStyle
}, {
property: 'textPosition',
constType: Action.TextPosition
}, {
property: 'keyStrokeFirePolicy',
constType: Action.KeyStrokeFirePolicy
}]);
this.resolveTextKeys(['text', 'tooltipText']);
this.resolveIconIds(['iconId']);
this._setKeyStroke(this.keyStroke);
}
protected override _render() {
this.$container = this.$parent.appendDiv('action')
.on('mousedown', event => this._doubleClickSupport.mousedown(event))
.on('click', this._onClick.bind(this));
this.htmlComp = HtmlComponent.install(this.$container, this.session);
this.htmlComp.setLayout(this._createLayout());
}
protected _createLayout(): AbstractLayout {
return new NullLayout();
}
protected override _renderProperties() {
super._renderProperties();
this._renderText();
this._renderTextPosition();
this._renderIconId();
this._renderTooltipText();
this._renderKeyStroke();
this._renderSelected();
this._renderTabbable();
this._renderCompact();
this._renderActionStyle();
this._renderOverflown();
}
protected override _remove() {
this._removeText();
this._removeIconId();
super._remove();
}
/** @see ActionModel.actionStyle */
setActionStyle(actionStyle: ActionStyle) {
this.setProperty('actionStyle', actionStyle);
}
/** @see ActionModel.text */
setText(text: string) {
this.setProperty('text', text);
}
protected _renderText() {
let text = this.text || '';
if (text && this.textVisible) {
if (!this.$text) {
// Create a separate text element to so that setting the text does not remove the icon
this.$text = this.$container.appendSpan('content text');
HtmlComponent.install(this.$text, this.session);
}
if (this.htmlEnabled) {
this.$text.html(text);
} else {
this.$text.text(text);
}
// not needed if text is visible
aria.label(this.$container, null);
} else {
// add as label for screen readers
aria.label(this.$container, text);
this._removeText();
}
}
protected _removeText() {
if (this.$text) {
this.$text.remove();
this.$text = null;
}
}
/** @see ActionModel.textPosition */
setTextPosition(textPosition: ActionTextPosition) {
this.setProperty('textPosition', textPosition);
}
protected _renderTextPosition() {
this.$container.toggleClass('bottom-text', this.textPosition === Action.TextPosition.BOTTOM);
this.invalidateLayoutTree();
}
/** @see ActionModel.htmlEnabled */
setHtmlEnabled(htmlEnabled: boolean) {
this.setProperty('htmlEnabled', htmlEnabled);
}
protected _renderHtmlEnabled() {
// Render the text again when html enabled changes dynamically
this._renderText();
}
/** @see ActionModel.iconId */
setIconId(iconId: string) {
this.setProperty('iconId', iconId);
}
protected _renderIconId() {
let iconId = this.iconId || '';
// If the icon is an image (and not a font icon), the Icon class will invalidate the layout when the image has loaded
if (!iconId) {
this._removeIconId();
return;
}
if (this.icon) {
this.icon.setIconDesc(iconId);
return;
}
this.icon = scout.create(Icon, {
parent: this,
iconDesc: iconId,
prepend: true
});
this.icon.one('destroy', () => {
this.icon = null;
});
this.icon.render();
}
get$Icon(): JQuery {
if (this.icon) {
return this.icon.$container;
}
return $();
}
protected _removeIconId() {
if (this.icon) {
this.icon.destroy();
}
}
protected override _renderEnabled() {
super._renderEnabled();
if (this.rendered) { // No need to do this during initial rendering
this._updateTooltip();
this._renderTabbable();
}
}
/** @see ActionModel.tooltipText */
setTooltipText(tooltipText: string) {
this.setProperty('tooltipText', tooltipText);
}
protected _renderTooltipText() {
this._updateTooltip();
}
/**
* Returns the text to show as a tooltip, which might be different from the `tooltipText` property.
* If this value is falsy, the tooltip is not installed.
*
* @see _configureTooltip
* @see _shouldInstallTooltip
*/
protected _computeTooltipText(): string {
return this.tooltipText;
}
/**
* Installs or uninstalls tooltip based on tooltipText, selected and enabledComputed.
*/
protected _updateTooltip() {
if (this._shouldInstallTooltip()) {
tooltips.install(this.$container, this._configureTooltip());
} else {
tooltips.uninstall(this.$container);
}
aria.description(this.$container, this.tooltipText);
}
protected _shouldInstallTooltip(): boolean {
if (this.selected && !this.showTooltipWhenSelected) {
return false;
}
let tooltipText = this._computeTooltipText();
return !!tooltipText;
}
/** @see ActionModel.tabbable */
setTabbable(tabbable: boolean) {
this.setProperty('tabbable', tabbable);
}
protected _renderTabbable() {
this.$container.setTabbable(this.tabbable && this.enabledComputed && !Device.get().supportsOnlyTouch());
}
/** @see ActionModel.compact */
setCompact(compact: boolean) {
if (this.compact === compact) {
return;
}
this.compact = compact;
if (this.rendered) {
this._renderCompact();
}
}
protected _renderCompact() {
this.$container.toggleClass('compact', this.compact);
this.invalidateLayoutTree();
}
protected _renderActionStyle() {
aria.role(this.$container, 'button');
}
/** @see ActionModel.tooltipPosition */
setTooltipPosition(position: TooltipPosition) {
this.setProperty('tooltipPosition', position);
}
protected _configureTooltip(): InitModelOf<TooltipSupport> {
return {
parent: this,
text: this._computeTooltipText(),
$anchor: this.$container,
arrowPosition: 50,
arrowPositionUnit: '%',
tooltipPosition: this.tooltipPosition
};
}
/**
* @returns true if the action has been performed or false if it has not been performed (e.g. when the button is not enabledComputed).
*/
doAction(): boolean {
if (!this.prepareDoAction()) {
return false;
}
if (this.isToggleAction()) {
this.setSelected(!this.selected);
}
this._doAction();
return true;
}
toggle() {
if (this.isToggleAction()) {
this.setSelected(!this.selected);
}
}
/** @see ActionModel.toggleAction */
setToggleAction(toggleAction: boolean) {
this.setProperty('toggleAction', toggleAction);
}
isToggleAction(): boolean {
return this.toggleAction;
}
_renderToggleAction() {
aria.pressed(this.$container, this.isToggleAction() ? this.selected : null);
}
/**
* @returns true if the action may be executed, false if it should be ignored.
*/
prepareDoAction(): boolean {
if (!this.enabledComputed || !this.visible) {
return false;
}
return true;
}
protected _doAction() {
this.trigger('action');
}
/** @see ActionModel.selected */
setSelected(selected: boolean) {
this.setProperty('selected', selected);
}
protected _renderSelected() {
this.$container.toggleClass('selected', this.selected);
aria.pressed(this.$container, this.isToggleAction() ? this.selected : null);
if (this.rendered) { // prevent unnecessary tooltip updates during initial rendering
this._updateTooltip();
}
}
/** @see ActionModel.keyStroke */
setKeyStroke(keyStroke: string) {
this.setProperty('keyStroke', keyStroke);
}
protected _setKeyStroke(keyStroke: string) {
this.actionKeyStroke.parseAndSetKeyStroke(keyStroke);
this._setProperty('keyStroke', keyStroke);
}
protected _renderKeyStroke() {
let keyStroke = this.keyStroke;
if (keyStroke === undefined) {
this.$container.removeAttr('data-shortcut');
} else {
this.$container.attr('data-shortcut', keyStroke);
}
}
/** @see ActionModel.textVisible */
setTextVisible(textVisible: boolean) {
this.setProperty('textVisible', textVisible);
}
protected _renderTextVisible() {
this._renderText();
}
/** @see ActionModel.horizontalAlignment */
setHorizontalAlignment(horizontalAlignment: Alignment) {
this.setProperty('horizontalAlignment', horizontalAlignment);
}
protected _createActionKeyStroke(): ActionKeyStroke {
return new ActionKeyStroke(this);
}
/** @see ActionModel.preventDoubleClick */
setPreventDoubleClick(preventDoubleClick: boolean) {
this.setProperty('preventDoubleClick', preventDoubleClick);
}
/**
* @internal
*/
_setOverflown(overflown: boolean) {
if (this.overflown === overflown) {
return;
}
this._setProperty('overflown', overflown);
if (this.rendered) {
this._renderOverflown();
}
}
protected _renderOverflown() {
this.$container.toggleClass('overflown', this.overflown);
}
protected _allowMouseEvent(event: JQuery.MouseEventBase): boolean {
if (event.which !== 1) {
return false; // Other button than left mouse button --> nop
}
if (event.type === 'click' && this.preventDoubleClick && this._doubleClickSupport.doubleClicked()) {
return false; // More than one consecutive click --> nop
}
return true;
}
protected _onClick(event: JQuery.ClickEvent) {
if (!this._allowMouseEvent(event)) {
return;
}
// When the action is clicked the user wants to execute the action and not see the tooltip -> cancel the task
// If it is already displayed it will stay
tooltips.cancel(this.$container);
this.doAction();
}
/**
* Sets the action into compact mode. Can be reversed by calling {@link #undoMakeCompact}.
* @see ActionModel.compact
*/
makeCompact() {
if (this._compactOrig !== undefined) {
return; // already done
}
this._compactOrig = this.compact;
this.setCompact(true);
}
/**
* Undoes the effect of {@link #makeCompact}, i.e. restores the previous compact state.
* If {@link #makeCompact} was not called previously, nothing happens.
*/
undoMakeCompact() {
if (this._compactOrig === undefined) {
return; // nothing to undo
}
this.setCompact(this._compactOrig);
this._compactOrig = undefined;
}
/**
* If the action has an icon, the text is made invisible. Otherwise, nothing happens.
* Can be reversed by calling {@link #undoShrink}.
*/
shrink() {
if (!this.iconId) {
return; // not shrinkable
}
if (this._textVisibleOrig !== undefined) {
return; // already done
}
this._textVisibleOrig = this.textVisible;
this.setTextVisible(false);
}
/**
* Undoes the effect of {@link #shrink}, i.e. restores the text visibility to the previous state.
* If {@link #shrink} was not called previously, nothing happens.
*/
undoShrink() {
if (this._textVisibleOrig === undefined) {
return; // nothing to undo
}
this.setTextVisible(this._textVisibleOrig);
this._textVisibleOrig = undefined;
}
}