orgchart
Version:
Simple and direct organization chart(tree-like hierarchy) library based on pure DOM.
1,311 lines (1,194 loc) • 169 kB
JavaScript
/*
* JavaScript OrgChart Component
* https://github.com/dabeng/OrgChart
*
* Copyright 2016, dabeng
* https://github.com/dabeng
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
'use strict';
(function (factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory(window, document);
} else {
window.OrgChart = factory(window, document);
}
}(function (window, document, undefined) {
const elementDataStore = new WeakMap();
const getWindow = () => (typeof globalThis !== 'undefined' && globalThis.window ? globalThis.window : window);
const getDocument = () => {
const currentWindow = getWindow();
return currentWindow && currentWindow.document
? currentWindow.document
: (typeof globalThis !== 'undefined' && globalThis.document ? globalThis.document : document);
};
const isElement = (value) => value && (value.nodeType === 1 || value.nodeType === 9 || value === getWindow());
const isNode = (value) => value && typeof value.nodeType === 'number';
const isNodeList = (value) => value && typeof value.length === 'number' && typeof value !== 'string' && !isNode(value);
const isHtmlString = (value) => typeof value === 'string' && value.trim().startsWith('<') && value.trim().endsWith('>');
const isPlainObject = (value) => Object.prototype.toString.call(value) === '[object Object]';
const unique = (elements) => Array.from(new Set(elements.filter(Boolean)));
function getElement(input, context) {
return normalizeInput(input, context || getDocument())[0] || null;
}
function getElements(input, context) {
return normalizeInput(input, context || getDocument());
}
function mergeObjects() {
return Object.assign.apply(Object, [{}].concat(Array.from(arguments)));
}
function forEachValue(collection, callback) {
if (Array.isArray(collection) || isNodeList(collection)) {
for (let index = 0; index < collection.length; index += 1) {
if (callback.call(collection[index], index, collection[index]) === false) {
break;
}
}
return collection;
}
Object.keys(collection || {}).some((key) => callback.call(collection[key], key, collection[key]) === false);
return collection;
}
function createEventLike(type) {
return {
type,
defaultPrevented: false,
preventDefault: function () {
this.defaultPrevented = true;
},
isDefaultPrevented: function () {
return this.defaultPrevented;
}
};
}
function normalizeSelector(selector) {
if (!selector) {
return selector;
}
return selector.replace(/:not\("([^"]+)"\)/g, ':not($1)');
}
function parseSelector(selector) {
const normalizedSelector = normalizeSelector(selector || '*');
const meta = {
selector: normalizedSelector,
first: false,
last: false,
gt: null,
hidden: false,
visible: false
};
meta.selector = meta.selector.replace(/:first\b/g, function () {
meta.first = true;
return '';
});
meta.selector = meta.selector.replace(/:last\b/g, function () {
meta.last = true;
return '';
});
meta.selector = meta.selector.replace(/:gt\((\d+)\)/g, function (_, index) {
meta.gt = Number(index);
return '';
});
meta.selector = meta.selector.replace(/:hidden\b/g, function () {
meta.hidden = true;
return '';
});
meta.selector = meta.selector.replace(/:visible\b/g, function () {
meta.visible = true;
return '';
});
meta.selector = meta.selector.trim() || '*';
return meta;
}
function isVisibleElement(element) {
if (!element || element.nodeType !== 1) {
return false;
}
const style = getWindow().getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
}
function matchesSelector(element, selector) {
if (!selector || selector === '*') {
return true;
}
if (!element || element.nodeType !== 1) {
return false;
}
try {
return element.matches(selector);
} catch (error) {
return false;
}
}
function filterBySelector(elements, selector) {
const meta = parseSelector(selector);
let filtered = elements.filter((element) => matchesSelector(element, meta.selector));
if (meta.hidden) {
filtered = filtered.filter((element) => !isVisibleElement(element));
}
if (meta.visible) {
filtered = filtered.filter((element) => isVisibleElement(element));
}
if (meta.gt !== null) {
filtered = filtered.filter((_, index) => index > meta.gt);
}
if (meta.first) {
filtered = filtered.slice(0, 1);
}
if (meta.last) {
filtered = filtered.slice(-1);
}
return filtered;
}
function createNodesFromHtml(html) {
const template = getDocument().createElement('template');
template.innerHTML = html.trim();
return Array.from(template.content.childNodes);
}
function toDataKey(key) {
return String(key).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
function getStoredState(element) {
if (!elementDataStore.has(element)) {
elementDataStore.set(element, {});
}
return elementDataStore.get(element);
}
function setState(element, key, value) {
const store = getStoredState(element);
store[toDataKey(key)] = value;
}
function getState(element, key) {
const store = getStoredState(element);
const normalizedKey = toDataKey(key);
if (Object.prototype.hasOwnProperty.call(store, normalizedKey)) {
return store[normalizedKey];
}
return undefined;
}
function getDatasetSnapshot(element) {
return element.dataset ? Object.assign({}, element.dataset) : {};
}
function normalizeInput(input, context) {
if (input === null || input === undefined) {
return [];
}
if (Array.isArray(input)) {
return unique(input.flatMap((item) => normalizeInput(item, context)));
}
if (isNode(input)) {
return [input];
}
if (isNodeList(input)) {
return Array.from(input).flatMap((item) => normalizeInput(item, context));
}
if (typeof input === 'string') {
if (isHtmlString(input)) {
return createNodesFromHtml(input);
}
const currentDocument = getDocument();
const parents = normalizeInput(context || currentDocument, currentDocument);
const selected = parents.length
? parents.flatMap((parent) => {
if (parent.nodeType !== 1 && parent.nodeType !== 9) {
return [];
}
return filterBySelector(Array.from(parent.querySelectorAll(parseSelector(input).selector)), input);
})
: filterBySelector(Array.from(currentDocument.querySelectorAll(parseSelector(input).selector)), input);
return unique(selected);
}
return [];
}
function toNodeArray(content) {
if (content instanceof DocumentFragment) {
return Array.from(content.childNodes);
}
if (Array.isArray(content)) {
return content.flatMap((item) => toNodeArray(item));
}
if (isNode(content)) {
return [content];
}
if (typeof content === 'string') {
return isHtmlString(content) ? createNodesFromHtml(content) : [getDocument().createTextNode(content)];
}
return [];
}
function cloneNodesForTarget(nodes, needsClone) {
return nodes.map((node) => (needsClone ? node.cloneNode(true) : node));
}
function createTriggeredEvent(eventLike, extraData) {
const type = typeof eventLike === 'string' ? eventLike : eventLike.type;
const event = new (getWindow().CustomEvent)(type, {
bubbles: true,
cancelable: true,
detail: extraData
});
event.originalEvent = event;
event.data = extraData;
event.delegateTarget = null;
return event;
}
function clampValue(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function isTransparentColor(colorValue) {
const normalizedColor = (colorValue || '').replace(/\s+/g, '').toLowerCase();
return !normalizedColor
|| normalizedColor === 'transparent'
|| normalizedColor === 'rgba(0,0,0,0)'
|| normalizedColor === 'hsla(0,0%,0%,0)';
}
function toReadableMinimapColor(colorValue) {
const rgbaMatch = /^rgba?\(([^)]+)\)$/i.exec((colorValue || '').trim());
const darkenFactor = 0.68;
let channelValues;
if (!rgbaMatch) {
return colorValue;
}
channelValues = rgbaMatch[1].split(',').slice(0, 3).map(function (value) {
return clampValue(Math.round(parseFloat(value.trim()) * darkenFactor), 0, 255);
});
if (channelValues.some(function (value) { return Number.isNaN(value); })) {
return colorValue;
}
return 'rgb(' + channelValues.join(', ') + ')';
}
function getMinimapNodeColor(nodeElement) {
const fallbackColor = 'rgba(217, 83, 79, 0.8)';
const titleElement = nodeElement ? nodeElement.querySelector('.title') : null;
const primaryColor = titleElement ? getWindow().getComputedStyle(titleElement).backgroundColor : '';
const secondaryColorCandidates = [
nodeElement,
titleElement,
nodeElement ? nodeElement.querySelector('.content') : null
].flatMap(function (element) {
const style = element ? getWindow().getComputedStyle(element) : null;
if (!style) {
return [];
}
return [
style.backgroundColor,
style.borderTopColor,
style.borderRightColor,
style.borderBottomColor,
style.borderLeftColor
];
});
return toReadableMinimapColor((!isTransparentColor(primaryColor) && primaryColor) || secondaryColorCandidates.find(function (color) {
return !isTransparentColor(color);
}) || fallbackColor);
}
const MINIMAP_NODE_HEIGHT = 18;
function renderMinimapNodes(chartElement, minimapContentElement, chartRect, chartScale) {
const fragment = getDocument().createDocumentFragment();
Array.from(chartElement.querySelectorAll('.node')).filter(function (nodeElement) {
return isVisibleElement(nodeElement);
}).forEach(function (nodeElement) {
const nodeRect = nodeElement.getBoundingClientRect();
const minimapNodeElement = getDocument().createElement('div');
const nodeTop = (nodeRect.top - chartRect.top) / chartScale;
minimapNodeElement.className = 'orgchart-minimap-node';
minimapNodeElement.style.left = Math.max((nodeRect.left - chartRect.left) / chartScale, 0) + 'px';
minimapNodeElement.style.top = Math.max(nodeTop + ((nodeRect.height / chartScale - MINIMAP_NODE_HEIGHT) / 2), 0) + 'px';
minimapNodeElement.style.width = Math.max(nodeRect.width / chartScale, 6) + 'px';
minimapNodeElement.style.height = MINIMAP_NODE_HEIGHT + 'px';
minimapNodeElement.style.backgroundColor = getMinimapNodeColor(nodeElement);
fragment.appendChild(minimapNodeElement);
});
minimapContentElement.innerHTML = '';
minimapContentElement.appendChild(fragment);
}
function getChartTransform(chartElement) {
let currentTransform;
let matrixValues;
let currentScale = 1;
let currentTranslateX = 0;
let currentTranslateY = 0;
if (!chartElement) {
return {
scale: currentScale,
translateX: currentTranslateX,
translateY: currentTranslateY
};
}
currentTransform = getWindow().getComputedStyle(chartElement).transform || chartElement.style.transform || 'none';
if (currentTransform && currentTransform !== 'none') {
if (currentTransform.indexOf('matrix3d(') === 0) {
matrixValues = currentTransform.slice(9, -1).split(',').map(function (value) {
return getWindow().parseFloat(value.trim());
});
currentScale = Math.abs(matrixValues[0]) || 1;
currentTranslateX = matrixValues[12] || 0;
currentTranslateY = matrixValues[13] || 0;
} else if (currentTransform.indexOf('matrix(') === 0) {
matrixValues = currentTransform.slice(7, -1).split(',').map(function (value) {
return getWindow().parseFloat(value.trim());
});
currentScale = Math.abs(matrixValues[0]) || 1;
currentTranslateX = matrixValues[4] || 0;
currentTranslateY = matrixValues[5] || 0;
} else if (currentTransform.indexOf('scale3d(') === 0) {
matrixValues = currentTransform.slice(8, -1).split(',').map(function (value) {
return getWindow().parseFloat(value.trim());
});
currentScale = Math.abs(matrixValues[0]) || 1;
} else if (currentTransform.indexOf('scale(') === 0) {
matrixValues = currentTransform.slice(6, -1).split(',').map(function (value) {
return getWindow().parseFloat(value.trim());
});
currentScale = Math.abs(matrixValues[0]) || 1;
}
}
return {
scale: currentScale,
translateX: currentTranslateX,
translateY: currentTranslateY
};
}
const OrgChart = function (elem, opts) {
if (!(this instanceof OrgChart)) {
return new OrgChart(elem, opts);
}
if (isPlainObject(elem) && !opts) {
this.opts = elem;
this.chartContainer = getElement(elem.chartContainer || elem.container);
} else {
this.chartContainer = getElement(elem);
this.opts = opts || {};
}
this.chart = null;
this.defaultOptions = {
'icons': {
'theme': 'oci',
'parentNode': 'oci-menu',
'expandToUp': 'oci-chevron-up',
'collapseToDown': 'oci-chevron-down',
'collapseToLeft': 'oci-chevron-left',
'expandToRight': 'oci-chevron-right',
'backToCompact': 'oci-corner-top-left',
'backToLoose': 'oci-corner-bottom-right',
'collapsed': 'oci-plus-square',
'expanded': 'oci-minus-square',
'spinner': 'oci-spinner'
},
'nodeTitle': 'name',
'nodeId': 'id',
'toggleSiblingsResp': false,
'visibleLevel': 999,
'chartClass': '',
'exportButton': false,
'exportButtonName': 'Export',
'exportFilename': 'OrgChart',
'exportFileextension': 'png',
'exportScale': 2,
'draggable': false,
'direction': 't2b',
'pan': false,
'minimap': false,
'zoom': false,
'zoominLimit': 7,
'zoomoutLimit': 0.5
};
if (this.chartContainer) {
this.init();
}
};
if (typeof module === 'object' && typeof module.exports === 'object') {
Object.defineProperty(OrgChart, '__testing__', {
value: {
getState: getState,
setState: setState
}
});
}
//
OrgChart.prototype = {
//
init: function (opts) {
this.options = mergeObjects({}, this.defaultOptions, this.opts, opts);
// build the org-chart
const chartContainerElement = this.chartContainer;
let chartElement;
let rootElement;
let rootNodesContainerElement;
let rootHierarchyElement;
this.detachMinimap();
if (this.chart) {
this.chart.remove();
}
const data = this.options.data;
chartElement = getDocument().createElement('div');
chartElement.className = 'orgchart' + (this.options.chartClass !== '' ? ' ' + this.options.chartClass : '') + (this.options.direction !== 't2b' ? ' ' + this.options.direction : '');
setState(chartElement, 'options', this.options);
this.chart = chartElement;
chartElement.addEventListener('click', function (event) {
const nativeEvent = event.originalEvent || event;
const clickTargetElement = getElement(nativeEvent.target);
if (!clickTargetElement || !(typeof clickTargetElement.closest === 'function' && clickTargetElement.closest('.node'))) {
Array.from(chartElement.querySelectorAll('.node.focused')).forEach(function (focusedNodeEl) {
focusedNodeEl.classList.remove('focused');
});
}
});
if (typeof getWindow().MutationObserver !== 'undefined') {
this.triggerInitEvent();
}
const isListElementData = isElement(data) && data.tagName === 'UL';
rootNodesContainerElement = getDocument().createElement('ul');
rootNodesContainerElement.className = 'nodes';
chartElement.appendChild(rootNodesContainerElement);
if (!isListElementData && Array.isArray(data)) {
rootElement = rootNodesContainerElement;
} else {
rootHierarchyElement = getDocument().createElement('li');
rootHierarchyElement.className = 'hierarchy';
rootNodesContainerElement.appendChild(rootHierarchyElement);
rootElement = rootHierarchyElement;
}
if (isListElementData) { // ul datasource
this.buildHierarchy(rootElement, this.buildJsonDS(data.firstElementChild));
} else { // local json datasource
if (data.relationship) {
this.buildHierarchy(rootElement, data);
} else {
this.buildHierarchy(rootElement, Array.isArray(data) ? data : this.attachRel(data, '00'));
}
}
if (chartContainerElement) {
chartContainerElement.appendChild(chartElement);
}
// append the export button
if (this.options.exportButton && !getDocument().querySelector('.oc-export-btn')) {
this.attachExportButton();
}
if (this.options.pan) {
this.bindPan();
}
if (this.options.zoom) {
this.bindZoom();
}
if (this.options.minimap) {
this.attachMinimap();
}
return this;
},
handleCompactNodes: function () {
const chartElement = this.chart;
if (!chartElement) {
return;
}
// caculate the compact nodes' level which is used to add different styles
Array.from(chartElement.querySelectorAll('.node.compact')).forEach(function (compactNodeEl) {
let compactAncestorCount = 0;
let currentAncestorElement = compactNodeEl.parentElement;
while (currentAncestorElement) {
if (currentAncestorElement.classList && currentAncestorElement.classList.contains('compact')) {
compactAncestorCount += 1;
}
currentAncestorElement = currentAncestorElement.parentElement;
}
compactNodeEl.classList.add(compactAncestorCount % 2 === 0 ? 'even' : 'odd');
}); // the following code snippets are kept only as a reminder for a future native-DOM approach
// that would add direction arrows for the top compact node once the styles are corrected.
},
//
triggerInitEvent: function () {
const orgChart = this;
const MutationObserverCtor = getWindow().MutationObserver;
const mo = new MutationObserverCtor(function (mutations) {
mo.disconnect();
initTime:
for (let i = 0; i < mutations.length; i++) {
for (let j = 0; j < mutations[i].addedNodes.length; j++) {
if (mutations[i].addedNodes[j].classList && mutations[i].addedNodes[j].classList.contains('orgchart')) {
orgChart.handleCompactNodes();
if (!orgChart.chart) {
break initTime;
}
if (orgChart.options.initCompleted && typeof orgChart.options.initCompleted === 'function') {
orgChart.options.initCompleted(orgChart.chart);
}
mutations[i].addedNodes[j].dispatchEvent(createTriggeredEvent('init.orgchart'));
break initTime;
}
}
}
});
mo.observe(this.chartContainer, { childList: true });
},
triggerShowEvent: function (targetEl, rel) {
if (targetEl && typeof targetEl.dispatchEvent === 'function') {
targetEl.dispatchEvent(createTriggeredEvent('show-' + rel + '.orgchart'));
}
},
triggerHideEvent: function (targetEl, rel) {
if (targetEl && typeof targetEl.dispatchEvent === 'function') {
targetEl.dispatchEvent(createTriggeredEvent('hide-' + rel + '.orgchart'));
}
},
// add export button for orgchart
attachExportButton: function () {
const orgChart = this;
const chartContainerElement = this.chartContainer;
let exportButtonElement;
if (!chartContainerElement || !chartContainerElement.parentElement) {
return;
}
exportButtonElement = getDocument().createElement('button');
exportButtonElement.className = 'oc-export-btn';
exportButtonElement.textContent = this.options.exportButtonName;
exportButtonElement.addEventListener('click', function (event) {
event.preventDefault();
orgChart.export();
});
chartContainerElement.insertAdjacentElement('afterend', exportButtonElement);
},
setOptions: function (opts, val) {
if (typeof opts === 'string') {
if (opts === 'pan') {
if (val) {
this.bindPan();
} else {
this.unbindPan();
}
}
if (opts === 'zoom') {
if (val) {
this.bindZoom();
} else {
this.unbindZoom();
}
}
if (opts === 'minimap') {
this.options = mergeObjects({}, this.options || {}, { minimap: val });
if (this.chart) {
setState(this.chart, 'options', this.options);
}
if (val) {
this.attachMinimap();
} else {
this.detachMinimap();
}
}
}
if (typeof opts === 'object') {
if (opts.data) {
this.init(opts);
} else {
if (typeof opts.pan !== 'undefined') {
if (opts.pan) {
this.bindPan();
} else {
this.unbindPan();
}
}
if (typeof opts.zoom !== 'undefined') {
if (opts.zoom) {
this.bindZoom();
} else {
this.unbindZoom();
}
}
if (typeof opts.minimap !== 'undefined') {
this.options = mergeObjects({}, this.options || {}, { minimap: opts.minimap });
if (this.chart) {
setState(this.chart, 'options', this.options);
}
if (opts.minimap) {
this.attachMinimap();
} else {
this.detachMinimap();
}
}
}
}
return this;
},
attachMinimap: function () {
const orgChart = this;
const chartContainerElement = this.chartContainer;
const chartElement = this.chart;
let minimapElement;
let minimapStageElement;
let minimapContentElement;
let minimapViewportElement;
let minimapWheelBoundHandler;
let minimapPointerDownBoundHandler;
let minimapMutationObserver;
if (!chartContainerElement || !chartElement) {
return;
}
this.detachMinimap();
if (!getWindow().getComputedStyle(chartContainerElement).position || getWindow().getComputedStyle(chartContainerElement).position === 'static') {
setState(chartContainerElement, 'minimapOriginalPosition', chartContainerElement.style.position || '');
setState(chartContainerElement, 'minimapManagedPosition', true);
chartContainerElement.style.position = 'relative';
}
minimapElement = getDocument().createElement('div');
minimapElement.className = 'orgchart-minimap';
minimapStageElement = getDocument().createElement('div');
minimapStageElement.className = 'orgchart-minimap-stage';
minimapContentElement = getDocument().createElement('div');
minimapContentElement.className = 'orgchart-minimap-content';
minimapViewportElement = getDocument().createElement('div');
minimapViewportElement.className = 'orgchart-minimap-viewport';
minimapStageElement.appendChild(minimapContentElement);
minimapStageElement.appendChild(minimapViewportElement);
minimapElement.appendChild(minimapStageElement);
chartContainerElement.appendChild(minimapElement);
minimapWheelBoundHandler = function (event) {
orgChart.minimapWheelHandler(event);
};
minimapPointerDownBoundHandler = function (event) {
orgChart.minimapStartHandler(event);
};
minimapElement.addEventListener('wheel', minimapWheelBoundHandler);
minimapViewportElement.addEventListener('mousedown', minimapPointerDownBoundHandler);
if (typeof getWindow().MutationObserver !== 'undefined') {
minimapMutationObserver = new (getWindow().MutationObserver)(function () {
orgChart.updateMinimap(true);
});
minimapMutationObserver.observe(chartElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
}
setState(chartElement, 'minimapElement', minimapElement);
setState(chartElement, 'minimapStageElement', minimapStageElement);
setState(chartElement, 'minimapContentElement', minimapContentElement);
setState(chartElement, 'minimapViewportElement', minimapViewportElement);
setState(chartElement, 'minimapWheelBoundHandler', minimapWheelBoundHandler);
setState(chartElement, 'minimapPointerDownBoundHandler', minimapPointerDownBoundHandler);
setState(chartElement, 'minimapMutationObserver', minimapMutationObserver || null);
this.updateMinimap(true);
},
detachMinimap: function () {
const chartContainerElement = this.chartContainer;
const chartElement = this.chart;
let minimapElement;
let minimapViewportElement;
let minimapWheelBoundHandler;
let minimapPointerDownBoundHandler;
let minimapMoveBoundHandler;
let minimapEndBoundHandler;
let minimapMutationObserver;
if (chartElement) {
minimapElement = getState(chartElement, 'minimapElement');
minimapViewportElement = getState(chartElement, 'minimapViewportElement');
minimapWheelBoundHandler = getState(chartElement, 'minimapWheelBoundHandler');
minimapPointerDownBoundHandler = getState(chartElement, 'minimapPointerDownBoundHandler');
minimapMoveBoundHandler = getState(chartElement, 'minimapMoveBoundHandler');
minimapEndBoundHandler = getState(chartElement, 'minimapEndBoundHandler');
minimapMutationObserver = getState(chartElement, 'minimapMutationObserver');
if (minimapElement && minimapWheelBoundHandler) {
minimapElement.removeEventListener('wheel', minimapWheelBoundHandler);
}
if (minimapViewportElement && minimapPointerDownBoundHandler) {
minimapViewportElement.removeEventListener('mousedown', minimapPointerDownBoundHandler);
}
if (minimapMoveBoundHandler) {
getDocument().removeEventListener('mousemove', minimapMoveBoundHandler);
}
if (minimapEndBoundHandler) {
getDocument().removeEventListener('mouseup', minimapEndBoundHandler);
}
if (minimapMutationObserver) {
minimapMutationObserver.disconnect();
}
if (minimapElement && minimapElement.parentElement) {
minimapElement.remove();
}
[
'minimapElement',
'minimapStageElement',
'minimapContentElement',
'minimapViewportElement',
'minimapWheelBoundHandler',
'minimapPointerDownBoundHandler',
'minimapMoveBoundHandler',
'minimapEndBoundHandler',
'minimapMutationObserver',
'minimapDragState',
'minimapScale',
'minimapOffsetX',
'minimapOffsetY',
'minimapContentWidth',
'minimapContentHeight'
].forEach(function (key) {
setState(chartElement, key, null);
});
}
if (chartContainerElement && getState(chartContainerElement, 'minimapManagedPosition')) {
chartContainerElement.style.position = getState(chartContainerElement, 'minimapOriginalPosition') || '';
setState(chartContainerElement, 'minimapManagedPosition', null);
setState(chartContainerElement, 'minimapOriginalPosition', null);
}
},
minimapWheelHandler: function (event) {
const chartContainerElement = this.chartContainer;
if (!this.chart || !chartContainerElement) {
return;
}
if (typeof event.preventDefault === 'function') {
event.preventDefault();
}
this.setChartScale(this.chart, 1 + (event.deltaY > 0 ? -0.2 : 0.2), {
x: chartContainerElement.getBoundingClientRect().left + (chartContainerElement.clientWidth / 2),
y: chartContainerElement.getBoundingClientRect().top + (chartContainerElement.clientHeight / 2)
});
},
minimapStartHandler: function (event) {
const chartElement = this.chart;
const minimapViewportElement = chartElement ? getState(chartElement, 'minimapViewportElement') : null;
const minimapMoveBoundHandler = this.minimapMoveHandler.bind(this);
const minimapEndBoundHandler = this.minimapEndHandler.bind(this);
const viewportRect = minimapViewportElement ? minimapViewportElement.getBoundingClientRect() : null;
if (!chartElement || !minimapViewportElement || !viewportRect) {
return;
}
if (typeof event.preventDefault === 'function') {
event.preventDefault();
}
setState(chartElement, 'minimapDragState', {
startClientX: event.clientX,
startClientY: event.clientY,
startLeft: viewportRect.left,
startTop: viewportRect.top,
viewportWidth: viewportRect.width,
viewportHeight: viewportRect.height
});
setState(chartElement, 'minimapMoveBoundHandler', minimapMoveBoundHandler);
setState(chartElement, 'minimapEndBoundHandler', minimapEndBoundHandler);
getDocument().addEventListener('mousemove', minimapMoveBoundHandler);
getDocument().addEventListener('mouseup', minimapEndBoundHandler);
},
minimapMoveHandler: function (event) {
const chartElement = this.chart;
const dragState = chartElement ? getState(chartElement, 'minimapDragState') : null;
const minimapStageElement = chartElement ? getState(chartElement, 'minimapStageElement') : null;
let stageRect;
if (!chartElement || !dragState || !minimapStageElement) {
return;
}
stageRect = minimapStageElement.getBoundingClientRect();
this.moveChartFromMinimap(
dragState.startLeft + (event.clientX - dragState.startClientX) - stageRect.left,
dragState.startTop + (event.clientY - dragState.startClientY) - stageRect.top,
dragState.viewportWidth,
dragState.viewportHeight
);
},
minimapEndHandler: function () {
const chartElement = this.chart;
const minimapMoveBoundHandler = chartElement ? getState(chartElement, 'minimapMoveBoundHandler') : null;
const minimapEndBoundHandler = chartElement ? getState(chartElement, 'minimapEndBoundHandler') : null;
if (minimapMoveBoundHandler) {
getDocument().removeEventListener('mousemove', minimapMoveBoundHandler);
setState(chartElement, 'minimapMoveBoundHandler', null);
}
if (minimapEndBoundHandler) {
getDocument().removeEventListener('mouseup', minimapEndBoundHandler);
setState(chartElement, 'minimapEndBoundHandler', null);
}
if (chartElement) {
setState(chartElement, 'minimapDragState', null);
}
},
moveChartFromMinimap: function (stageLeft, stageTop, viewportWidth, viewportHeight) {
const chartElement = this.chart;
const chartContainerElement = this.chartContainer;
const minimapStageElement = chartElement ? getState(chartElement, 'minimapStageElement') : null;
const minimapScale = chartElement ? getState(chartElement, 'minimapScale') : null;
const minimapOffsetX = chartElement ? getState(chartElement, 'minimapOffsetX') : 0;
const minimapOffsetY = chartElement ? getState(chartElement, 'minimapOffsetY') : 0;
const minimapContentWidth = chartElement ? getState(chartElement, 'minimapContentWidth') : 0;
const minimapContentHeight = chartElement ? getState(chartElement, 'minimapContentHeight') : 0;
const chartTransform = getChartTransform(chartElement);
const chartRect = chartElement ? chartElement.getBoundingClientRect() : null;
const containerRect = chartContainerElement ? chartContainerElement.getBoundingClientRect() : null;
let localViewportWidth;
let localViewportHeight;
let localLeft;
let localTop;
let baseLeft;
let baseTop;
if (!chartElement || !chartContainerElement || !minimapStageElement || !minimapScale || !chartRect || !containerRect) {
return;
}
localViewportWidth = viewportWidth / minimapScale;
localViewportHeight = viewportHeight / minimapScale;
localLeft = clampValue((stageLeft - minimapOffsetX) / minimapScale, 0, Math.max(minimapContentWidth - localViewportWidth, 0));
localTop = clampValue((stageTop - minimapOffsetY) / minimapScale, 0, Math.max(minimapContentHeight - localViewportHeight, 0));
baseLeft = chartRect.left - chartTransform.translateX;
baseTop = chartRect.top - chartTransform.translateY;
chartElement.style.transformOrigin = '0 0';
chartElement.style.transform = 'matrix(' + chartTransform.scale + ', 0, 0, ' + chartTransform.scale + ', '
+ (containerRect.left - baseLeft - (localLeft * chartTransform.scale)) + ', '
+ (containerRect.top - baseTop - (localTop * chartTransform.scale)) + ')';
this.updateMinimap();
},
updateMinimap: function (forceRefresh) {
const chartElement = this.chart;
const chartContainerElement = this.chartContainer;
const minimapStageElement = chartElement ? getState(chartElement, 'minimapStageElement') : null;
const minimapContentElement = chartElement ? getState(chartElement, 'minimapContentElement') : null;
const minimapViewportElement = chartElement ? getState(chartElement, 'minimapViewportElement') : null;
const chartTransform = getChartTransform(chartElement);
const chartRect = chartElement ? chartElement.getBoundingClientRect() : null;
const containerRect = chartContainerElement ? chartContainerElement.getBoundingClientRect() : null;
let contentWidth;
let contentHeight;
let stageWidth;
let stageHeight;
let minimapScale;
let minimapOffsetX;
let minimapOffsetY;
let viewportLeft;
let viewportTop;
let viewportWidth;
let viewportHeight;
if (!this.options || !this.options.minimap || !chartElement || !chartContainerElement || !minimapStageElement || !minimapContentElement || !minimapViewportElement || !chartRect || !containerRect) {
return;
}
contentWidth = chartTransform.scale !== 0 && chartRect.width
? chartRect.width / chartTransform.scale
: (chartElement.scrollWidth || chartElement.offsetWidth || 1);
contentHeight = chartTransform.scale !== 0 && chartRect.height
? chartRect.height / chartTransform.scale
: (chartElement.scrollHeight || chartElement.offsetHeight || 1);
stageWidth = minimapStageElement.clientWidth || 180;
stageHeight = minimapStageElement.clientHeight || 120;
minimapScale = Math.min(stageWidth / Math.max(contentWidth, 1), stageHeight / Math.max(contentHeight, 1));
minimapOffsetX = (stageWidth - (contentWidth * minimapScale)) / 2;
minimapOffsetY = (stageHeight - (contentHeight * minimapScale)) / 2;
minimapContentElement.style.width = contentWidth + 'px';
minimapContentElement.style.height = contentHeight + 'px';
minimapContentElement.style.transformOrigin = '0 0';
minimapContentElement.style.transform = 'translate(' + minimapOffsetX + 'px, ' + minimapOffsetY + 'px) scale(' + minimapScale + ')';
if (forceRefresh || !minimapContentElement.firstElementChild) {
renderMinimapNodes(chartElement, minimapContentElement, chartRect, chartTransform.scale || 1);
}
viewportLeft = clampValue((containerRect.left - chartRect.left) / chartTransform.scale, 0, Math.max(contentWidth, 0));
viewportTop = clampValue((containerRect.top - chartRect.top) / chartTransform.scale, 0, Math.max(contentHeight, 0));
viewportWidth = Math.min(contentWidth, containerRect.width / chartTransform.scale);
viewportHeight = Math.min(contentHeight, containerRect.height / chartTransform.scale);
minimapViewportElement.style.width = Math.max(viewportWidth * minimapScale, 12) + 'px';
minimapViewportElement.style.height = Math.max(viewportHeight * minimapScale, 12) + 'px';
minimapViewportElement.style.transform = 'translate(' + (minimapOffsetX + (viewportLeft * minimapScale)) + 'px, ' + (minimapOffsetY + (viewportTop * minimapScale)) + 'px)';
setState(chartElement, 'minimapScale', minimapScale);
setState(chartElement, 'minimapOffsetX', minimapOffsetX);
setState(chartElement, 'minimapOffsetY', minimapOffsetY);
setState(chartElement, 'minimapContentWidth', contentWidth);
setState(chartElement, 'minimapContentHeight', contentHeight);
},
//
panStartHandler: function (e) {
const orgChart = this;
const nativeEvent = e.originalEvent || e;
const chartElement = getElement(e.chart || nativeEvent.currentTarget);
const targetElement = getElement(nativeEvent.target);
let lastTf;
let existingPanMoveHandler;
let panMoveHandler;
if (!chartElement) {
return;
}
if ((targetElement && typeof targetElement.closest === 'function' && targetElement.closest('.node')) || (nativeEvent.touches && nativeEvent.touches.length > 1)) {
setState(chartElement, 'panning', false);
return;
} else {
chartElement.style.cursor = 'move';
setState(chartElement, 'panning', true);
}
let lastX = 0;
let lastY = 0;
lastTf = chartElement.style.transform || getWindow().getComputedStyle(chartElement).transform || 'none';
if (lastTf !== 'none') {
const temp = lastTf.split(',');
if (lastTf.indexOf('3d') === -1) {
lastX = parseInt(temp[4]);
lastY = parseInt(temp[5]);
} else {
lastX = parseInt(temp[12]);
lastY = parseInt(temp[13]);
}
}
let startX = 0;
let startY = 0;
if (!nativeEvent.targetTouches) { // pand on desktop
startX = (typeof nativeEvent.pageX === 'number' ? nativeEvent.pageX : nativeEvent.clientX) - lastX;
startY = (typeof nativeEvent.pageY === 'number' ? nativeEvent.pageY : nativeEvent.clientY) - lastY;
} else if (nativeEvent.targetTouches.length === 1) { // pan on mobile device
startX = nativeEvent.targetTouches[0].pageX - lastX;
startY = nativeEvent.targetTouches[0].pageY - lastY;
} else if (nativeEvent.targetTouches.length > 1) {
return;
}
existingPanMoveHandler = getState(chartElement, 'panMoveHandler');
if (existingPanMoveHandler) {
chartElement.removeEventListener('mousemove', existingPanMoveHandler);
chartElement.removeEventListener('touchmove', existingPanMoveHandler);
}
panMoveHandler = function (e) {
let currentTransform;
let matrix;
if (!getState(chartElement, 'panning')) {
return;
}
let newX = 0;
let newY = 0;
if (!e.targetTouches) { // pand on desktop
newX = (typeof e.pageX === 'number' ? e.pageX : e.clientX) - startX;
newY = (typeof e.pageY === 'number' ? e.pageY : e.clientY) - startY;
} else if (e.targetTouches.length === 1) { // pan on mobile device
newX = e.targetTouches[0].pageX - startX;
newY = e.targetTouches[0].pageY - startY;
} else if (e.targetTouches.length > 1) {
return;
}
currentTransform = chartElement.style.transform || getWindow().getComputedStyle(chartElement).transform || 'none';
if (currentTransform === 'none') {
if (currentTransform.indexOf('3d') === -1) {
chartElement.style.transform = 'matrix(1, 0, 0, 1, ' + newX + ', ' + newY + ')';
} else {
chartElement.style.transform = 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ' + newX + ', ' + newY + ', 0, 1)';
}
} else {
matrix = currentTransform.split(',');
if (currentTransform.indexOf('3d') === -1) {
matrix[4] = ' ' + newX;
matrix[5] = ' ' + newY + ')';
} else {
matrix[12] = ' ' + newX;
matrix[13] = ' ' + newY;
}
chartElement.style.transform = matrix.join(',');
orgChart.updateMinimap();
}
};
setState(chartElement, 'panMoveHandler', panMoveHandler);
chartElement.addEventListener('mousemove', panMoveHandler);
chartElement.addEventListener('touchmove', panMoveHandler);
},
//
panEndHandler: function (e) {
const nativeEvent = e.originalEvent || e;
const chartElement = getElement(e.chart || nativeEvent.chart || nativeEvent.currentTarget);
let panMoveHandler;
if (!chartElement) {
return;
}
if (getState(chartElement, 'panning')) {
panMoveHandler = getState(chartElement, 'panMoveHandler');
setState(chartElement, 'panning', false);
chartElement.style.cursor = 'default';
if (panMoveHandler) {
chartElement.removeEventListener('mousemove', panMoveHandler);
chartElement.removeEventListener('touchmove', panMoveHandler);
setState(chartElement, 'panMoveHandler', null);
}
}
},
//
bindPan: function () {
const orgChart = this;
const chartContainerElement = this.chartContainer;
const chartElement = this.chart;
let panStartBoundHandler;
let panEndBoundHandler;
if (!chartContainerElement || !chartElement) {
return;
}
this.unbindPan();
chartContainerElement.style.overflow = 'hidden';
panStartBoundHandler = function (event) {
orgChart.panStartHandler({
chart: chartElement,
originalEvent: event
});
};
panEndBoundHandler = function () {
orgChart.panEndHandler({
chart: chartElement
});
};
setState(chartElement, 'panStartBoundHandler', panStartBoundHandler);
setState(chartElement, 'panEndBoundHandler', panEndBoundHandler);
chartElement.addEventListener('mousedown', panStartBoundHandler);
chartElement.addEventListener('touchstart', panStartBoundHandler);
getDocument().addEventListener('mouseup', panEndBoundHandler);
getDocument().addEventListener('touchend', panEndBoundHandler);
},
//
unbindPan: function () {
const chartContainerElement = this.chartContainer;
const chartElement = this.chart;
let panStartBoundHandler;
let panEndBoundHandler;
if (!chartContainerElement || !chartElement) {
return;
}
chartContainerElement.style.overflow = 'auto';
panStartBoundHandler = getState(chartElement, 'panStartBoundHandler');
panEndBoundHandler = getState(chartElement, 'panEndBoundHandler');
if (panStartBoundHandler) {
chartElement.removeEventListener('mousedown', panStartBoundHandler);
chartElement.removeEventListener('touchstart', panStartBoundHandler);
setState(chartElement, 'panStartBoundHandler', null);
}
if (panEndBoundHandler) {
getDocument().removeEventListener('mouseup', panEndBoundHandler);
getDocument().removeEventListener('touchend', panEndBoundHandler);
setState(chartElement, 'panEndBoundHandler', null);
}
},
//
zoomWheelHandler: function (e) {
const nativeEvent = e.originalEvent || e;
const orgChart = e.orgChart;
const chartContainerElement = orgChart ? orgChart.chartContainer : null;
const chartContainerRect = chartContainerElement && typeof chartContainerElement.getBoundingClientRect === 'function'
? chartContainerElement.getBoundingClientRect()
: null;
const hasPointerPosition = nativeEvent && typeof nativeEvent.clientX === 'number' && typeof nativeEvent.clientY === 'number';
let zoomAnchor = null;
if (!orgChart || !orgChart.chart || !chartContainerElement) {
return;
}
if (chartContainerRect && hasPointerPosition) {
if (nativeEvent.clientX < chartContainerRect.left || nativeEvent.clientX > chartContainerRect.right
|| nativeEvent.clientY < chartContainerRect.top || nativeEvent.clientY > chartContainerRect.bottom) {
return;
}
zoomAnchor = {
x: nativeEvent.clientX,
y: nativeEvent.clientY
};
}
if (nativeEvent && typeof nativeEvent.preventDefault === 'function') {
nativeEvent.preventDefault();
}
const newScale = 1 + (nativeEvent.deltaY > 0 ? -0.2 : 0.2);
orgChart.setChartScale(orgChart.chart, newScale, zoomAnchor);
},
//
zoomStartHandler: function (e) {
let orgChart;
let chartElement;
let dist;
let pinchCenter;
const nativeEvent = e.originalEvent || e;
if(nativeEvent.touches && nativeEvent.touches.length === 2) {
orgChart = e.orgChart;
chartElement = orgChart.chart;
if (!chartElement) {
return;
}
setState(chartElement, 'pinching', true);
dist = orgChart.getPinchDist(nativeEvent);
pinchCenter = orgChart.getPinchCenter(nativeEvent);
setState(chartElement, 'pinchDistStart', dist);
setState(chartElement, 'pinchCenterStart', pinchCenter);
setState(chartElement, 'pinchCenterEnd', pinchCenter);
}
},
zoomingHandler: function (e) {
const nativeEvent = e.originalEvent || e;
const orgChart = e.orgChart;
const chartElement = orgChart.chart;
let dist;
let pinchCenter;
if(chartElement && getState(chartElement, 'pinching')) {
dist = orgChart.getPinchDist(nativeEvent);
pinchCenter = orgChart.getPinchCenter(nativeEvent);
setState(chartElement, 'pinchDistEnd', dist);
setState(chartElement, 'pinchCenterEnd', pinchCenter);
}
},
zoomEndHandler: function (e) {
const orgChart = e.orgChart;
const chartElement = orgChart.chart;
let diff;
let pinchCenter;
if(chartElement && getState(chartElement, 'pinching')) {
setState(chartElement, 'pinching', false);
diff = getState(chartElement, 'pinchDistEnd') - getState(chartElement, 'pinchDistStart');
pinchCenter = getState(chartElement, 'pinchCenterEnd') || getState(chartElement, 'pinchCenterStart') || null;
if (diff > 0) {
orgChart.setChartScale(orgChart.chart, 1.2, pinchCenter);
} else if (diff < 0) {
orgChart.setChartScale(orgChart.chart, 0.8, pinchCenter);
}
}
},
//
bindZoom: function () {
const orgChart = this;
const chartContainerElement = this.chartContainer;
const chartElement = this.chart;
let zoomWheelBoundHandler;
let zoomStartBoundHandler;
let zoomingBoundHandler;
let zoomEndBoundHandler;
if (!chartContainerElement || !chartElement) {
return;
}
this.unbindZoom();
zoomWheelBoundHandler = function (event) {
orgChart.zoomWheelHandler({
orgChart: orgChart,
originalEvent: event
});
};
zoomStartBoundHandler = function (event) {
orgChart.zoomStartHandler({
orgChart: orgChart,
originalEvent: event
});
};
zoomingBoundHandler = function (event) {
orgChart.zoomingHandler({
orgChart: orgChart,
originalEvent: event
});
};
zoomEndBoundHandler = function () {
orgChart.zoomEndHandler({
orgChart: orgChart
});
};
setState(chartElement, 'zoomWheelBoundHandler', zoomWheelBoundHandler);
setState(chartElement, 'zoomStartBoundHandler', zoomStartBoundHandler);
setState(chartElement, 'zoomingBoundHandler', zoomingBoundHandler);
setState(chartElement, 'zoomEndBoundHandler', zoomEndBoundHandler);
chartContainerElement.addEventListener('wheel', zoomWheelBoundHandler);
chartContainerElement.addEventListener('touchstart', zoomStartBoundHandler);
getDocument().addEventListener('touchmove', zoomingBoundHandler);
getDocument().addEventListener('touchend', zoomEndBoundHandler);
},
unbindZoom: function () {
const chartContainerElement = this.chartContainer;
const chartElement = this.chart;
let zoomWheelBoundHandler;
let zoomStartBoundHandler;
let zoomingBoundHandler;
let zoomEndBoundHandler;
if (!chartContainerElement || !chartElement) {
return;
}
zoomWheelBoundHandler = getState(chartElement, 'zoomWheelBoundHandler');
zoomStartBoundHandler = getState(chartElement, 'zoomStartBoundHandler');
zoomingBoundHandler = getState(chartElement, 'zoomingBoundHandler');
zoomEndBoundHandler = getState(chartElement, 'zoomEndBoundHandler');
if (zoomWheelBoundHandler) {
chartContainerElement.removeEventListener('wheel', zoomWheelBoundHandler);
setState(chartElement, 'zoomWheelBoundHandler', null);
}
if (zoomStartBoundHandler) {
chartContainerElement.removeEventListener('touchstart', zoomStartBoundHandler);