UNPKG

orgchart

Version:

Simple and direct organization chart(tree-like hierarchy) library based on pure DOM.

1,311 lines (1,194 loc) 169 kB
/* * 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);