@amcharts/amcharts5
Version:
amCharts 5
648 lines • 25.7 kB
JavaScript
import { Children } from "../util/Children";
import { Percent } from "../util/Percent";
import { Sprite } from "./Sprite";
import { Rectangle } from "./Rectangle";
import { HorizontalLayout } from "./HorizontalLayout";
import { VerticalLayout } from "./VerticalLayout";
import { GridLayout } from "./GridLayout";
import { populateString } from "../util/PopulateString";
import * as $array from "../util/Array";
import * as $type from "../util/Type";
import * as $utils from "../util/Utils";
/**
* A basic element that can have child elements, maintain their layout, and
* have a background.
*
* It can have any [[Sprite]] element as a child, from very basic shapes, to
* full-fledged charts.
*
* @see {@link https://www.amcharts.com/docs/v5/concepts/common-elements/containers/} for more info
* @important
*/
export class Container extends Sprite {
constructor() {
super(...arguments);
this._display = this._root._renderer.makeContainer();
this._childrenDisplay = this._root._renderer.makeContainer();
/**
* List of Container's child elements.
*/
this.children = new Children(this);
this._percentageSizeChildren = [];
this._percentagePositionChildren = [];
this._prevWidth = 0;
this._prevHeight = 0;
this._contentWidth = 0;
this._contentHeight = 0;
this._childrenPrep = false;
this._childrenUpdt = false;
}
_afterNew() {
super._afterNew();
this._display.addChild(this._childrenDisplay);
}
/**
* Returns the sub-element held in `key`, creating it with `f` if there is
* none, and merging `themeTags` into whichever element ends up there.
*
* The tags describe the role the element plays within this class, so an
* element passed in by a user (or by a parsed config) needs them just as
* much as a default one - without them the theme rules keyed on those tags
* never match and the element renders unstyled.
*
* @ignore
*/
_setDefaultTagged(key, themeTags, f) {
const value = this._setDefaultFn(key, f);
const sprite = value;
sprite.set("themeTags", $utils.mergeTags(sprite.get("themeTags", []), themeTags));
return value;
}
_afterChanged() {
super._afterChanged();
this._childrenPrep = false;
this._childrenUpdt = false;
}
_dispose() {
$array.eachReverse(this.allChildren(), (child) => {
child.dispose();
});
if (this.getPrivate("htmlElement")) {
this._root._removeHTMLContent(this);
}
super._dispose();
}
_beforeChanged() {
if (!this._childrenPrep) {
this._prepareChildren();
}
if (!this._childrenUpdt) {
this._updateChildren();
}
super._beforeChanged();
}
_changed() {
super._changed();
if (this.isDirty("interactiveChildren")) {
this._display.interactiveChildren = this.get("interactiveChildren", false);
}
if (this.isDirty("layout")) {
this._prevWidth = 0;
this._prevHeight = 0;
this.markDirtyBounds();
if (this._prevSettings.layout) {
this.children.each((child) => {
child.removePrivate("x");
child.removePrivate("y");
});
}
}
if (this.isDirty("paddingTop") || this.isDirty("paddingBottom") || this.isDirty("paddingLeft") || this.isDirty("paddingRight")) {
this.children.each((child) => {
child.markDirtyPosition();
});
this.markDirtyBounds();
}
if (this.isDirty("maskContent")) {
const childrenDisplay = this._childrenDisplay;
let contentMask = this._contentMask;
if (this.get("maskContent")) {
if (!contentMask) {
contentMask = Rectangle.new(this._root, {
x: -.5,
y: -.5,
width: this.width() + 1,
height: this.height() + 1
});
this._contentMask = contentMask;
childrenDisplay.addChildAt(contentMask._display, 0);
childrenDisplay.mask = contentMask._display;
}
}
else {
if (contentMask) {
childrenDisplay.removeChild(contentMask._display);
childrenDisplay.mask = null;
contentMask.dispose();
this._contentMask = undefined;
}
}
}
}
_updateSize() {
super._updateSize();
$array.each(this._percentageSizeChildren, (child) => {
child._updateSize();
});
$array.each(this._percentagePositionChildren, (child) => {
child.markDirtyPosition();
child._updateSize();
});
this.updateBackground();
}
updateBackground() {
const background = this.get("background");
let bounds = this._localBounds;
if (bounds && !this.isHidden()) {
let x = bounds.left;
let y = bounds.top;
let w = bounds.right - x;
let h = bounds.bottom - y;
let maxWidth = this.get("maxWidth");
let maxHeight = this.get("maxHeight");
if (maxHeight) {
if (h > maxHeight) {
h = maxHeight;
}
}
if (maxWidth) {
if (w > maxWidth) {
w = maxWidth;
}
}
let width = this.width();
let height = this.height();
if (background) {
background._setCAll({ width: w, height: h, x: x, y: y });
if (this._display.interactive) {
background._display.interactive = true;
}
}
const contentMask = this._contentMask;
if (contentMask) {
contentMask.setAll({ width: width + 1, height: height + 1 });
}
const verticalScrollbar = this.get("verticalScrollbar");
if (verticalScrollbar) {
verticalScrollbar.set("height", height);
verticalScrollbar.set("x", width - verticalScrollbar.width() - verticalScrollbar.get("marginRight", 0));
const visibleRatio = Math.min(1, height / this._contentHeight);
const maxStart = Math.max(0, 1 - visibleRatio);
let start = verticalScrollbar.get("start", 0);
// The viewport can grow past the current scroll position, e.g. when the
// container is resized while scrolled to the bottom. Without pulling the
// position back the content stays pushed up, leaving a gap below it.
// Only an out of range position is written, so a scroll animation that
// stays in range runs on - one that does not is stopped by `set`.
if (start > maxStart) {
start = maxStart;
verticalScrollbar._setC("start", start);
}
verticalScrollbar._setC("end", start + visibleRatio);
const bg = verticalScrollbar.get("background");
if (bg) {
bg.setAll({ width: verticalScrollbar.width(), height: height });
}
let visible = true;
if (this._contentHeight <= height) {
visible = false;
}
verticalScrollbar.setPrivate("visible", visible);
}
}
}
_applyThemes(force = false) {
if (super._applyThemes(force)) {
this.eachChildren((child) => {
child._applyThemes(force);
});
return true;
}
else {
return false;
}
}
_applyState(name) {
super._applyState(name);
if (this.get("setStateOnChildren")) {
this.eachChildren((child) => {
child.states.apply(name);
});
}
}
_applyStateAnimated(name, duration) {
super._applyStateAnimated(name, duration);
if (this.get("setStateOnChildren")) {
this.eachChildren((child) => {
child.states.applyAnimate(name, duration);
});
}
}
/**
* Returns container's inner width (width without padding) in pixels.
*
* @return Inner width (px)
*/
innerWidth() {
return this.width() - this.get("paddingRight", 0) - this.get("paddingLeft", 0);
}
/**
* Returns container's inner height (height without padding) in pixels.
*
* @return Inner height (px)
*/
innerHeight() {
return this.height() - this.get("paddingTop", 0) - this.get("paddingBottom", 0);
}
_getBounds() {
if (!this.get("html")) {
let width = this.get("width");
let height = this.get("height");
let pWidth = this.getPrivate("width");
let pHeight = this.getPrivate("height");
let bounds = {
left: 0,
top: 0,
right: this.width(),
bottom: this.height()
};
let layout = this.get("layout");
let horizontal = false;
let vertical = false;
if (layout instanceof HorizontalLayout || layout instanceof GridLayout) {
horizontal = true;
}
if (layout instanceof VerticalLayout) {
vertical = true;
}
if ((width != null || pWidth != null) && (height != null || pHeight != null) && !this.get("verticalScrollbar")) {
// void
}
else {
let m = Number.MAX_VALUE;
let l = m;
let r = -m;
let t = m;
let b = -m;
const paddingLeft = this.get("paddingLeft", 0);
const paddingTop = this.get("paddingTop", 0);
const paddingRight = this.get("paddingRight", 0);
const paddingBottom = this.get("paddingBottom", 0);
this.children.each((child) => {
if (child.get("position") != "absolute" && child.get("isMeasured")) {
let childBounds = child.adjustedLocalBounds();
let childX = child.x();
let childY = child.y();
let cl = childX + childBounds.left;
let cr = childX + childBounds.right;
let ct = childY + childBounds.top;
let cb = childY + childBounds.bottom;
if (horizontal) {
cl -= child.get("marginLeft", 0);
cr += child.get("marginRight", 0);
}
if (vertical) {
ct -= child.get("marginTop", 0);
cb += child.get("marginBottom", 0);
}
if (cl < l) {
l = cl;
}
if (cr > r) {
r = cr;
}
if (ct < t) {
t = ct;
}
if (cb > b) {
b = cb;
}
}
});
if (l == m) {
l = 0;
}
if (r == -m) {
r = 0;
}
if (t == m) {
t = 0;
}
if (b == -m) {
b = 0;
}
bounds.left = l - paddingLeft;
bounds.top = t - paddingTop;
bounds.right = r + paddingRight;
bounds.bottom = b + paddingBottom;
const minWidth = this.get("minWidth");
if ($type.isNumber(minWidth) && minWidth > 0) {
if (bounds.right - bounds.left < minWidth) {
if (bounds.right >= minWidth) {
bounds.left = bounds.right - minWidth;
}
else {
bounds.right = bounds.left + minWidth;
}
}
}
const minHeight = this.get("minHeight");
if ($type.isNumber(minHeight) && minHeight > 0) {
if (bounds.bottom - bounds.top < minHeight) {
if (bounds.bottom >= minHeight) {
bounds.top = bounds.bottom - minHeight;
}
else {
bounds.bottom = bounds.top + minHeight;
}
}
}
}
this._contentWidth = bounds.right - bounds.left;
this._contentHeight = bounds.bottom - bounds.top;
if ($type.isNumber(width)) {
bounds.left = 0;
bounds.right = width;
}
if ($type.isNumber(pWidth)) {
bounds.left = 0;
bounds.right = pWidth;
}
if ($type.isNumber(height)) {
bounds.top = 0;
bounds.bottom = height;
}
if ($type.isNumber(pHeight)) {
bounds.top = 0;
bounds.bottom = pHeight;
}
this._localBounds = bounds;
}
else {
let bounds = this._localBounds;
if (bounds) {
this._contentWidth = bounds.right - bounds.left;
this._contentHeight = bounds.bottom - bounds.top;
}
}
}
_updateBounds() {
const layout = this.get("layout");
if (layout) {
layout.updateContainer(this);
}
super._updateBounds();
this.updateBackground();
}
/**
* @ignore
*/
markDirty() {
super.markDirty();
this._root._addDirtyParent(this);
}
_prepareChildren() {
this._childrenPrep = true;
const innerWidth = this.innerWidth();
const innerHeight = this.innerHeight();
if (innerWidth != this._prevWidth || innerHeight != this._prevHeight) {
let layout = this.get("layout");
let horizontal = false;
let vertical = false;
if (layout) {
if (layout instanceof HorizontalLayout || layout instanceof GridLayout) {
horizontal = true;
}
if (layout instanceof VerticalLayout) {
vertical = true;
}
}
$array.each(this._percentageSizeChildren, (child) => {
if (!horizontal) {
let width = child.get("width");
if (width instanceof Percent) {
child.setPrivate("width", width.value * innerWidth);
}
}
if (!vertical) {
let height = child.get("height");
if (height instanceof Percent) {
child.setPrivate("height", height.value * innerHeight);
}
}
});
$array.each(this._percentagePositionChildren, (child) => {
child.markDirtyPosition();
child.markDirtyBounds();
});
this._prevWidth = innerWidth;
this._prevHeight = innerHeight;
this._sizeDirty = true;
this.updateBackground();
}
//this._handleStates();
}
_updateHTMLContent() {
const html = this.get("html", "");
if (html && html !== "") {
this._root._setHTMLContent(this, populateString(this, html));
}
else {
this._root._removeHTMLContent(this);
}
this._root._positionHTMLElement(this);
}
/**
* If scrolling is enabled on the Container (by adding `verticalScrollbar`)
* the Container will scroll in such way so that target element becomes
* visible if its currently outside of view.
*
* @param child Target child
* @since 5.10.5
*/
scrollToChild(child) {
const verticalScrollbar = this.get("verticalScrollbar");
if (verticalScrollbar) {
let y = child.y();
let h = this.innerHeight();
let ch = child.height();
let contentH = this._contentHeight;
// scrolled all the way down is the last child sitting on the bottom edge -
// going past that only opens a gap below the content. Measured against
// the full height, same as the scroll position is clamped to.
let max = Math.max(0, 1 - this.height() / contentH);
if (y + ch * .7 + this._childrenDisplay.y > h || y - ch * .3 + this._childrenDisplay.y < 0) {
let pos = Math.max(0, Math.min(max, (y - ch / 2) / contentH));
verticalScrollbar.animate({ key: "start", to: pos, duration: verticalScrollbar.get("animationDuration", 0), easing: verticalScrollbar.get("animationEasing") });
}
}
}
_updateChildren() {
this._childrenUpdt = true;
if (this.isDirty("html")) {
this._updateHTMLContent();
}
if (this.isDirty("verticalScrollbar")) {
const verticalScrollbar = this.get("verticalScrollbar");
if (verticalScrollbar) {
verticalScrollbar._setParent(this);
verticalScrollbar.startGrip.setPrivate("visible", false);
verticalScrollbar.endGrip.setPrivate("visible", false);
this.set("maskContent", true);
this.set("paddingRight", verticalScrollbar.width() + verticalScrollbar.get("marginRight", 0) + verticalScrollbar.get("marginLeft", 0));
this._setDefaultTagged("background", ["background"], () => {
return Rectangle.new(this._root, {
fillOpacity: 0,
fill: this._root.interfaceColors.get("alternativeBackground")
});
});
this._vsbd0 = this.events.on("wheel", (event) => {
const wheelEvent = event.originalEvent;
// Ignore wheel event if it is happening on a non-chart element, e.g. if
// some page element is over the chart.
if ($utils.isLocalEvent(wheelEvent, this)) {
wheelEvent.preventDefault();
}
else {
return;
}
let shiftY = wheelEvent.deltaY / 5000;
const start = verticalScrollbar.get("start", 0);
const end = verticalScrollbar.get("end", 1);
if (start + shiftY <= 0) {
shiftY = -start;
}
if (end + shiftY >= 1) {
shiftY = 1 - end;
}
if (start + shiftY >= 0 && end + shiftY <= 1) {
verticalScrollbar._setC("start", start + shiftY);
verticalScrollbar._setC("end", end + shiftY);
}
});
this._disposers.push(this._vsbd0);
this._vsbd1 = verticalScrollbar.events.on("rangechanged", () => {
let h = this._contentHeight;
const childrenDisplay = this._childrenDisplay;
const contentMask = this._contentMask;
childrenDisplay.y = -verticalScrollbar.get("start", 0) * h;
childrenDisplay.markDirtyLayer();
if (contentMask) {
contentMask._display.y = -childrenDisplay.y;
childrenDisplay.mask = contentMask._display;
}
});
this._disposers.push(this._vsbd1);
this._display.addChild(verticalScrollbar._display);
}
else {
const previous = this._prevSettings.verticalScrollbar;
if (previous) {
this._display.removeChild(previous._display);
if (this._vsbd0) {
this._vsbd0.dispose();
}
if (this._vsbd1) {
this._vsbd1.dispose();
}
const childrenDisplay = this._childrenDisplay;
childrenDisplay.y = 0;
this.setPrivate("height", undefined);
this.set("maskContent", false);
this.set("paddingRight", undefined);
}
}
}
if (this.isDirty("background")) {
// TODO maybe this should dispose ?
const previous = this._prevSettings["background"];
if (previous) {
this._display.removeChild(previous._display);
}
const background = this.get("background");
if (background instanceof Sprite) {
background._setC("isMeasured", false);
background._setParent(this);
this._display.addChildAt(background._display, 0);
// Rectangle / RoundedRectangle backgrounds render sharper edges and
// strokes when crisp; default it on unless the user set it explicitly.
if (background instanceof Rectangle && !background.isUserSetting("crisp")) {
background._setC("crisp", true);
}
}
}
if (this.isDirty("mask")) {
const mask = this.get("mask");
const previous = this._prevSettings["mask"];
if (previous) {
this._display.removeChild(previous._display);
if (previous != mask) {
previous.dispose();
}
}
if (mask) {
const parent = mask.parent;
if (parent) {
parent.children.removeValue(mask);
}
mask._setParent(this);
this._display.addChildAt(mask._display, 0);
this._childrenDisplay.mask = mask._display;
}
else {
// mask removed: clear the clip, otherwise the children keep being
// clipped by the (now disposed) previous mask display. Fall back to
// the content mask if `maskContent` is in effect.
this._childrenDisplay.mask = this._contentMask ? this._contentMask._display : null;
}
}
}
_processTemplateField() {
super._processTemplateField();
this.children.each((child) => {
child._processTemplateField();
});
}
/**
* @ignore
*/
walkChildren(f) {
this.children.each((child, index) => {
if (child instanceof Container) {
child.walkChildren(f);
}
f(child, index);
});
}
eachChildren(f) {
const background = this.get("background");
if (background) {
f(background);
}
const verticalScrollbar = this.get("verticalScrollbar");
if (verticalScrollbar) {
f(verticalScrollbar);
}
const mask = this.get("mask");
if (mask) {
f(mask);
}
this.children.values.forEach((child) => {
f(child);
});
}
allChildren() {
const output = [];
this.eachChildren((x) => {
output.push(x);
});
return output;
}
_setDataItem(dataItem) {
const updated = (dataItem !== this._dataItem);
super._setDataItem(dataItem);
const html = this.get("html", "");
if (html && html !== "" && updated) {
this._root._setHTMLContent(this, populateString(this, html));
}
}
contentWidth() {
return this._contentWidth;
}
contentHeight() {
return this._contentHeight;
}
}
Container.className = "Container";
Container.classNames = Sprite.classNames.concat([Container.className]);
//# sourceMappingURL=Container.js.map