blob: 2b64e2a10bbd598fc268c9f95619e5da669b8a5d [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 {Dimension, graphics, GroupLayout, GroupToggleCollapseKeyStroke, HtmlComponent, Icon, Insets, KeyStrokeContext, LoadingSupport, scout, tooltips, Widget} from '../index';
import * as $ from 'jquery';
export default class Group extends Widget {
constructor() {
super();
this.bodyAnimating = false;
this.collapsed = false;
this.collapsible = true;
this.title = null;
this.titleHtmlEnabled = false;
this.titleSuffix = null;
this.header = null;
this.headerFocusable = false;
this.headerVisible = true;
this.body = null;
this.$container = null;
this.$header = null;
this.$footer = null;
this.$collapseIcon = null;
this.$collapseBorderLeft = null;
this.$collapseBorderRight = null;
this.collapseStyle = Group.CollapseStyle.LEFT;
this.htmlComp = null;
this.htmlHeader = null;
this.htmlFooter = null;
this.iconId = null;
this.icon = null;
this._addWidgetProperties(['header']);
this._addWidgetProperties(['body']);
}
static CollapseStyle = {
LEFT: 'left',
RIGHT: 'right',
BOTTOM: 'bottom'
};
_init(model) {
super._init(model);
this.resolveTextKeys(['title', 'titleSuffix']);
this.resolveIconIds(['iconId']);
this._setBody(this.body);
this._setHeader(this.header);
}
/**
* @override
*/
_createKeyStrokeContext() {
return new KeyStrokeContext();
}
/**
* @override
*/
_initKeyStrokeContext() {
super._initKeyStrokeContext();
// Key stroke should only work when header is focused
this.keyStrokeContext.$bindTarget = function() {
return this.$header;
}.bind(this);
this.keyStrokeContext.registerKeyStroke([
new GroupToggleCollapseKeyStroke(this)
]);
}
_render() {
this.$container = this.$parent.appendDiv('group');
this.htmlComp = HtmlComponent.install(this.$container, this.session);
this.htmlComp.setLayout(new GroupLayout(this));
this._renderHeader();
this.$collapseIcon = this.$header.appendDiv('group-collapse-icon');
this.$footer = this.$container.appendDiv('group-footer');
this.$collapseBorderLeft = this.$footer.appendDiv('group-collapse-border');
this.$collapseBorderRight = this.$footer.appendDiv('group-collapse-border');
this.htmlFooter = HtmlComponent.install(this.$footer, this.session);
this.$footer.on('mousedown', this._onFooterMouseDown.bind(this));
}
_renderProperties() {
super._renderProperties();
this._renderIconId();
this._renderTitle();
this._renderTitleSuffix();
this._renderHeaderVisible();
this._renderHeaderFocusable();
this._renderCollapsed();
this._renderCollapseStyle();
this._renderCollapsible();
}
_remove() {
this.$header = null;
this.$title = null;
this.$titleSuffix = null;
this.$footer = null;
this.$collapseIcon = null;
this.$collapseBorderLeft = null;
this.$collapseBorderRight = null;
this._removeIconId();
super._remove();
}
_renderEnabled() {
super._renderEnabled();
this.$header.setTabbable(this.enabledComputed);
}
setIconId(iconId) {
this.setProperty('iconId', iconId);
}
/**
* Adds an image or font-based icon to the group header by adding either an IMG or SPAN element.
*/
_renderIconId() {
var 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();
this._updateIconStyle();
return;
}
if (this.icon) {
this.icon.setIconDesc(iconId);
this._updateIconStyle();
return;
}
this.icon = scout.create('Icon', {
parent: this,
iconDesc: iconId,
prepend: true
});
this.icon.one('destroy', function() {
this.icon = null;
}.bind(this));
this.icon.render(this.$header);
this._updateIconStyle();
}
_updateIconStyle() {
var hasTitle = !!this.title;
this.get$Icon().toggleClass('with-title', hasTitle);
this.get$Icon().addClass('group-icon');
this._renderCollapseStyle();
}
get$Icon() {
if (this.icon) {
return this.icon.$container;
}
return $();
}
_removeIconId() {
if (this.icon) {
this.icon.destroy();
}
}
setHeader(header) {
this.setProperty('header', header);
}
_setHeader(header) {
this._setProperty('header', header);
}
setHeaderFocusable(headerFocusable) {
this.setProperty('headerFocusable', headerFocusable);
}
_renderHeaderFocusable() {
this.$header.toggleClass('unfocusable', !this.headerFocusable);
}
setTitle(title) {
this.setProperty('title', title);
}
_renderTitle() {
if (this.$title) {
if (this.titleHtmlEnabled) {
this.$title.htmlOrNbsp(this.title);
} else {
this.$title.textOrNbsp(this.title);
}
this._updateIconStyle();
}
}
setTitleSuffix(titleSuffix) {
this.setProperty('titleSuffix', titleSuffix);
}
_renderTitleSuffix() {
if (this.$titleSuffix) {
this.$titleSuffix.text(this.titleSuffix || '');
}
}
setHeaderVisible(headerVisible) {
this.setProperty('headerVisible', headerVisible);
}
_renderHeaderVisible() {
this.$header.setVisible(this.headerVisible);
this._renderCollapsible();
this.invalidateLayoutTree();
}
setBody(body) {
this.setProperty('body', body);
}
_setBody(body) {
if (!body) {
// Create empty body if no body was provided
body = scout.create('Widget', {
parent: this,
_render: function() {
this.$container = this.$parent.appendDiv('group');
this.htmlComp = HtmlComponent.install(this.$container, this.session);
}
});
}
this._setProperty('body', body);
}
_createLoadingSupport() {
return new LoadingSupport({
widget: this,
$container: function() {
return this.$header;
}.bind(this)
});
}
_renderHeader() {
if (this.$header) {
this.$header.remove();
this._removeIconId();
}
if (this.header) {
this.header.render();
this.$header = this.header.$container
.addClass('group-header')
.addClass('custom-header-widget')
.prependTo(this.$container);
this.htmlHeader = this.header.htmlComp;
} else {
this.$header = this.$container
.prependDiv('group-header')
.unfocusable()
.addClass('prevent-initial-focus');
this.$title = this.$header.appendDiv('group-title');
this.$titleSuffix = this.$header.appendDiv('group-title-suffix');
tooltips.installForEllipsis(this.$title, {
parent: this
});
this.htmlHeader = HtmlComponent.install(this.$header, this.session);
if (!this.rendering) {
this._renderIconId();
this._renderTitle();
this._renderTitleSuffix();
}
}
this.$header.on('mousedown', this._onHeaderMouseDown.bind(this));
this.invalidateLayoutTree();
}
_renderBody() {
this.body.render();
this.body.$container.insertAfter(this.$header);
this.body.$container.addClass('group-body');
this.body.invalidateLayoutTree();
}
/**
* @override
*/
getFocusableElement() {
if (!this.rendered) {
return false;
}
return this.$header;
}
toggleCollapse() {
this.setCollapsed(!this.collapsed && this.collapsible);
}
setCollapsed(collapsed) {
this.setProperty('collapsed', collapsed);
}
_renderCollapsed() {
this.$container.toggleClass('collapsed', this.collapsed);
this.$collapseIcon.toggleClass('collapsed', this.collapsed);
if (!this.collapsed && !this.bodyAnimating) {
this._renderBody();
}
if (this.rendered) {
this.resizeBody();
} else if (this.collapsed) {
// Body will be removed after the animation, if there is no animation, remove it now
this.body.remove();
}
this.invalidateLayoutTree();
}
setCollapsible(collapsible) {
this.setProperty('collapsible', collapsible);
}
_renderCollapsible() {
this.$container.toggleClass('collapsible', this.collapsible);
this.$header.toggleClass('disabled', !this.collapsible);
// footer is visible if collapseStyle is 'bottom' and either header is visible or has a (collapsible) body
this.$footer.setVisible(this.collapseStyle === Group.CollapseStyle.BOTTOM && (this.headerVisible || this.collapsible));
this.$collapseIcon.setVisible(this.collapsible);
this.invalidateLayoutTree();
}
setCollapseStyle(collapseStyle) {
this.setProperty('collapseStyle', collapseStyle);
}
_renderCollapseStyle() {
this.$header.toggleClass('collapse-right', this.collapseStyle === Group.CollapseStyle.RIGHT);
this.$container.toggleClass('collapse-bottom', this.collapseStyle === Group.CollapseStyle.BOTTOM);
if (this.collapseStyle === Group.CollapseStyle.RIGHT && !this.header) {
this.$collapseIcon.appendTo(this.$header);
} else if (this.collapseStyle === Group.CollapseStyle.LEFT && !this.header) {
this.$collapseIcon.prependTo(this.$header);
} else if (this.collapseStyle === Group.CollapseStyle.BOTTOM) {
var sibling = this.body.$container ? this.body.$container : this.$header;
this.$footer.insertAfter(sibling);
this.$collapseIcon.insertAfter(this.$collapseBorderLeft);
}
this._renderCollapsible();
this.invalidateLayoutTree();
}
_onHeaderMouseDown(event) {
if (this.collapsible && (!this.header || this.collapseStyle !== Group.CollapseStyle.BOTTOM)) {
this.setCollapsed(!this.collapsed && this.collapsible);
}
}
_onFooterMouseDown(event) {
if (this.collapsible) {
this.setCollapsed(!this.collapsed && this.collapsible);
}
}
/**
* Resizes the body to its preferred size by animating the height.
*/
resizeBody() {
this.animateToggleCollapse().done(function() {
if (this.bodyAnimating) {
// Another animation has been started in the mean time -> ignore done event
return;
}
if (this.collapsed) {
this.body.remove();
}
this.invalidateLayoutTree();
}.bind(this));
}
/**
* @param {object} [options]
* @returns {Promise}
*/
animateToggleCollapse(options) {
var currentSize = graphics.cssSize(this.body.$container);
var currentMargins = graphics.margins(this.body.$container);
var currentPaddings = graphics.paddings(this.body.$container);
var targetHeight, targetMargins, targetPaddings;
if (this.collapsed) {
// Collapsing
// Set target values to 0 when collapsing
targetHeight = 0;
targetMargins = new Insets();
targetPaddings = new Insets();
} else {
// Expanding
// Expand to preferred size of the body
targetHeight = this.body.htmlComp.prefSize({
widthHint: currentSize.width
}).height;
// Make sure body is layouted correctly before starting the animation (with the target size)
// Use setSize to explicitly call its layout (this might even be necessary during the animation, see GroupLayout.invalidate)
this.body.htmlComp.setSize(new Dimension(this.body.$container.outerWidth(), targetHeight));
if (this.bodyAnimating) {
// The group may be expanded while being collapsed or vice verca.
// In that case, use the current values of the inline style as starting values
// Clear current insets to read target insets from CSS anew
this.body.$container
.cssMarginY('')
.cssPaddingY('');
targetMargins = graphics.margins(this.body.$container);
targetPaddings = graphics.paddings(this.body.$container);
} else {
// If toggling is not already in progress, start expanding from 0
currentSize.height = 0;
currentMargins = new Insets();
currentPaddings = new Insets();
targetMargins = graphics.margins(this.body.$container);
targetPaddings = graphics.paddings(this.body.$container);
}
}
this.bodyAnimating = true;
if (this.collapsed) {
this.$container.addClass('collapsing');
}
return this.body.$container
.stop(true)
.cssHeight(currentSize.height)
.cssMarginTop(currentMargins.top)
.cssMarginBottom(currentMargins.bottom)
.cssPaddingTop(currentPaddings.top)
.cssPaddingBottom(currentPaddings.bottom)
.animate({
height: targetHeight,
marginTop: targetMargins.top,
marginBottom: targetMargins.bottom,
paddingTop: targetPaddings.top,
paddingBottom: targetPaddings.bottom
}, {
duration: 350,
progress: function() {
this.trigger('bodyHeightChange');
this.revalidateLayoutTree();
}.bind(this),
complete: function() {
this.bodyAnimating = false;
if (this.body.rendered) {
// Remove inline styles when finished
this.body.$container.cssMarginY('');
this.body.$container.cssPaddingY('');
}
if (this.rendered) {
this.$container.removeClass('collapsing');
}
this.trigger('bodyHeightChangeDone');
}.bind(this)
})
.promise();
}
}