@ux-aspects/ux-aspects
Version:
Open source user interface framework for building modern, responsive, mobile big data applications
830 lines • 130 kB
JavaScript
/* eslint-disable @typescript-eslint/no-empty-object-type */
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DOWN_ARROW, ENTER, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
import { DomPortalOutlet, TemplatePortal } from '@angular/cdk/portal';
import { ApplicationRef, ChangeDetectionStrategy, Component, ComponentFactoryResolver, ContentChild, ElementRef, EventEmitter, Injector, Input, NgZone, Output, Renderer2, ViewChild, ViewContainerRef, inject, } from '@angular/core';
import { hierarchy, interpolate, linkVertical, select, transition, tree, zoom, zoomTransform, } from 'd3';
import { Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { FocusIndicatorService } from '../../directives/accessibility/index';
import { ResizeService } from '../../directives/resize/index';
import * as i0 from "@angular/core";
import * as i1 from "../../directives/accessibility/focus-indicator/focus-indicator-origin/focus-indicator-origin.directive";
import * as i2 from "@angular/common";
import * as i3 from "../icon/icon.component";
export class OrganizationChartComponent {
constructor() {
this._resizeService = inject(ResizeService);
this._componentFactoryResolver = inject(ComponentFactoryResolver);
this._injector = inject(Injector);
this._elementRef = inject(ElementRef);
this._appRef = inject(ApplicationRef);
this._viewContainerRef = inject(ViewContainerRef);
this._renderer = inject(Renderer2);
this._focusIndicator = inject(FocusIndicatorService);
this._ngZone = inject(NgZone);
/** Define the presentation of the connectors */
this.connector = 'elbow';
/** Define the duration of the transition animations */
this.duration = 750;
/** Define whether or not we can reveal additional parents */
this.showReveal = false;
/** Define the aria label for the reveal button */
this.revealAriaLabel = 'Reveal More';
/** Emit whenever a node is selected */
this.selectedChange = new EventEmitter(true);
/** Emit whenever the reveal button is pressed */
this.reveal = new EventEmitter();
/** Emit when the transition ends */
this.transitionEnd = new EventEmitter();
this._toggleNodesOnClick = true;
/** Store a flattened array of nodes */
this._nodeLayout = [];
/** Store a flattened array of links */
this._linkLayout = [];
/** Store the portal/outlets associated with some data */
this._portals = new Map();
/** Store the focus indicators associated with nodes */
this._indicators = new Map();
/** Store whether or not a transition is in progress */
this._isTransitioning = false;
/** Store whether or not a camera pan is in progress */
this._isPanning = false;
/** Determine if the component is initialised */
this._isInitialised = false;
/** Determine if the connector type has changed since the last render */
this._hasConnectorChanged = false;
/** Automatically unsubscribe from all subscriptions on destroy */
this._onDestroy = new Subject();
}
/** Defines whether nodes can be toggled or not */
set toggleNodesOnClick(toggleNodesOnClick) {
this._toggleNodesOnClick = coerceBooleanProperty(toggleNodesOnClick);
}
get toggleNodesOnClick() {
return this._toggleNodesOnClick;
}
/** Programmatically select an item */
set selected(selected) {
if (this.selected === selected || !selected) {
return;
}
if (this._isInitialised) {
this.select(selected);
this.centerNode(selected);
}
else {
this._pendingSelection = selected;
}
}
ngAfterViewInit() {
// before we do anything ensure they have provided a template
if (!this.nodeTemplate) {
throw new Error('Organization Chart - You must provide a node template!');
}
if (!this.nodeWidth || !this.nodeHeight) {
throw new Error('Organization Chart - You must specify a nodeWidth and nodeHeight');
}
// create the zoom drag listener
this._zoom = zoom()
.scaleExtent([1, 1])
.interpolate(interpolate)
.on('zoom', this.applyCameraPosition.bind(this))
.on('end', () => {
if (!this._isPanning) {
this.ensureNodesAreVisible();
}
});
// set up the selections
this._linksContainer = select(this.linksContainer.nativeElement);
this._nodesContainer = select(this.nodesContainer.nativeElement);
// setup the zoom on the node layer
this._ngZone.runOutsideAngular(() => this._nodesContainer.call(this._zoom));
// perform the initial render
this.render();
// ensure we set the initial chart size
this._width = this._elementRef.nativeElement.offsetWidth;
this._height = this._elementRef.nativeElement.offsetHeight;
// watch for any resizing of the chart
const resize$ = this._resizeService.addResizeListener(this._elementRef.nativeElement);
// on size change immediate update the width and height measurements
resize$.pipe(takeUntil(this._onDestroy)).subscribe(this.onResize.bind(this));
// after a debounce ensure nodes are visible
resize$
.pipe(takeUntil(this._onDestroy), debounceTime(this.duration))
.subscribe(this.ensureNodesAreVisible.bind(this));
// initially horizontally center the root node
this.centerNode(this.dataset, OrganizationChartAxis.Horizontal, false);
// initally move the camera down slightly so the root node does not appear at the very top of the chart
this.moveCamera(0, 150, false);
// mark this component as initialised
this._isInitialised = true;
}
ngOnChanges(changes) {
if (changes.connector && !changes.connector.firstChange) {
this._hasConnectorChanged = true;
}
// if only the selected property has changed then don't re-render as this is handled by the setter
if (Object.keys(changes).length === 1 && changes.selected) {
return;
}
if (this._isInitialised) {
this.render();
}
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
// correctly dispose all portals and outlets
this._portals.forEach(node => {
node.portal.detach();
node.outlet.dispose();
});
}
/** Perform the actual rendering of the chart */
render() {
// perform the layout algorithm on the current dataset
this.updateLayout();
// select all the existing links and nodes
this.updateSelections();
// create a d3 transition based in the specified transition time
const defaultTransition = transition('organizationChartDefaultTransition')
.duration(this.duration)
.on('start', () => (this._isTransitioning = true))
.on('end', () => {
this._isTransitioning = false;
this.transitionEnd.emit();
});
// render the links when they are first added to the DOM
this._links
.enter()
.insert('path')
.attr('class', 'ux-organization-chart-link')
.attr('d', link => this.getLinkPath(link))
.attr('opacity', -2)
.transition(defaultTransition)
.attr('d', link => this.getLinkPath(link))
.attr('opacity', 1);
// define the standard transition while the link is 'alive'
this._links
.transition()
.duration(this._hasConnectorChanged ? 0 : this.duration)
.attr('d', link => this.getLinkPath(link));
// apply transitions when removing nodes
this._links
.exit()
.transition(defaultTransition)
.attr('d', (link) => this.getCollapsedLinkPath(link))
.attr('opacity', 0)
.remove();
// when a node is first added to the DOM position it
this._nodes
.enter()
.append('div')
.attr('class', 'ux-organization-chart-node')
.style('width', this.nodeWidth + 'px')
.style('height', this.nodeHeight + 'px')
.style('left', node => (node.parent ? node.parent.x : node.x) + 'px')
.style('top', node => (node.parent ? node.parent.y : node.y) + 'px')
.style('opacity', 0)
.on('keydown', (event, node) => this.onKeydown(event, node))
.on('focus', (event, node) => this.onFocus(node))
.on('click', (event, node) => this.onClick(node))
.each(this.renderNodeTemplate.bind(this))
.each((node, index, group) => this.monitorFocus(group[index], node))
.transition(defaultTransition)
.style('left', node => node.x + 'px')
.style('top', node => node.y + 'px')
.style('opacity', 1);
// apply any movements while nodes are 'alive'
this._nodes
.transition(defaultTransition)
.style('left', node => node.x + 'px')
.style('top', node => node.y + 'px');
// apply transitions when removing nodes
this._nodes
.exit()
.transition(defaultTransition)
.style('left', (node) => (node.parent ? node.parent.x : node.x) + 'px')
.style('opacity', 0)
.remove()
.on('end', (node) => this.destroyNode(node));
// update the position of the reveal button
select(this.revealElement.nativeElement)
.style('left', this.nodeWidth / 2 - this.revealElement.nativeElement.offsetWidth / 2 + 'px')
.style('top', -(this.nodeHeight / 2 + this.revealElement.nativeElement.offsetHeight / 2) + 'px');
// after any new links and nodes have been created or removed we should update the selections
this.updateSelections();
// update the selected classes - ensure there is always a selected node
if (!this._selected) {
this.select(this._pendingSelection || this.dataset);
this._pendingSelection = null;
}
// set the tab indexes and aria labels for any newly added items
this.setNodeAttributes();
// apply the current camera position to any new nodes/links
this.applyCameraPosition();
// reset the connector changed status
this._hasConnectorChanged = false;
}
/** Select a specified node */
select(node) {
// get the node in the desired format
node = this.coerceDataNode(node);
// check if the node is already selected
if (this._selected === node) {
return;
}
// ensure all parents are expanded
this.expandParents(node);
// deselect any current node
this.deselect(false);
// if the selected item has changed then store the latest selection
this._selected = node;
// emit the latest selection
this.selectedChange.emit(this._selected);
// show reveal any nodes that may previously have been hidden but are now visible due to selection
if (this._isInitialised) {
this.render();
}
// add the styling to the selected node
this._renderer.addClass(this.getNodeElement(this._selected), 'ux-organization-chart-node-selected');
// update the styling and tabindexes
this.setNodeAttributes();
}
/** Deselect the currently selected node */
deselect(emit = true) {
if (this._nodes) {
this._nodes
.nodes()
.forEach(element => this._renderer.removeClass(element, 'ux-organization-chart-node-selected'));
}
if (emit && !!this._selected) {
this._selected = null;
this.selectedChange.next(null);
// update the tab indexes and aria labels
this.setNodeAttributes();
}
}
/** Toggle the collapsed state of a node */
toggle(node) {
if (this._isTransitioning) {
return;
}
// get the node in the desired format
node = this.coercePointNode(node);
// ensure the clicked node is selected
this.select(node);
// apply the appropriate action
this.isExpanded(node) ? this.collapse(node) : this.expand(node);
}
/** Expand a node */
expand(node) {
if (this._isTransitioning || !this.toggleNodesOnClick) {
return;
}
// get the node in the desired format
node = this.coercePointNode(node);
// ensure this node and all parent nodes are expanded
node.ancestors().forEach(_node => (_node.data.expanded = true));
// re-render the nodes
this.render();
// if the node has children then we want to move the camera to a child node
if (Array.isArray(node.data.children) && node.data.children.length > 0) {
// center on the middle child
this.centerNode(node.data.children[Math.floor(node.data.children.length / 2)]);
}
else {
this.centerNode(node);
}
}
/** Collapse a node */
collapse(node) {
// do nothing if a transition is currently in progress
if (this._isTransitioning) {
return;
}
// get the node in the desired format
node = this.coercePointNode(node);
// ensure this node and all child nodes are collapse
node.descendants().forEach(_node => (_node.data.expanded = false));
// re-render the nodes
this.render();
// center the node that has just been collapsed
this.centerNode(node);
}
/** Move a specific node to the center of the screen */
centerNode(node, axis = OrganizationChartAxis.Both, animate = true) {
// get the node in the desired format
node = this.coercePointNode(node);
// get the current camera position
const camera = this.getCameraPosition();
const x = axis === OrganizationChartAxis.Vertical
? camera.x
: this._width / 2 - (node.x + this.nodeWidth / 2);
const y = axis === OrganizationChartAxis.Horizontal
? camera.y
: this._height / 2 - (node.y + this.nodeHeight / 2);
// update the camera position
this.setCameraPosition(x, y, animate);
}
/** Explicity set the position of the camera */
setCameraPosition(x, y, animate = true) {
// get the current transform
let camera = zoomTransform(this._nodesContainer.node());
// do nothing if the co-orindates have not changed
if (camera.x === x && camera.y === y) {
return;
}
// update the camera position
camera = camera.translate(x - camera.x, y - camera.y);
// indicate that the camera is panning programmatically
this._isPanning = true;
if (animate) {
this._nodesContainer
.transition()
.duration(this.duration)
.call(this._zoom.transform, camera)
.on('end interrupt cancel', () => (this._isPanning = false));
}
else {
this._nodesContainer.call(this._zoom.transform, camera);
this._isPanning = false;
}
}
/** Move the camera an amount from its current position */
moveCamera(x, y, animate = true) {
// get the current camera position
const camera = this.getCameraPosition();
this.setCameraPosition(camera.x + x, camera.y + y, animate);
}
/** Focus a given node */
focus(node) {
this.focusNode(this.coercePointNode(node));
}
/** Focus the root node */
_focusRootNode() {
this.focusNode(this.coercePointNode(this.dataset));
}
/** Destroy the outlet and portal associated with a node */
destroyNode(node) {
// get the node in a consistent format
node = this.coercePointNode(node);
// remove focus monitoring
if (this._indicators.has(node.data)) {
// remove the focus monitoring
this._indicators.get(node.data).destroy();
// remove the indicator from the list of indicators
this._indicators.delete(node.data);
}
// if there is not portal/outlets associated with this node then do nothing
if (!this._portals.has(node.data)) {
return;
}
// get the portal and outlet from the map
const portalRef = this._portals.get(node.data);
// perform the cleanup
portalRef.portal.detach();
portalRef.outlet.dispose();
// remove this entry from the map
this._portals.delete(node.data);
}
// update the data structure for the node and link layouts
updateLayout() {
this._layout = this.getLayout();
this._nodeLayout = this._layout.descendants();
this._linkLayout = this._layout.links();
}
/** Ensure the selections stay in sync with the view */
updateSelections() {
// select all the newly added dom nodes and associate the dataset
this._nodes = this._nodesContainer
.selectAll('.ux-organization-chart-node')
.data(this._nodeLayout, (node) => node.data.id.toString());
// select all the newly added path nodes
this._links = this._linksContainer
.selectAll('.ux-organization-chart-link')
.data(this._linkLayout, (link) => {
return `${link.source.data.id}-${link.target.data.id}`;
});
}
/** Render the content of the node based on the template provided */
renderNodeTemplate(node, index, group) {
// create the context for the node
const context = {
data: node.data.data,
node: node.data,
focused: false,
};
// the focused state should be a getter
Object.defineProperty(context, 'focused', {
get: () => this._focused === node.data,
});
// create the outlet to insert the Template and the portal from the TemplateRef
const outlet = this.createPortalOutlet(group[index]);
const portal = new TemplatePortal(this.nodeTemplate, this._viewContainerRef, context);
// insert the TemplateRef into the specified region
portal.attach(outlet);
// store the portal and outlet so we can correctly dispose of the nodes
this._portals.set(node.data, { portal, outlet });
}
/** Handle any zoom events (we use zoom for panning behaviour) */
applyCameraPosition() {
// get the new x and y position
let { x, y } = zoomTransform(this._nodesContainer.node());
// round the precision to integers to prevent any anti-aliasing
x = Math.round(x);
y = Math.round(y);
// transform the position of the reveal button
this._renderer.setStyle(this.revealElement.nativeElement, 'transform', `translate(${x}px, ${y}px)`);
// transform the position of the nodes
this._nodesContainer
.selectAll('.ux-organization-chart-node')
.style('transform', `translate(${x}px, ${y}px)`);
// transform the position of the links
this._linksContainer
.selectAll('.ux-organization-chart-link')
.attr('transform', `translate(${x} ${y})`);
}
/** Get the data in with the required layout information */
getLayout() {
// create a hierarchical representation of the data - don't include collapsed nodes
const treeHierarchy = hierarchy(this.dataset, node => Array.isArray(node.children) && node.expanded ? node.children : []);
// create our layout
const layout = tree()
.nodeSize([this.nodeWidth, this.nodeHeight])
.separation(this.getNodeSpacing.bind(this));
// process the data with the layout
const treeLayout = layout(treeHierarchy);
// calculate the vertical spacing
const verticalSpacing = this.verticalSpacing === undefined ? this.nodeHeight : this.verticalSpacing;
// set the vertical spacing
treeLayout.each(data => (data.y = data.depth * (this.nodeHeight + verticalSpacing)));
return treeLayout;
}
/** Determine how much horizontal spacing should be between nodes */
getNodeSpacing(nodeOne, nodeTwo) {
// if the nodes are not siblings then space further apart
if (nodeOne.parent !== nodeTwo.parent) {
return 2;
}
// if they are siblings they should be closer together
return 1.5;
}
/** Ensure we consistently use the HierarchyPoint data structure */
coercePointNode(node) {
// determine if this is a raw data node or a hierarchy point
// eslint-disable-next-line no-prototype-builtins
if (node.hasOwnProperty('depth') && node.hasOwnProperty('x') && node.hasOwnProperty('y')) {
return node;
}
// otherwise find the matching node
const match = this._nodeLayout.find(_node => _node.data === node);
// if the data does not exist in the hierarchy throw an exception
if (!match) {
throw new Error('The node does not exist in the hierarchy');
}
return match;
}
coerceDataNode(node) {
// eslint-disable-next-line no-prototype-builtins
if (node.hasOwnProperty('depth') && node.hasOwnProperty('x') && node.hasOwnProperty('y')) {
return node.data;
}
return node;
}
/** Handle chart resize events */
onResize({ width, height }) {
this._width = width;
this._height = height;
}
/** Deteremine if a node is expanded or collapsed */
isExpanded(node) {
return !!node.data.expanded;
}
/** Get the current position of the camera */
getCameraPosition() {
return zoomTransform(this._nodesContainer.node());
}
/** Get the SVG line definition for each link */
getLinkPath(pointLink) {
if (this.connector === 'elbow') {
const source = {
x: pointLink.source.x + this.nodeWidth / 2,
y: pointLink.source.y + this.nodeHeight,
};
const target = { x: pointLink.target.x + this.nodeWidth / 2, y: pointLink.target.y };
return ('M' +
source.x +
',' +
source.y +
'v' +
(target.y - source.y) / 2 +
'h' +
(target.x - source.x) +
'v' +
(target.y - source.y) / 2);
}
else {
const source = {
x: pointLink.source.x + this.nodeWidth / 2,
y: pointLink.source.y + this.nodeHeight / 2,
};
const target = {
x: pointLink.target.x + this.nodeWidth / 2,
y: pointLink.target.y + this.nodeHeight / 2,
};
return linkVertical()({ source: [source.x, source.y], target: [target.x, target.y] });
}
}
/** Get the link path line defintion when the link is collapsing */
getCollapsedLinkPath(pointLink) {
return this.getLinkPath({ source: pointLink.source, target: pointLink.source });
}
/** Create a dynamic region that Angular can insert into */
createPortalOutlet(element) {
return new DomPortalOutlet(element, this._componentFactoryResolver, this._appRef, this._injector);
}
/** Make the appropriate node tabbable and update aria attributes */
setNodeAttributes() {
for (const element of this._nodes.nodes()) {
// intially the tab index of all items to -1
this._renderer.setAttribute(element, 'tabindex', '-1');
// set the expanded aria attribute
this._renderer.setAttribute(element, 'aria-expanded', this.getNodeData(element).data.expanded ? 'true' : 'false');
}
// if there is a selected item then it should be tabbable otherwise make the root tabbable
if (this._selected) {
this._renderer.setAttribute(this.getNodeElement(this._selected), 'tabindex', '0');
}
}
/** Get the element that represents a given node */
getNodeElement(node) {
node = this.coercePointNode(node);
// find the element that matches the node data
const index = this._nodes.data().indexOf(node);
return this._nodes.nodes()[index];
}
/** Get the element that represents a given node */
getNodeData(node) {
// find the element that matches the node element
const index = this._nodes.nodes().indexOf(node);
return this._nodes.data()[index];
}
/** Handle click events */
onClick(node) {
if (!this.toggleNodesOnClick) {
return;
}
this.toggle(node);
}
/** Handle keyboard events */
onKeydown(event, node) {
if (!this.toggleNodesOnClick) {
return;
}
switch (event.keyCode) {
case DOWN_ARROW:
event.preventDefault();
// if the node is collapsed and has children expand
if (!node.data.expanded &&
Array.isArray(node.data.children) &&
node.data.children.length > 0) {
return this.expand(node);
}
return this.focusChild(node);
case RIGHT_ARROW:
event.preventDefault();
return this.focusNextSibling(node);
case UP_ARROW:
event.preventDefault();
return this.focusParent(node);
case LEFT_ARROW:
event.preventDefault();
return this.focusPreviousSibling(node);
case ENTER:
return this.toggle(node);
}
}
/** When a node receives focus */
onFocus(node) {
if (!this.isNodeInViewport(node, this._width * 0.1, this._height * 0.1)) {
this.centerNode(node);
}
}
/** Move focus to the parent node */
focusParent(node) {
if (node.parent) {
this.focusNode(node.parent);
}
else if (this.revealElement) {
this.revealElement.nativeElement.focus();
// center the root node to ensure the reveal button is in view
this.centerNode(this.dataset);
}
}
/** Move focus to the child node */
focusChild(node) {
if (Array.isArray(node.children) && node.children.length > 0) {
this.focusNode(node.children[Math.floor(node.children.length / 2)]);
}
}
/** Move focus to the sibling on the left */
focusPreviousSibling(node) {
if (node.parent) {
this.focusNode(node.parent.children[node.parent.children.indexOf(node) - 1]);
}
}
/** Move focus to the sibling on the right */
focusNextSibling(node) {
if (node.parent) {
this.focusNode(node.parent.children[node.parent.children.indexOf(node) + 1]);
}
}
/** Focus a given node */
focusNode(node) {
if (node) {
this.getNodeElement(node).focus({ preventScroll: true });
// ensure we don't perform scrolling if the node is not in view (we rely on preventScroll as IE doesn't support it)
this.nodesContainer.nativeElement.scrollTop = 0;
this.nodesContainer.nativeElement.scrollLeft = 0;
}
}
/** Determine if a node is fully visible within the viewport */
isNodeInViewport(node, insetX = 0, insetY = 0) {
const { x, y } = this.getCameraPosition();
const left = node.x + x;
const top = node.y + y;
const right = node.x + x + this.nodeWidth;
const bottom = node.y + y + this.nodeHeight;
return (left >= insetX &&
top >= insetY &&
right <= this._width - insetX &&
bottom <= this._height - insetY);
}
/** Determine if a node is fully outside of the viewport */
isNodeOutsideViewport(node, insetX = 0, insetY = 0) {
const { x, y } = this.getCameraPosition();
const left = node.x + x + this.nodeWidth;
const top = node.y + y + this.nodeHeight;
const right = node.x + x;
const bottom = node.y + y;
return (left < insetX ||
top < insetY ||
right > this._width - insetX ||
bottom > this._height - insetY);
}
/** Determine how far a node is from being within the viewport */
getDistanceFromViewport(node, insetX = 0, insetY = 0) {
// if the node is in the viewport then it will always be 0, 0
if (!this.isNodeOutsideViewport(node, insetX, insetY)) {
return [0, 0];
}
const { x, y } = this.getCameraPosition();
const left = insetX - (node.x + x + this.nodeWidth);
const top = insetY - (node.y + y + this.nodeHeight);
const right = node.x + x - (this._width - insetX);
const bottom = node.y + y - (this._height - insetY);
let horizontal = 0;
let vertical = 0;
if (left > 0 && left > right) {
horizontal = left;
}
if (right > 0 && left < right) {
horizontal = -right;
}
if (top > 0 && top > bottom) {
vertical = top;
}
if (bottom > 0 && top < bottom) {
vertical = -bottom;
}
// calculate the distances on both axis
return [horizontal, vertical];
}
/** Begin monitoring the element focus so we only show styling when navigated by keyboard */
monitorFocus(element, node) {
// create the focus indicator
const indicator = this._focusIndicator.monitor(element, {
checkChildren: false,
programmaticFocusIndicator: true,
});
// store the currently selected node as an instance variable
indicator.isFocused$.pipe(takeUntil(this._onDestroy)).subscribe(isFocused => {
// by default the CDK runs this outside of NgZone however we need it to run inside NgZone to update the node template
this._ngZone.run(() => {
if (isFocused) {
this._focused = node.data;
}
else if (node.data === this._focused) {
this._focused = null;
}
});
});
// store the focus indicator reference
this._indicators.set(node.data, indicator);
}
// ensure that there are at least some nodes visible
ensureNodesAreVisible() {
// determine how many nodes are currently visible
const visibleCount = this._nodes.filter(node => !this.isNodeOutsideViewport(node)).size();
if (visibleCount > 0) {
return;
}
// get the distance each node is from being within the viewport
const distances = this._nodes
.data()
.map(node => this.getDistanceFromViewport(node, this.nodeWidth * 1.25, this.nodeHeight * 1.5));
// find the closest node
const [x, y] = distances.reduce((previous, current) => {
const [previousX, previousY] = previous;
const [currentX, currentY] = current;
return Math.abs(previousX) + Math.abs(previousY) < Math.abs(currentX) + Math.abs(currentY)
? previous
: current;
});
// move the camera by the required amount
this.moveCamera(x, y);
}
/** Expand all parent nodes */
expandParents(node) {
// get the parent node
let parent = this.getParent(node);
while (parent) {
parent.expanded = true;
parent = this.getParent(parent);
}
}
/** Get the parent of a given node */
getParent(node) {
return [this.coerceDataNode(this.dataset), ...this.getAllChildren(this.dataset)].find(_node => {
if (!Array.isArray(_node.children)) {
return false;
}
return _node.children.find((child) => child.id === node.id);
});
}
/** Get a flat array of all the nodes childrent */
getAllChildren(node) {
const children = node.children || [];
// check for any children on the children
return [
...children,
...children.reduce((accumulation, child) => [...accumulation, ...this.getAllChildren(child)], []),
].map(child => this.coerceDataNode(child));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.12", ngImport: i0, type: OrganizationChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.12", type: OrganizationChartComponent, selector: "ux-organization-chart", inputs: { dataset: "dataset", connector: "connector", nodeWidth: "nodeWidth", nodeHeight: "nodeHeight", duration: "duration", verticalSpacing: "verticalSpacing", showReveal: "showReveal", revealAriaLabel: "revealAriaLabel", toggleNodesOnClick: "toggleNodesOnClick", selected: "selected" }, outputs: { selectedChange: "selectedChange", reveal: "reveal", transitionEnd: "transitionEnd" }, queries: [{ propertyName: "revealTemplate", first: true, predicate: ["revealTemplate"], descendants: true }, { propertyName: "nodeTemplate", first: true, predicate: ["nodeTemplate"], descendants: true }], viewQueries: [{ propertyName: "revealElement", first: true, predicate: ["revealElement"], descendants: true, static: true }, { propertyName: "linksContainer", first: true, predicate: ["links"], descendants: true, static: true }, { propertyName: "nodesContainer", first: true, predicate: ["nodes"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Add a button above the root node to load additional parent items -->\n<button #revealElement\n uxFocusIndicatorOrigin\n class=\"ux-organization-chart-reveal\"\n tabindex=\"-1\"\n [attr.aria-label]=\"revealAriaLabel\"\n [hidden]=\"!showReveal\"\n (click)=\"reveal.emit(); _focusRootNode()\"\n (keydown.ArrowDown)=\"_focusRootNode(); $event.preventDefault()\">\n\n <!-- Display Reveal Template -->\n <ng-container [ngTemplateOutlet]=\"revealTemplate || defaultRevealTemplate\"></ng-container>\n</button>\n\n<!-- Show the links connecting each node -->\n<svg #links class=\"ux-organization-chart-links\"></svg>\n\n<!-- Show the nodes containing information about each item -->\n<div #nodes class=\"ux-organization-chart-nodes\"></div>\n\n<!-- Provide a default reveal template -->\n<ng-template #defaultRevealTemplate>\n <ux-icon name=\"tab-up\"></ux-icon>\n</ng-template>", dependencies: [{ kind: "directive", type: i1.FocusIndicatorOriginDirective, selector: "[uxFocusIndicatorOrigin]" }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i3.IconComponent, selector: "ux-icon", inputs: ["name", "size", "rotate", "flipHorizontal", "flipVertical"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.12", ngImport: i0, type: OrganizationChartComponent, decorators: [{
type: Component,
args: [{ selector: 'ux-organization-chart', changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Add a button above the root node to load additional parent items -->\n<button #revealElement\n uxFocusIndicatorOrigin\n class=\"ux-organization-chart-reveal\"\n tabindex=\"-1\"\n [attr.aria-label]=\"revealAriaLabel\"\n [hidden]=\"!showReveal\"\n (click)=\"reveal.emit(); _focusRootNode()\"\n (keydown.ArrowDown)=\"_focusRootNode(); $event.preventDefault()\">\n\n <!-- Display Reveal Template -->\n <ng-container [ngTemplateOutlet]=\"revealTemplate || defaultRevealTemplate\"></ng-container>\n</button>\n\n<!-- Show the links connecting each node -->\n<svg #links class=\"ux-organization-chart-links\"></svg>\n\n<!-- Show the nodes containing information about each item -->\n<div #nodes class=\"ux-organization-chart-nodes\"></div>\n\n<!-- Provide a default reveal template -->\n<ng-template #defaultRevealTemplate>\n <ux-icon name=\"tab-up\"></ux-icon>\n</ng-template>" }]
}], propDecorators: { dataset: [{
type: Input
}], connector: [{
type: Input
}], nodeWidth: [{
type: Input
}], nodeHeight: [{
type: Input
}], duration: [{
type: Input
}], verticalSpacing: [{
type: Input
}], showReveal: [{
type: Input
}], revealAriaLabel: [{
type: Input
}], toggleNodesOnClick: [{
type: Input
}], selected: [{
type: Input
}], selectedChange: [{
type: Output
}], reveal: [{
type: Output
}], transitionEnd: [{
type: Output
}], revealTemplate: [{
type: ContentChild,
args: ['revealTemplate', { static: false }]
}], nodeTemplate: [{
type: ContentChild,
args: ['nodeTemplate', { static: false }]
}], revealElement: [{
type: ViewChild,
args: ['revealElement', { static: true }]
}], linksContainer: [{
type: ViewChild,
args: ['links', { static: true }]
}], nodesContainer: [{
type: ViewChild,
args: ['nodes', { static: true }]
}] } });
export var OrganizationChartAxis;
(function (OrganizationChartAxis) {
OrganizationChartAxis[OrganizationChartAxis["Horizontal"] = 0] = "Horizontal";
OrganizationChartAxis[OrganizationChartAxis["Vertical"] = 1] = "Vertical";
OrganizationChartAxis[OrganizationChartAxis["Both"] = 2] = "Both";
})(OrganizationChartAxis || (OrganizationChartAxis = {}));
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3JnYW5pemF0aW9uLWNoYXJ0LmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9jb21wb25lbnRzL29yZ2FuaXphdGlvbi1jaGFydC9vcmdhbml6YXRpb24tY2hhcnQuY29tcG9uZW50LnRzIiwiLi4vLi4vLi4vLi4vLi4vc3JjL2NvbXBvbmVudHMvb3JnYW5pemF0aW9uLWNoYXJ0L29yZ2FuaXphdGlvbi1jaGFydC5jb21wb25lbnQuaHRtbCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSw0REFBNEQ7QUFDNUQsT0FBTyxFQUFnQixxQkFBcUIsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQzVFLE9BQU8sRUFBRSxVQUFVLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsUUFBUSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDN0YsT0FBTyxFQUFFLGVBQWUsRUFBRSxjQUFjLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUN0RSxPQUFPLEVBRUwsY0FBYyxFQUNkLHVCQUF1QixFQUN2QixTQUFTLEVBQ1Qsd0JBQXdCLEVBQ3hCLFlBQVksRUFDWixVQUFVLEVBQ1YsWUFBWSxFQUNaLFFBQVEsRUFDUixLQUFLLEVBQ0wsTUFBTSxFQUdOLE1BQU0sRUFDTixTQUFTLEVBR1QsU0FBUyxFQUNULGdCQUFnQixFQUNoQixNQUFNLEdBQ1AsTUFBTSxlQUFlLENBQUM7QUFDdkIsT0FBTyxFQU1MLFNBQVMsRUFDVCxXQUFXLEVBQ1gsWUFBWSxFQUNaLE1BQU0sRUFDTixVQUFVLEVBQ1YsSUFBSSxFQUNKLElBQUksRUFDSixhQUFhLEdBQ2QsTUFBTSxJQUFJLENBQUM7QUFDWixPQUFPLEVBQUUsT0FBTyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBQy9CLE9BQU8sRUFBRSxZQUFZLEVBQUUsU0FBUyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDekQsT0FBTyxFQUFrQixxQkFBcUIsRUFBRSxNQUFNLHNDQUFz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QUFJLENBQUMsU0FBUyxHQUFHLElBQUksQ0FBQzthQUNyQyxLQUFLLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxVQUFVLEdBQUcsSUFBSSxDQUFDO2FBQ3ZDLEtBQUssQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDO2FBQ3BFLEtBQUssQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDO2FBQ25FLEtBQUssQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDO2FBQ25CLEVBQUUsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssRUFBRSxJQUFJLENBQUMsQ0FBQzthQUMzRCxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQzthQUNoRCxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQzthQUNoRCxJQUFJLENBQUMsSUFBSSxDQUFDLGtCQUFrQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQzthQUN4QyxJQUFJLENBQUMsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEVBQUUsSUFBSSxDQUFDLENBQUM7YUFDbkUsVUFBVSxDQUFDLGlCQUFpQixDQUFDO2FBQzdCLEtBQUssQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLElBQUksQ0FBQzthQUNwQyxLQUFLLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxJQUFJLENBQUM7YUFDbkMsS0FBSyxDQUFDLFNBQVMsRUFBRSxDQUFDLENBQUMsQ0FBQztRQUV2Qiw4Q0FBOEM7UUFDOUMsSUFBSSxDQUFDLE1BQU07YUFDUixVQUFVLENBQUMsaUJBQWlCLENBQUM7YUFDN0IsS0FBSyxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDO2FBQ3BDLEtBQUssQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFDO1FBRXZDLHdDQUF3QztRQUN4QyxJQUFJLENBQUMsTUFBTTthQUNSLElBQUksRUFBRTthQUNOLFVBQVUsQ0FBQyxpQkFBaUIsQ0FBQzthQUM3QixLQUFLLENBQ0osTUFBTSxFQUNOLENBQUMsSUFBa0QsRUFBRSxFQUFFLENBQ3JELENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsR0FBRyxJQUFJLENBQ2hEO2FBQ0EsS0FBSyxDQUFDLFNBQVMsRUFBRSxDQUFDLENBQUM7YUFDbkIsTUFBTSxFQUFFO2FBQ1IsRUFBRSxDQUFDLEtBQUssRUFBRSxDQUFDLElBQWtELEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUU3RiwyQ0FBMkM7UUFDM0MsTUFBTSxDQUFDLElBQUksQ0FBQyxhQUFhLENBQUMsYUFBYSxDQUFDO2FBQ3JDLEtBQUssQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDLFNBQVMsR0FBRyxDQUFDLEdBQUcsSUFBSSxDQUFDLGFBQWEsQ0FBQyxhQUFhLENBQUMsV0FBVyxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUM7YUFDM0YsS0FBSyxDQUNKLEtBQUssRUFDTCxDQUFDLENBQUMsSUFBSSxDQUFDLFVBQVUsR0FBRyxDQUFDLEdBQUcsSUFBSSxDQUFDLGFBQWEsQ0FBQyxhQUFhLENBQUMsWUFBWSxHQUFHLENBQUMsQ0FBQyxHQUFHLElBQUksQ0FDbEYsQ0FBQztRQUVKLDZGQUE2RjtRQUM3RixJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztRQUV4Qix1RUFBdUU7UU