import { BaseType, Selection } from 'd3-selection';
import { D3Node, FlextreeD3Node, OrgChartDataItem, OrgChartOptions } from '../types';
import { d3 } from '../constants';
import { isEdge } from '../utils/core';
import { nodeHeight, nodeWidth } from '../utils/compact';
const collapseBtn = `
`;
/**
* This function basically redraws visible graph, based on nodes state
*/
export const restyleAllForeignObjectElements = (
options: OrgChartOptions,
svg: Selection,
) => {
svg
.selectAll>('.node-foreign-object')
.attr('width', ({ width }) => width)
.attr('height', ({ height }) => height)
.attr('x', () => 0)
.attr('y', () => 0);
svg
.selectAll>('.node-foreign-object-div')
.style('width', ({ width }: D3Node) => `${width}px`)
.style('height', ({ height }: D3Node) => `${height}px`)
.html(function (d) {
if (d.data._type === 'group-toggle' && d.parent) {
return options.compactCollapsedContent(d.parent);
}
return options.nodeContent(d);
});
};
const renderForeignObjectElements = (
nodeWrapperGroup: Selection, SVGGraphicsElement, string>,
onNodeClick: (e: MouseEvent, d: D3Node) => void,
) => {
// Add foreignObject element inside rectangle
const fo = nodeWrapperGroup
.patternify({
tag: 'foreignObject',
selector: 'node-foreign-object',
data: (d) => [d],
})
.style('overflow', 'visible')
.attr('cursor', 'pointer')
.on('click', (event: any, node: FlextreeD3Node) => {
if (event.target.classList.contains('node-button-foreign-object')) {
return;
}
onNodeClick(event, node);
});
// Add foreign object
fo.patternify({
tag: 'xhtml:div',
selector: 'node-foreign-object-div',
data: (d) => [d],
});
};
const updateForeignObjectElements = (
options: OrgChartOptions,
nodeUpdate: Selection, SVGGraphicsElement, string>,
) => {
nodeUpdate
.select('.node-foreign-object')
.attr('width', ({ width }) => width)
.attr('height', ({ height }) => height)
.attr('x', () => 0)
.attr('y', () => 0);
nodeUpdate
.select('.node-foreign-object-div')
.style('width', ({ width }: D3Node) => `${width}px`)
.style('height', ({ height }: D3Node) => `${height}px`)
.html(function (d) {
if (d.data._type === 'group-toggle' && d.parent) {
return options.compactCollapsedContent(d.parent);
}
return options.nodeContent(d);
});
};
const renderNodeButton = (
options: OrgChartOptions,
nodeWrapperGroup: Selection, SVGGraphicsElement, string>,
onButtonClick: (e: MouseEvent, d: D3Node) => void,
) => {
// Add Node button circle's group (expand-collapse button)
const nodeButtonGroups = nodeWrapperGroup
.patternify({
tag: 'g',
selector: 'node-button-g',
data: (d) => [d],
})
.on('click', onButtonClick);
nodeButtonGroups
.patternify({
tag: 'rect',
selector: 'node-button-rect',
data: (d) => [d],
})
.attr('opacity', 0)
.attr('pointer-events', 'all')
.attr('width', (d) => options.nodeButtonWidth(d))
.attr('height', (d) => options.nodeButtonHeight(d))
.attr('x', (d) => options.nodeButtonX(d))
.attr('y', (d) => options.nodeButtonY(d));
// Add expand collapse button content
nodeButtonGroups
.patternify({
tag: 'foreignObject',
selector: 'node-button-foreign-object',
data: (d) => [d],
})
.attr('width', (d) => options.nodeButtonWidth(d))
.attr('height', (d) => options.nodeButtonHeight(d))
.attr('x', (d) => options.nodeButtonX(d))
.attr('y', (d) => options.nodeButtonY(d))
.style('overflow', 'visible')
.patternify({
tag: 'xhtml:div',
selector: 'node-button-div',
data: (d) => [d],
})
.style('pointer-events', 'none')
.style('display', 'flex')
.style('width', '100%')
.style('height', '100%');
};
const updateNodeButton = (
options: OrgChartOptions,
nodeUpdate: Selection, SVGGraphicsElement, string>,
) => {
nodeUpdate
.select('.node-button-g')
.attr('transform', (d) => {
const x = options.layoutBindings[options.layout].buttonX(d);
const y = options.layoutBindings[options.layout].buttonY(d);
return `translate(${x},${y})`;
})
.attr('display', (d) => (options.isNodeButtonVisible(d) ? null : 'none'));
// Restyle node button circle
nodeUpdate.select('.node-button-foreign-object .node-button-div').html((node) => {
return options.buttonContent({ node, state: options });
});
// Restyle button texts
nodeUpdate
.select('.node-button-text')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('font-size', ({ children }) => {
if (children) return 40;
return 26;
})
.text(({ children }) => {
if (children) return '-';
return '+';
})
.attr('y', isEdge() ? 10 : 0);
};
const renderNodeCompact = (
options: OrgChartOptions,
nodeEnter: Selection, SVGGraphicsElement, string>,
onCompactGroupToggleButtonClick: (e: MouseEvent, d: D3Node) => void,
) => {
// Add Node rect for compactNoChildren mode
const nodeCompactGroup = nodeEnter.patternify({
tag: 'g',
selector: 'node-compact',
data: (d) => [d],
});
const nodeCompactGroupRect = nodeCompactGroup
.patternify({
tag: 'rect',
selector: 'node-compact__rect',
data: (d) => [d],
})
.attr('pointer-events', 'none')
.attr('width', (d) => nodeWidth(d, options) + options.compactNoChildrenMargin * 2)
.attr('height', (d) => {
const { children, compactNoChildren } = d;
if (children && children.length > 1 && options.compactNoChildren && compactNoChildren) {
const compactAsGroupChildrenSize =
d3.sum(
children,
(d) =>
options.layoutBindings[options.layout].compactDimension.sizeColumn(d) + options.compactMarginBetween(d),
) - options.compactMarginBetween(d);
return compactAsGroupChildrenSize + options.compactNoChildrenMargin * 2;
}
return nodeHeight(d, options) + options.compactNoChildrenMargin * 2;
});
const nodeCompactToggleBtnGroup = nodeCompactGroup
.patternify({
tag: 'g',
selector: 'node-compact__toggle-btn',
})
.attr('cursor', 'pointer')
.on('click', onCompactGroupToggleButtonClick);
nodeCompactToggleBtnGroup
.patternify({
tag: 'rect',
selector: 'node-compact__toggle-btn-rect',
})
.attr('width', 20)
.attr('height', 20)
.attr('fill', 'transparent');
nodeCompactToggleBtnGroup
.patternify({
tag: 'g',
selector: 'node-compact__toggle-btn-icon',
})
.html(options.compactToggleBtnIcon || collapseBtn);
options.compactNoChildrenUpdate(nodeCompactGroupRect);
};
const updateNodeCompact = (
options: OrgChartOptions,
nodeUpdate: Selection, SVGGraphicsElement, string>,
) => {
const compactGroup = nodeUpdate
.select('.node-compact')
.attr('transform', (d) => {
const { height } = d;
// todo: set to correct based on the layout
const x = -options.compactNoChildrenMargin;
const y = height - options.compactNoChildrenMargin + options.childrenMargin(d);
return `translate(${x},${y})`;
})
.attr('display', (d) => {
const { data, compactNoChildren } = d;
return compactNoChildren && data._expanded ? null : 'none';
});
compactGroup.select('.node-compact__rect').attr('height', (d) => {
const { children, compactNoChildren } = d;
if (children && children.length > 1 && compactNoChildren) {
const compactAsGroupChildrenSize =
d3.sum(
children,
(d) => options.layoutBindings[options.layout].compactDimension.sizeRow(d) + options.compactMarginBetween(d),
) - options.compactMarginBetween(d);
return compactAsGroupChildrenSize + options.compactNoChildrenMargin * 2;
}
return options.nodeHeight(d) + options.compactNoChildrenMargin * 2;
});
compactGroup
.select('.node-compact__toggle-btn')
.attr('transform', (d) => {
const { width } = d;
const x = width + options.compactNoChildrenMargin * 2 + options.compactToggleButtonMargin;
return `translate(${x},0)`;
})
.attr('display', (d) => {
const { data } = d;
return data._compactExpanded ? null : 'none';
});
compactGroup.select('.node-compact__toggle-btn-icon svg').attr('display', (d) => {
const { data } = d;
return data._compactExpanded ? null : 'none';
});
};
export const renderOrUpdateNodes = (
options: OrgChartOptions,
root: D3Node | undefined,
node: D3Node,
nodesSelection: Selection, SVGGraphicsElement, string>,
onNodeClick: (e: MouseEvent, d: D3Node) => void,
onButtonClick: (e: MouseEvent, d: D3Node) => void,
onCompactGroupToggleButtonClick: (e: MouseEvent, d: D3Node) => void,
) => {
const { x0, y0, x = 0, y = 0, width, height } = node;
// Enter any new nodes at the parent's previous position.
const nodeEnter = nodesSelection
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', (d: D3Node) => {
if (d == root) {
return `translate(${x0},${y0})`;
}
const xj = options.layoutBindings[options.layout].nodeJoinX({ x: x0, y: y0, width, height });
const yj = options.layoutBindings[options.layout].nodeJoinY({ x: x0, y: y0, width, height });
return `translate(${xj},${yj})`;
});
renderNodeCompact(options, nodeEnter, onCompactGroupToggleButtonClick);
// Add Node wrapper
const nodeWrapperGroup = nodeEnter;
// Add background rectangle for the nodes
nodeWrapperGroup.patternify({
tag: 'rect',
selector: 'node-rect',
data: (d) => [d],
});
renderForeignObjectElements(nodeWrapperGroup, onNodeClick);
renderNodeButton(options, nodeWrapperGroup, onButtonClick);
// Node update styles
const nodeUpdate = nodeEnter.merge(nodesSelection).style('font', '12px sans-serif');
// Transition to the proper position for the node
nodeUpdate
.transition()
.attr('opacity', 0)
.duration(options.duration)
.attr('transform', ({ x, y, width, height }) => {
return options.layoutBindings[options.layout].nodeUpdateTransform({ x, y, width, height });
})
.attr('opacity', 1);
// Style node rectangles
nodeUpdate
.select('.node-rect')
.attr('width', ({ width }) => width)
.attr('height', ({ height }) => height)
.attr('x', () => 0)
.attr('y', () => 0)
.attr('cursor', 'pointer')
.attr('rx', 3)
.attr('fill', 'none');
updateForeignObjectElements(options, nodeUpdate);
updateNodeCompact(options, nodeUpdate);
updateNodeButton(options, nodeUpdate);
nodeUpdate.each(function (node, i, arr) {
const nodeGroup = d3.select>(this);
if (node.data._type === 'group-toggle') {
options.compactCollapsedNodeUpdate(nodeGroup);
return;
}
options.nodeUpdate(nodeGroup, node, i, arr);
});
// Remove any exiting nodes after transition
nodesSelection
.exit()
.attr('opacity', 1)
.transition()
.duration(options.duration)
.attr('transform', () => {
const ex = options.layoutBindings[options.layout].nodeJoinX({ x, y, width, height });
const ey = options.layoutBindings[options.layout].nodeJoinY({ x, y, width, height });
return `translate(${ex},${ey})`;
})
.on('end', function (this: BaseType) {
d3.select>(this).remove();
})
.attr('opacity', 0);
};