@eclipse-scout/core
Version:
Eclipse Scout runtime
333 lines (291 loc) • 11.1 kB
text/typescript
/*
* Copyright (c) 2010, 2023 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, Dimension, EnumObject, FlexboxLayoutData, HtmlComponent, HtmlCompPrefSizeOptions, Rectangle, webstorage} from '../../index';
import $ from 'jquery';
export type FlexboxDirection = EnumObject<typeof FlexboxLayout.Direction>;
export class FlexboxLayout extends AbstractLayout {
childrenLayoutDatas: FlexboxLayoutData[];
cacheKey: string[];
protected _getDimensionValue: (dimension: Dimension) => number;
protected _layoutFromLayoutData: (children: HtmlComponent[], containerSize: Dimension) => void;
constructor(direction: FlexboxDirection, cacheKey: string[]) {
super();
this.childrenLayoutDatas = [];
this.cacheKey = null;
this.setCacheKey(cacheKey);
if (direction === FlexboxLayout.Direction.ROW) {
this.preferredLayoutSize = this.preferredLayoutSizeRow;
this._getDimensionValue = this._getWidth;
this._layoutFromLayoutData = this._layoutFromLayoutDataRow;
} else {
this.preferredLayoutSize = this.preferredLayoutSizeColumn;
this._getDimensionValue = this._getHeight;
this._layoutFromLayoutData = this._layoutFromLayoutDataColumn;
}
}
static Direction = {
COLUMN: 0,
ROW: 1
} as const;
setCacheKey(cacheKey: string[]) {
this.cacheKey = cacheKey;
if (this.cacheKey && this.cacheKey.length > 0) {
this.cacheKey.unshift('scout.flexboxLayout');
}
}
protected _readCache(childCount: number): number[] {
if (!this.cacheKey || this.cacheKey.length === 0 || childCount < 2) {
return;
}
let keySequence = this.cacheKey.slice(),
cacheValue = webstorage.getItemFromLocalStorage(keySequence[0]),
i = 1,
cacheObj;
keySequence.push('' + childCount);
if (cacheValue) {
cacheObj = JSON.parse(cacheValue);
}
while (cacheObj && i < keySequence.length) {
cacheObj = cacheObj[keySequence[i]];
i++;
}
return cacheObj;
}
protected _writeCache(childCount: number, sizes: number[]) {
if (!this.cacheKey || this.cacheKey.length === 0 || childCount < 2) {
return;
}
let keySequence = this.cacheKey.slice(),
cacheValue = webstorage.getItemFromLocalStorage(keySequence[0]),
i = 1,
cacheObj,
cachedSizes;
keySequence.push('' + childCount);
if (cacheValue) {
cacheObj = JSON.parse(cacheValue);
} else {
cacheObj = {};
}
cachedSizes = cacheObj;
while (i < keySequence.length - 1) {
if (!cachedSizes[keySequence[i]]) {
cachedSizes[keySequence[i]] = {};
}
cachedSizes = cachedSizes[keySequence[i]];
i++;
}
cachedSizes[keySequence[i]] = sizes;
webstorage.setItemToLocalStorage(keySequence[0], JSON.stringify(cacheObj));
}
protected _computeCacheKey(childCount: number): string {
// no need to cache bounds of a single child
if (!this.cacheKey || childCount < 2) {
return;
}
return this.cacheKey + '-' + childCount;
}
// layout functions
override layout($container: JQuery) {
let children = this._getChildren($container),
htmlContainer = HtmlComponent.get($container),
containerSize = htmlContainer.availableSize({
exact: true
}),
splitterWithDelta;
containerSize = containerSize.subtract(htmlContainer.insets());
splitterWithDelta = children.filter(c => (<FlexboxLayoutData>c.layoutData).diff)[0];
if (splitterWithDelta) {
this._layoutDelta(children, splitterWithDelta, containerSize);
} else {
this._layoutComponents(children, containerSize);
}
}
protected _getChildren($container: JQuery): HtmlComponent[] {
let children = [];
$container.children().each(function() {
let htmlChild = HtmlComponent.optGet($(this));
if (htmlChild) {
children.push(htmlChild);
}
});
children = children.sort((a, b) => {
return (a.layoutData.order || 0) - (b.layoutData.order || 0);
});
return children;
}
reset() {
this.childrenLayoutDatas.forEach(ld => {
ld.sizePx = 0;
ld.initialPx = 0;
ld.diff = null;
});
this.childrenLayoutDatas = [];
}
protected _layoutDelta(children: HtmlComponent[], deltaComp: HtmlComponent, containerSize: Dimension) {
this.ensureInitialValues(children, containerSize);
let delta = (<FlexboxLayoutData>deltaComp.layoutData).diff,
componentsBefore = children.slice(0, children.indexOf(deltaComp)).reverse(),
componentsAfter = children.slice(children.indexOf(deltaComp) + 1),
deltaDiffPrev,
deltaDiffNext;
// calculate if the delta can be applied to the previous and following columns
deltaDiffPrev = _distributeDelta(componentsBefore, delta, false);
deltaDiffNext = -_distributeDelta(componentsAfter, -delta, false);
// compute the max delta could be applied
delta = Math.sign(delta) * (Math.min(Math.abs(delta - deltaDiffPrev), Math.abs(delta - deltaDiffNext)));
if (delta !== 0) {
// apply the delta to the previous and following columns
_distributeDelta(componentsBefore, delta, true);
_distributeDelta(componentsAfter, -delta, true);
}
this._layoutFromLayoutDataWithCache(children, containerSize);
/* private functions */
function _distributeDelta(components, delta, applyDelta) {
return components.reduce((diff, c) => {
if (diff !== 0) {
diff = c.layoutData.acceptDelta(diff, applyDelta);
}
return diff;
}, delta);
}
}
protected _layoutComponents(children: HtmlComponent[], containerSize: Dimension) {
let delta = this.ensureInitialValues(children, containerSize);
if (delta < 0) {
this._adjust(children, delta, ld => ld.shrink);
} else if (delta > 0) {
this._adjust(children, delta, ld => ld.grow);
}
this._layoutFromLayoutDataWithCache(children, containerSize);
}
protected _adjust(children: HtmlComponent[], delta: number, getWeightFunction: (ld: FlexboxLayoutData) => number) {
let weightSum,
deltaFactor,
layoutDatas = children.map(c => c.layoutData as FlexboxLayoutData).filter(ld => {
// resizable
return ld.acceptDelta(Math.sign(delta)) === 0;
});
if (layoutDatas.length < 1) {
return;
}
weightSum = layoutDatas.reduce((sum, ld) => {
return sum + getWeightFunction(ld);
}, 0);
// delta factor
deltaFactor = delta / weightSum;
delta = layoutDatas.reduce((delta, ld) => {
return ld.acceptDelta(deltaFactor * getWeightFunction(ld), true);
}, delta);
if (Math.abs(delta) > 0.2) {
this._adjust(children, delta, getWeightFunction);
}
}
protected _getPreferredSize(htmlComp: HtmlComponent): Dimension {
return htmlComp.prefSize({useCssSize: true})
.add(htmlComp.margins());
}
ensureInitialValues(children: HtmlComponent[], containerSize: Dimension): number {
let totalPx = this._getDimensionValue(containerSize),
sumOfAbsolutePx = 0,
sumOfRelatives = 0,
colLayoutDatas = children.map(c => {
return c.layoutData as FlexboxLayoutData;
}),
cachedSizes = this._readCache(children.length) || [];
// setup initial values
children.forEach((comp, i) => {
let ld = comp.layoutData as FlexboxLayoutData;
if (ld.sizePx) {
sumOfAbsolutePx += ld.sizePx;
} else if (ld.initial < 0) {
// use ui height
ld.initialPx = this._getDimensionValue(this._getPreferredSize(comp));
sumOfAbsolutePx += ld.initialPx;
} else if (ld.relative) {
sumOfRelatives += ld.initial;
} else {
ld.initialPx = ld.initial;
sumOfAbsolutePx += ld.initialPx;
}
});
let relativeFactor = (totalPx - sumOfAbsolutePx) / sumOfRelatives;
colLayoutDatas.filter(ld => {
return ld.relative && ld.initial > -1 && !ld.sizePx;
}).reduce((restWidth, ld) => {
ld.initialPx = Math.max(30, relativeFactor * ld.initial);
return restWidth - ld.initialPx;
}, (totalPx - sumOfAbsolutePx));
// set px values
return colLayoutDatas
.reduce((restWidth, ld, i) => {
if (!ld.sizePx) {
if (cachedSizes[i]) {
ld.sizePx = ld.validate(Math.round(totalPx * cachedSizes[i]));
} else {
ld.sizePx = ld.initialPx;
}
}
this.childrenLayoutDatas.push(ld);
return restWidth - ld.sizePx;
}, totalPx);
}
protected _layoutFromLayoutDataWithCache(children: HtmlComponent[], containerSize: Dimension) {
this._cacheSizes(children, containerSize);
this._layoutFromLayoutData(children, containerSize);
}
protected _cacheSizes(children: HtmlComponent[], containerSize: Dimension) {
let totalPx = this._getDimensionValue(containerSize);
let value = children.map(c => (<FlexboxLayoutData>c.layoutData).sizePx / totalPx);
this._writeCache(children.length, value);
}
// functions differ from row to column mode
preferredLayoutSizeColumn($container: JQuery, options: HtmlCompPrefSizeOptions): Dimension {
return this._getChildren($container).reduce((size, c) => {
let prefSize = this._getPreferredSize(c);
size.width = Math.max(prefSize.width, size.width);
size.height += prefSize.height;
return size;
}, new Dimension(0, 0));
}
preferredLayoutSizeRow($container: JQuery, options: HtmlCompPrefSizeOptions): Dimension {
return this._getChildren($container).reduce((size, c) => {
let prefSize = this._getPreferredSize(c);
size.height = Math.max(prefSize.height, size.height);
size.width += prefSize.width;
return size;
}, new Dimension(0, 0));
}
protected _getWidth(dimension: Dimension): number {
return dimension.width;
}
protected _getHeight(dimension: Dimension): number {
return dimension.height;
}
protected _layoutFromLayoutDataRow(children: HtmlComponent[], containerSize: Dimension) {
children.reduce((x, comp) => {
let margins = comp.margins();
let insets = comp.insets();
let w = (<FlexboxLayoutData>comp.layoutData).sizePx;
let bounds = new Rectangle(x - insets.left - margins.left, 0, w + insets.left + insets.right, containerSize.height);
comp.setBounds(bounds);
return x + w;
}, 0);
}
protected _layoutFromLayoutDataColumn(children: HtmlComponent[], containerSize: Dimension) {
children.reduce((y, comp) => {
let margins = comp.margins();
let insets = comp.insets();
let h = (<FlexboxLayoutData>comp.layoutData).sizePx;
let bounds = new Rectangle(0, y - insets.top - margins.top, containerSize.width, h + insets.top + insets.bottom);
comp.setBounds(bounds);
return y + h;
}, 0);
}
}