UNPKG

@amcharts/amcharts5

Version:
648 lines 25.7 kB
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