diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 17:14:58 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 17:14:58 +0700 |
| commit | 1ca3b3df3421961caec3b747a364071c80f5c7da (patch) | |
| tree | 6778a1f0f3f9b4c6e26d6d87ccde16e24da6c9d6 /hr_organizational_chart/static/js | |
| parent | b57188be371d36d96caac4b8d65a40745c0e972c (diff) | |
initial commit
Diffstat (limited to 'hr_organizational_chart/static/js')
| -rw-r--r-- | hr_organizational_chart/static/js/hr_org_chart.js | 89 | ||||
| -rw-r--r-- | hr_organizational_chart/static/js/jquery_hr_orgchart.js | 1047 |
2 files changed, 1136 insertions, 0 deletions
diff --git a/hr_organizational_chart/static/js/hr_org_chart.js b/hr_organizational_chart/static/js/hr_org_chart.js new file mode 100644 index 0000000..58bc15a --- /dev/null +++ b/hr_organizational_chart/static/js/hr_org_chart.js @@ -0,0 +1,89 @@ +var employee_data = []; + +var nodeTemplate = function(data) { + return ` + <span class="office">${data.office}</span> + <div class="title">${data.name}</div> + <div class="content">${data.title}</div> + `; + }; + +odoo.define("hr_org_chart_employee.hr_org_chart", function (require) { + "use strict"; + + var core = require('web.core'); + var session = require('web.session'); + var ajax = require('web.ajax'); + var Widget = require('web.Widget'); + var QWeb = core.qweb; + var _t = core._t; + var AbstractAction = require('web.AbstractAction'); + var _lt = core._lt; + + var OrgChartDepartment = AbstractAction.extend({ + events: { + 'click .nodes,.node': 'view_employee', + }, + init: function(parent, context){ + this._super(parent, context); + var self = this; + if (context.tag == 'employee_organization_chart') { + this._rpc({ + route: '/get/employees', + }).then(function (result) { + self._rpc({ + model: 'hr.organizational.chart', + method: 'get_employee_data', + args: [result], + }, []).then(function(values){ + employee_data = values; + self.render(); + self.href = window.location.href; + }); + }); + } + }, + willStart: function() { + return $.when(ajax.loadLibs(this), this._super()); + }, + start: function() { + var self = this; + return this._super(); + }, + render: function() { + var super_render = this._super; + var self = this; + var org_chart = QWeb.render('hr_organizational_chart.org_chart_template', { + widget: self, + }); + $(".o_control_panel").addClass("o_hidden"); + $(org_chart).prependTo(self.$el); + return org_chart; + }, + reload: function () { + window.location.href = this.href; + }, + view_employee: function(ev){ + if (ev.target.attributes[1]){ + var id = parseInt(ev.target.attributes[1].nodeValue) + this.do_action({ + name: _t("Employee"), + type: 'ir.actions.act_window', + res_model: 'hr.employee', + res_id: id, + view_mode: 'form', + views: [[false, 'form']], + }) + } + }, + }); + + + + core.action_registry.add('employee_organization_chart', OrgChartDepartment); + window.reload() + + return OrgChartDepartment; + + +});
\ No newline at end of file diff --git a/hr_organizational_chart/static/js/jquery_hr_orgchart.js b/hr_organizational_chart/static/js/jquery_hr_orgchart.js new file mode 100644 index 0000000..f074951 --- /dev/null +++ b/hr_organizational_chart/static/js/jquery_hr_orgchart.js @@ -0,0 +1,1047 @@ +/* + * jQuery OrgChart Plugin + * 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') { + factory(require('jquery'), window, document); + } else { + factory(jQuery, window, document); + } +}(function ($, window, document, undefined) { + var OrgChart = function (elem, opts) { + this.$chartContainer = $(elem); + this.opts = opts; + this.defaultOptions = { + 'nodeTitle': 'name', + 'nodeId': 'id', + 'toggleSiblingsResp': false, + 'visibleLevel': 999, + 'chartClass': '', + 'exportButton': false, + 'exportFilename': 'OrgChart', + 'exportFileextension': 'png', + 'draggable': false, + 'direction': 't2b', + 'pan': false, + 'zoom': false, + 'zoominLimit': 7, + 'zoomoutLimit': 0.5 + }; + }; + // + OrgChart.prototype = { + // + init: function (opts) { + var that = this; + this.options = $.extend({}, this.defaultOptions, this.opts, opts); + // build the org-chart + var $chartContainer = this.$chartContainer; + if (this.$chart) { + this.$chart.remove(); + } + var data = this.options.data; + var $chart = this.$chart = $('<div>', { + 'data': { 'options': this.options }, + 'class': 'orgchart' + (this.options.chartClass !== '' ? ' ' + this.options.chartClass : '') + (this.options.direction !== 't2b' ? ' ' + this.options.direction : ''), + }); + if (typeof MutationObserver !== 'undefined') { + this.triggerInitEvent(); + } + if ($.type(data) === 'object') { + if (data instanceof $) { // ul datasource + this.buildHierarchy($chart, this.buildJsonDS(data.children()), 0, this.options); + } else { // local json datasource + this.buildHierarchy($chart, this.options.ajaxURL ? data : this.attachRel(data, '00')); + } + } else { + $chart.append('<i class="fa fa-circle-o-notch fa-spin spinner"></i>'); + $.ajax({ + 'url': data, + 'dataType': 'json' + }) + .done(function(data, textStatus, jqXHR) { + that.buildHierarchy($chart, that.options.ajaxURL ? data : that.attachRel(data, '00'), 0, that.options); + }) + .fail(function(jqXHR, textStatus, errorThrown) { + console.log(errorThrown); + }) + .always(function() { + $chart.children('.spinner').remove(); + }); + } + $chartContainer.append($chart); + + if (this.options.pan) { + this.bindPan(); + } + + if (this.options.zoom) { + this.bindZoom(); + } + + return this; + }, + // + triggerInitEvent: function () { + var that = this; + var mo = new MutationObserver(function (mutations) { + mo.disconnect(); + initTime: + for (var i = 0; i < mutations.length; i++) { + for (var j = 0; j < mutations[i].addedNodes.length; j++) { + if (mutations[i].addedNodes[j].classList.contains('orgchart')) { + if (that.options.initCompleted && typeof that.options.initCompleted === 'function') { + that.options.initCompleted(that.$chart); + var initEvent = $.Event('init.orgchart'); + that.$chart.trigger(initEvent); + break initTime; + } + } + } + } + }); + mo.observe(this.$chartContainer[0], { childList: true }); + }, + // + panStartHandler: function (e) { + var $chart = $(e.delegateTarget); + if ($(e.target).closest('.node').length || (e.touches && e.touches.length > 1)) { + $chart.data('panning', false); + return; + } else { + $chart.css('cursor', 'move').data('panning', true); + } + var lastX = 0; + var lastY = 0; + var lastTf = $chart.css('transform'); + if (lastTf !== 'none') { + var 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]); + } + } + var startX = 0; + var startY = 0; + if (!e.targetTouches) { // pand on desktop + startX = e.pageX - lastX; + startY = e.pageY - lastY; + } else if (e.targetTouches.length === 1) { // pan on mobile device + startX = e.targetTouches[0].pageX - lastX; + startY = e.targetTouches[0].pageY - lastY; + } else if (e.targetTouches.length > 1) { + return; + } + $chart.on('mousemove touchmove',function(e) { + if (!$chart.data('panning')) { + return; + } + var newX = 0; + var newY = 0; + if (!e.targetTouches) { // pand on desktop + newX = e.pageX - startX; + newY = e.pageY - 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; + } + var lastTf = $chart.css('transform'); + if (lastTf === 'none') { + if (lastTf.indexOf('3d') === -1) { + $chart.css('transform', 'matrix(1, 0, 0, 1, ' + newX + ', ' + newY + ')'); + } else { + $chart.css('transform', 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ' + newX + ', ' + newY + ', 0, 1)'); + } + } else { + var matrix = lastTf.split(','); + if (lastTf.indexOf('3d') === -1) { + matrix[4] = ' ' + newX; + matrix[5] = ' ' + newY + ')'; + } else { + matrix[12] = ' ' + newX; + matrix[13] = ' ' + newY; + } + $chart.css('transform', matrix.join(',')); + } + }); + }, + // + panEndHandler: function (e) { + if (e.data.chart.data('panning')) { + e.data.chart.data('panning', false).css('cursor', 'default').off('mousemove'); + } + }, + // + bindPan: function () { + this.$chartContainer.css('overflow', 'hidden'); + this.$chart.on('mousedown touchstart', this.panStartHandler); + $(document).on('mouseup touchend', { 'chart': this.$chart }, this.panEndHandler); + }, + // + unbindPan: function () { + this.$chartContainer.css('overflow', 'auto'); + this.$chart.off('mousedown touchstart', this.panStartHandler); + $(document).off('mouseup touchend', this.panEndHandler); + }, + // + zoomWheelHandler: function (e) { + var oc = e.data.oc; + e.preventDefault(); + var newScale = 1 + (e.originalEvent.deltaY > 0 ? -0.2 : 0.2); + oc.setChartScale(oc.$chart, newScale); + }, + // + zoomStartHandler: function (e) { + if(e.touches && e.touches.length === 2) { + var oc = e.data.oc; + oc.$chart.data('pinching', true); + var dist = oc.getPinchDist(e); + oc.$chart.data('pinchDistStart', dist); + } + }, + zoomingHandler: function (e) { + var oc = e.data.oc; + if(oc.$chart.data('pinching')) { + var dist = oc.getPinchDist(e); + oc.$chart.data('pinchDistEnd', dist); + } + }, + zoomEndHandler: function (e) { + var oc = e.data.oc; + if(oc.$chart.data('pinching')) { + oc.$chart.data('pinching', false); + var diff = oc.$chart.data('pinchDistEnd') - oc.$chart.data('pinchDistStart'); + if (diff > 0) { + oc.setChartScale(oc.$chart, 1.2); + } else if (diff < 0) { + oc.setChartScale(oc.$chart, 0.8); + } + } + }, + // + bindZoom: function () { + this.$chartContainer.on('wheel', { 'oc': this }, this.zoomWheelHandler); + this.$chartContainer.on('touchstart', { 'oc': this }, this.zoomStartHandler); + $(document).on('touchmove', { 'oc': this }, this.zoomingHandler); + $(document).on('touchend', { 'oc': this }, this.zoomEndHandler); + }, + unbindZoom: function () { + this.$chartContainer.off('wheel', this.zoomWheelHandler); + this.$chartContainer.off('touchstart', this.zoomStartHandler); + $(document).off('touchmove', this.zoomingHandler); + $(document).off('touchend', this.zoomEndHandler); + }, + // + getPinchDist: function (e) { + return Math.sqrt((e.touches[0].clientX - e.touches[1].clientX) * (e.touches[0].clientX - e.touches[1].clientX) + + (e.touches[0].clientY - e.touches[1].clientY) * (e.touches[0].clientY - e.touches[1].clientY)); + }, + // + setChartScale: function ($chart, newScale) { + var opts = $chart.data('options'); + var lastTf = $chart.css('transform'); + var matrix = ''; + var targetScale = 1; + if (lastTf === 'none') { + $chart.css('transform', 'scale(' + newScale + ',' + newScale + ')'); + } else { + matrix = lastTf.split(','); + if (lastTf.indexOf('3d') === -1) { + targetScale = Math.abs(window.parseFloat(matrix[3]) * newScale); + if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) { + $chart.css('transform', lastTf + ' scale(' + newScale + ',' + newScale + ')'); + } + } else { + targetScale = Math.abs(window.parseFloat(matrix[1]) * newScale); + if (targetScale > opts.zoomoutLimit && targetScale < opts.zoominLimit) { + $chart.css('transform', lastTf + ' scale3d(' + newScale + ',' + newScale + ', 1)'); + } + } + } + }, + // + buildJsonDS: function ($li) { + var that = this; + var subObj = { + 'name': $li.contents().eq(0).text().trim(), + 'relationship': ($li.parent().parent().is('li') ? '1': '0') + ($li.siblings('li').length ? 1: 0) + ($li.children('ul').length ? 1 : 0) + }; + $.each($li.data(), function(key, value) { + subObj[key] = value; + }); + $li.children('ul').children().each(function() { + if (!subObj.children) { subObj.children = []; } + subObj.children.push(that.buildJsonDS($(this))); + }); + return subObj; + }, + // + attachRel: function (data, flags) { + var that = this; + data.relationship = flags + (data.children && data.children.length > 0 ? 1 : 0); + if (data.children) { + data.children.forEach(function(item) { + that.attachRel(item, '1' + (data.children.length > 1 ? 1 : 0)); + }); + } + return data; + }, + // + loopChart: function ($chart) { + var that = this; + var $tr = $chart.find('tr:first'); + var subObj = { 'id': $tr.find('.node')[0].id }; + $tr.siblings(':last').children().each(function() { + if (!subObj.children) { subObj.children = []; } + subObj.children.push(that.loopChart($(this))); + }); + return subObj; + }, + // + getHierarchy: function () { + if (typeof this.$chart === 'undefined') { + return 'Error: orgchart does not exist' + } else { + if (!this.$chart.find('.node').length) { + return 'Error: nodes do not exist' + } else { + var valid = true; + this.$chart.find('.node').each(function () { + if (!this.id) { + valid = false; + return false; + } + }); + if (!valid) { + return 'Error: All nodes of orghcart to be exported must have data-id attribute!'; + } + } + } + return this.loopChart(this.$chart); + }, + // detect the exist/display state of related node + getNodeState: function ($node, relation) { + var $target = {}; + var relation = relation || 'self'; + if (relation === 'parent') { + $target = $node.closest('.nodes').siblings(':first'); + if ($target.length) { + if ($target.is('.hidden') || (!$target.is('.hidden') && $target.closest('.nodes').is('.hidden'))) { + return { 'exist': true, 'visible': false }; + } + return { 'exist': true, 'visible': true }; + } + } else if (relation === 'children') { + $target = $node.closest('tr').siblings(':last'); + if ($target.length) { + if (!$target.is('.hidden')) { + return { 'exist': true, 'visible': true }; + } + return { 'exist': true, 'visible': false }; + } + } else if (relation === 'siblings') { + $target = $node.closest('table').parent().siblings(); + if ($target.length) { + if (!$target.is('.hidden') && !$target.parent().is('.hidden')) { + return { 'exist': true, 'visible': true }; + } + return { 'exist': true, 'visible': false }; + } + } else { + $target = $node; + if ($target.length) { + if (!(($target.closest('.nodes').length && $target.closest('.nodes').is('.hidden')) || + ($target.closest('table').parent().length && $target.closest('table').parent().is('.hidden')) || + ($target.parent().is('li') && ($target.closest('ul').is('.hidden') || $target.closest('verticalNodes').is('.hidden'))) + )) { + return { 'exist': true, 'visible': true }; + } + return { 'exist': true, 'visible': false }; + } + } + return { 'exist': false, 'visible': false }; + }, + // find the related nodes + getRelatedNodes: function ($node, relation) { + if (!$node || !($node instanceof $) || !$node.is('.node')) { + return $(); + } + if (relation === 'parent') { + return $node.closest('.nodes').parent().children(':first').find('.node'); + } else if (relation === 'children') { + return $node.closest('tr').siblings('.nodes').children().find('.node:first'); + } else if (relation === 'siblings') { + return $node.closest('table').parent().siblings().find('.node:first'); + } else { + return $(); + } + }, + // show the parent node of the specified node + showParent: function ($node) { + // just show only one superior level + var $upperLevel = $node.closest('.nodes').siblings().removeClass('hidden'); + // just show only one line + $upperLevel.eq(2).children().slice(1, -1).addClass('hidden'); + // show parent node with animation + var $parent = $upperLevel.eq(0).find('.node'); + this.repaint($parent[0]); + $parent.addClass('sliding').removeClass('slide-down').one('transitionend', { 'node': $node }, this.showParentEnd.bind(this)); + }, + stopAjax: function ($nodeLevel) { + if ($nodeLevel.find('.spinner').length) { + $nodeLevel.closest('.orgchart').data('inAjax', false); + } + }, + isVisibleNode: function (index, elem) { + return this.getNodeState($(elem)).visible; + }, + // + // start up loading status for requesting new nodes + startLoading: function ($edge) { + var $chart = this.$chart; + if (typeof $chart.data('inAjax') !== 'undefined' && $chart.data('inAjax') === true) { + return false; + } + + $edge.addClass('hidden'); + $edge.parent().append('<i class="fa fa-circle-o-notch fa-spin spinner"></i>') + .children().not('.spinner').css('opacity', 0.2); + $chart.data('inAjax', true); + $('.oc-export-btn' + (this.options.chartClass !== '' ? '.' + this.options.chartClass : '')).prop('disabled', true); + return true; + }, + // terminate loading status for requesting new nodes + endLoading: function ($edge) { + var $node = $edge.parent(); + $edge.removeClass('hidden'); + $node.find('.spinner').remove(); + $node.children().removeAttr('style'); + this.$chart.data('inAjax', false); + $('.oc-export-btn' + (this.options.chartClass !== '' ? '.' + this.options.chartClass : '')).prop('disabled', false); + }, + // whether the cursor is hovering over the node + isInAction: function ($node) { + return $node.children('.edge').attr('class').indexOf('fa-') > -1 ? true : false; + }, + // + switchVerticalArrow: function ($arrow) { + $arrow.toggleClass('fa-chevron-up').toggleClass('fa-chevron-down'); + }, + // + switchHorizontalArrow: function ($node) { + var opts = this.options; + if (opts.toggleSiblingsResp && (typeof opts.ajaxURL === 'undefined' || $node.closest('.nodes').data('siblingsLoaded'))) { + var $prevSib = $node.closest('table').parent().prev(); + if ($prevSib.length) { + if ($prevSib.is('.hidden')) { + $node.children('.leftEdge').addClass('fa-chevron-left').removeClass('fa-chevron-right'); + } else { + $node.children('.leftEdge').addClass('fa-chevron-right').removeClass('fa-chevron-left'); + } + } + var $nextSib = $node.closest('table').parent().next(); + if ($nextSib.length) { + if ($nextSib.is('.hidden')) { + $node.children('.rightEdge').addClass('fa-chevron-right').removeClass('fa-chevron-left'); + } else { + $node.children('.rightEdge').addClass('fa-chevron-left').removeClass('fa-chevron-right'); + } + } + } else { + var $sibs = $node.closest('table').parent().siblings(); + var sibsVisible = $sibs.length ? !$sibs.is('.hidden') : false; + $node.children('.leftEdge').toggleClass('fa-chevron-right', sibsVisible).toggleClass('fa-chevron-left', !sibsVisible); + $node.children('.rightEdge').toggleClass('fa-chevron-left', sibsVisible).toggleClass('fa-chevron-right', !sibsVisible); + } + }, + // + repaint: function (node) { + if (node) { + node.style.offsetWidth = node.offsetWidth; + } + }, + // load new nodes by ajax + loadNodes: function (rel, url, $edge) { + var that = this; + var opts = this.options; + $.ajax({ 'url': url, 'dataType': 'json' }) + .done(function (data) { + if (that.$chart.data('inAjax')) { + if (rel === 'parent') { + if (!$.isEmptyObject(data)) { + that.addParent($edge.parent(), data); + } + } else if (rel === 'children') { + if (data.children.length) { + that.addChildren($edge.parent(), data[rel]); + } + } else { + that.addSiblings($edge.parent(), data.siblings ? data.siblings : data); + } + } + }) + .fail(function () { + console.log('Failed to get ' + rel + ' data'); + }) + .always(function () { + that.endLoading($edge); + }); + }, + // + HideFirstParentEnd: function (event) { + var $topEdge = event.data.topEdge; + var $node = $topEdge.parent(); + if (this.isInAction($node)) { + this.switchVerticalArrow($topEdge); + this.switchHorizontalArrow($node); + } + }, + // + expandVNodesEnd: function (event) { + event.data.vNodes.removeClass('sliding'); + }, + // + collapseVNodesEnd: function (event) { + event.data.vNodes.removeClass('sliding').closest('ul').addClass('hidden'); + }, + // + createGhostNode: function (event) { + var $nodeDiv = $(event.target); + var opts = this.options; + var origEvent = event.originalEvent; + var isFirefox = /firefox/.test(window.navigator.userAgent.toLowerCase()); + var ghostNode, nodeCover; + if (!document.querySelector('.ghost-node')) { + ghostNode = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + ghostNode.classList.add('ghost-node'); + nodeCover = document.createElementNS('http://www.w3.org/2000/svg','rect'); + ghostNode.appendChild(nodeCover); + $nodeDiv.closest('.orgchart').append(ghostNode); + } else { + ghostNode = $nodeDiv.closest('.orgchart').children('.ghost-node').get(0); + nodeCover = $(ghostNode).children().get(0); + } + var transValues = $nodeDiv.closest('.orgchart').css('transform').split(','); + var isHorizontal = opts.direction === 't2b' || opts.direction === 'b2t'; + var scale = Math.abs(window.parseFloat(isHorizontal ? transValues[0].slice(transValues[0].indexOf('(') + 1) : transValues[1])); + ghostNode.setAttribute('width', isHorizontal ? $nodeDiv.outerWidth(false) : $nodeDiv.outerHeight(false)); + ghostNode.setAttribute('height', isHorizontal ? $nodeDiv.outerHeight(false) : $nodeDiv.outerWidth(false)); + nodeCover.setAttribute('x',5 * scale); + nodeCover.setAttribute('y',5 * scale); + nodeCover.setAttribute('width', 120 * scale); + nodeCover.setAttribute('height', 40 * scale); + nodeCover.setAttribute('rx', 4 * scale); + nodeCover.setAttribute('ry', 4 * scale); + nodeCover.setAttribute('stroke-width', 1 * scale); + var xOffset = origEvent.offsetX * scale; + var yOffset = origEvent.offsetY * scale; + if (opts.direction === 'l2r') { + xOffset = origEvent.offsetY * scale; + yOffset = origEvent.offsetX * scale; + } else if (opts.direction === 'r2l') { + xOffset = $nodeDiv.outerWidth(false) - origEvent.offsetY * scale; + yOffset = origEvent.offsetX * scale; + } else if (opts.direction === 'b2t') { + xOffset = $nodeDiv.outerWidth(false) - origEvent.offsetX * scale; + yOffset = $nodeDiv.outerHeight(false) - origEvent.offsetY * scale; + } + if (isFirefox) { // hack for old version of Firefox(< 48.0) + nodeCover.setAttribute('fill', 'rgb(255, 255, 255)'); + nodeCover.setAttribute('stroke', 'rgb(191, 0, 0)'); + var ghostNodeWrapper = document.createElement('img'); + ghostNodeWrapper.src = 'data:image/svg+xml;utf8,' + (new XMLSerializer()).serializeToString(ghostNode); + origEvent.dataTransfer.setDragImage(ghostNodeWrapper, xOffset, yOffset); + } else { + origEvent.dataTransfer.setDragImage(ghostNode, xOffset, yOffset); + } + }, + // + filterAllowedDropNodes: function ($dragged) { + var opts = this.options; + var $dragZone = $dragged.closest('.nodes').siblings().eq(0).find('.node:first'); + var $dragHier = $dragged.closest('table').find('.node'); + this.$chart.data('dragged', $dragged) + .find('.node').each(function (index, node) { + if ($dragHier.index(node) === -1) { + if (opts.dropCriteria) { + if (opts.dropCriteria($dragged, $dragZone, $(node))) { + $(node).addClass('allowedDrop'); + } + } else { + $(node).addClass('allowedDrop'); + } + } + }); + }, + // + dragstartHandler: function (event) { + event.originalEvent.dataTransfer.setData('text/html', 'hack for firefox'); + // if users enable zoom or direction options + if (this.$chart.css('transform') !== 'none') { + this.createGhostNode(event); + } + this.filterAllowedDropNodes($(event.target)); + }, + // + dragoverHandler: function (event) { + event.preventDefault(); + if (!$(event.delegateTarget).is('.allowedDrop')) { + event.originalEvent.dataTransfer.dropEffect = 'none'; + } + }, + // + dragendHandler: function (event) { + this.$chart.find('.allowedDrop').removeClass('allowedDrop'); + }, + // + dropHandler: function (event) { + var $dropZone = $(event.delegateTarget); + var $dragged = this.$chart.data('dragged'); + var $dragZone = $dragged.closest('.nodes').siblings().eq(0).children(); + var dropEvent = $.Event('nodedrop.orgchart'); + this.$chart.trigger(dropEvent, { 'draggedNode': $dragged, 'dragZone': $dragZone.children(), 'dropZone': $dropZone }); + if (dropEvent.isDefaultPrevented()) { + return; + } + // firstly, deal with the hierarchy of drop zone + if (!$dropZone.closest('tr').siblings().length) { // if the drop zone is a leaf node + $dropZone.append('<i class="edge verticalEdge bottomEdge fa"></i>') + .parent().attr('colspan', 2) + .parent().after('<tr class="lines"><td colspan="2"><div class="lineDown"></div></td></tr>' + + '<tr class="lines"><td class="lineRight"></td><td class="lineLeft"></td></tr>' + + '<tr class="nodes"></tr>') + .siblings(':last').append($dragged.find('.horizontalEdge').remove().end().closest('table').parent()); + } else { + var dropColspan = parseInt($dropZone.parent().attr('colspan')) + 2; + var horizontalEdges = '<i class="edge horizontalEdge rightEdge fa"></i><i class="edge horizontalEdge leftEdge fa"></i>'; + $dropZone.closest('tr').next().addBack().children().attr('colspan', dropColspan); + if (!$dragged.find('.horizontalEdge').length) { + $dragged.append(horizontalEdges); + } + $dropZone.closest('tr').siblings().eq(1).children(':last').before('<td class="lineLeft lineTop"></td><td class="lineRight lineTop"></td>') + .end().next().append($dragged.closest('table').parent()); + var $dropSibs = $dragged.closest('table').parent().siblings().find('.node:first'); + if ($dropSibs.length === 1) { + $dropSibs.append(horizontalEdges); + } + } + // secondly, deal with the hierarchy of dragged node + var dragColspan = parseInt($dragZone.attr('colspan')); + if (dragColspan > 2) { + $dragZone.attr('colspan', dragColspan - 2) + .parent().next().children().attr('colspan', dragColspan - 2) + .end().next().children().slice(1, 3).remove(); + var $dragSibs = $dragZone.parent().siblings('.nodes').children().find('.node:first'); + if ($dragSibs.length ===1) { + $dragSibs.find('.horizontalEdge').remove(); + } + } else { + $dragZone.removeAttr('colspan') + .find('.bottomEdge').remove() + .end().end().siblings().remove(); + } + }, + // + touchstartHandler: function (event) { + console.log("orgChart: touchstart 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", target=" + event.target.innerText); + if (this.touchHandled) + return; + this.touchHandled = true; + this.touchMoved = false; // this is so we can work out later if this was a 'press' or a 'drag' touch + event.preventDefault(); + }, + // + touchmoveHandler: function (event) { + if (!this.touchHandled) + return; + event.preventDefault(); + if (!this.touchMoved) { + var nodeIsSelected = $(this).hasClass('focused'); + console.log("orgChart: touchmove 1: " + event.touches.length + " touches, we have not moved, so simulate a drag start", event.touches); + // TODO: visualise the start of the drag (as would happen on desktop) + this.simulateMouseEvent(event, 'dragstart'); + } + this.touchMoved = true; + var $touching = $(document.elementFromPoint(event.touches[0].clientX, event.touches[0].clientY)); + var $touchingNode = $touching.closest('div.node'); + + if ($touchingNode.length > 0) { + var touchingNodeElement = $touchingNode[0]; + // TODO: simulate the dragover visualisation + if ($touchingNode.is('.allowedDrop')) { + console.log("orgChart: touchmove 2: this node (" + touchingNodeElement.id + ") is allowed to be a drop target"); + this.touchTargetNode = touchingNodeElement; + } else { + console.log("orgChart: touchmove 3: this node (" + touchingNodeElement.id + ") is NOT allowed to be a drop target"); + this.touchTargetNode = null; + } + } else { + console.log("orgchart: touchmove 4: not touching a node"); + this.touchTargetNode = null; + } + }, + // + touchendHandler: function (event) { + console.log("orgChart: touchend 1: touchHandled=" + this.touchHandled + ", touchMoved=" + this.touchMoved + ", " + event.target.innerText + " "); + if (!this.touchHandled) { + console.log("orgChart: touchend 2: not handled by us, so aborting"); + return; + } + if (this.touchMoved) { + // we've had movement, so this was a 'drag' touch + if (this.touchTargetNode) { + console.log("orgChart: touchend 3: moved to a node, so simulating drop"); + var fakeEventForDropHandler = { delegateTarget: this.touchTargetNode }; + this.dropHandler(fakeEventForDropHandler); + this.touchTargetNode = null; + } + console.log("orgChart: touchend 4: simulating dragend"); + this.simulateMouseEvent(event, 'dragend'); + } + else { + // we did not move, so assume this was a 'press' touch + console.log("orgChart: touchend 5: moved, so simulating click"); + this.simulateMouseEvent(event, 'click'); + } + this.touchHandled = false; + }, + // simulate a mouse event (so we can fake them on a touch device) + simulateMouseEvent: function (event, simulatedType) { + // Ignore multi-touch events + if (event.originalEvent.touches.length > 1) { + return; + } + var touch = event.originalEvent.changedTouches[0]; + var simulatedEvent = document.createEvent('MouseEvents'); + simulatedEvent.initMouseEvent( + simulatedType, // type + true, // bubbles + true, // cancelable + window, // view + 1, // detail + touch.screenX, // screenX + touch.screenY, // screenY + touch.clientX, // clientX + touch.clientY, // clientY + false, // ctrlKey + false, // altKey + false, // shiftKey + false, // metaKey + 0, // button + null // relatedTarget + ); + // Dispatch the simulated event to the target element + event.target.dispatchEvent(simulatedEvent); + }, + // + bindDragDrop: function ($node) { + $node.on('dragstart', this.dragstartHandler.bind(this)) + .on('dragover', this.dragoverHandler.bind(this)) + .on('dragend', this.dragendHandler.bind(this)) + .on('drop', this.dropHandler.bind(this)) + .on('touchstart', this.touchstartHandler.bind(this)) + .on('touchmove', this.touchmoveHandler.bind(this)) + .on('touchend', this.touchendHandler.bind(this)); + }, + // create node + createNode: function (data) { + var that = this; + var opts = this.options; + var level = data.level; + if (data.children) { + $.each(data.children, function (index, child) { + child.parentId = data.id; + }); + } + // construct the content of node + var $nodeDiv = $('<div' + (opts.draggable ? ' draggable="true"' : '') + (data[opts.nodeId] ? ' id="' + data[opts.nodeId] + '"' : '') + (data.parentId ? ' data-parent="' + data.parentId + '"' : '') + '>') + .addClass('node ' + (data.className || '') + (level > opts.visibleLevel ? ' slide-up' : '')); + if (opts.nodeTemplate) { + $nodeDiv.append(opts.nodeTemplate(data)); + } else { + $nodeDiv.append('<div class="title">' + data[opts.nodeTitle] + '</div>') + .append(typeof opts.nodeContent !== 'undefined' ? '<div class="content">' + (data[opts.nodeContent] || '') + '</div>' : ''); + } + // + var nodeData = $.extend({}, data); + delete nodeData.children; + $nodeDiv.data('nodeData', nodeData); + // append 4 direction arrows or expand/collapse buttons + var flags = data.relationship || ''; + if (opts.verticalLevel && level >= opts.verticalLevel) { + if ((level + 1) > opts.verticalLevel && Number(flags.substr(2,1))) { + var icon = level + 1 > opts.visibleLevel ? 'plus' : 'minus'; + $nodeDiv.append('<i class="toggleBtn fa fa-' + icon + '-square"></i>'); + } + } else { + if (Number(flags.substr(0,1))) { + $nodeDiv.append('<i class="edge verticalEdge topEdge fa"></i>'); + } + if(Number(flags.substr(1,1))) { + $nodeDiv.append('<i class="edge horizontalEdge rightEdge fa"></i>' + + '<i class="edge horizontalEdge leftEdge fa"></i>'); + } + if(Number(flags.substr(2,1))) { + $nodeDiv.append('<i class="edge verticalEdge bottomEdge fa"></i>') + .children('.title').prepend('<i class="fa '+ opts.parentNodeSymbol + ' symbol"></i>'); + } + } + + if (opts.draggable) { + this.bindDragDrop($nodeDiv); + this.touchHandled = false; + this.touchMoved = false; + this.touchTargetNode = null; + } + // allow user to append dom modification after finishing node create of orgchart + if (opts.createNode) { + opts.createNode($nodeDiv, data); + } + + return $nodeDiv; + }, + // recursively build the tree + buildHierarchy: function ($appendTo, data) { + var that = this; + var opts = this.options; + var level = 0; + if (data.level) { + level = data.level; + } else { + level = data.level = $appendTo.parentsUntil('.orgchart', '.nodes').length + 1; + } + // Construct the node + var childrenData = data.children; + var hasChildren = childrenData ? childrenData.length : false; + var $nodeWrapper; + if (Object.keys(data).length > 2) { + var $nodeDiv = this.createNode(data); + if (opts.verticalLevel && level >= opts.verticalLevel) { + $appendTo.append($nodeDiv); + }else { + $nodeWrapper = $('<table>'); + $appendTo.append($nodeWrapper.append($('<tr/>').append($('<td' + (hasChildren ? ' colspan="' + childrenData.length * 2 + '"' : '') + '></td>').append($nodeDiv)))); + } + } + // Construct the lower level(two "connectiong lines" rows and "inferior nodes" row) + if (hasChildren) { + var isHidden = (level + 1 > opts.visibleLevel || data.collapsed) ? ' hidden' : ''; + var isVerticalLayer = (opts.verticalLevel && (level + 1) >= opts.verticalLevel) ? true : false; + var $nodesLayer; + if (isVerticalLayer) { + $nodesLayer = $('<ul>'); + if (isHidden && level + 1 > opts.verticalLevel) { + $nodesLayer.addClass(isHidden); + } + if (level + 1 === opts.verticalLevel) { + $appendTo.children('table').append('<tr class="verticalNodes' + isHidden + '"><td></td></tr>') + .find('.verticalNodes').children().append($nodesLayer); + } else { + $appendTo.append($nodesLayer); + } + } else { + var $upperLines = $('<tr class="lines' + isHidden + '"><td colspan="' + childrenData.length * 2 + '"><div class="lineDown"></div></td></tr>'); + var lowerLines = '<tr class="lines' + isHidden + '"><td class="lineRight"></td>'; + for (var i=1; i<childrenData.length; i++) { + lowerLines += '<td class="lineLeft lineTop"></td><td class="lineRight lineTop"></td>'; + } + lowerLines += '<td class="lineLeft"></td></tr>'; + $nodesLayer = $('<tr class="nodes' + isHidden + '">'); + if (Object.keys(data).length === 2) { + $appendTo.append($upperLines).append(lowerLines).append($nodesLayer); + } else { + $nodeWrapper.append($upperLines).append(lowerLines).append($nodesLayer); + } + } + // recurse through children nodes + $.each(childrenData, function () { + var $nodeCell = isVerticalLayer ? $('<li>') : $('<td colspan="2">'); + $nodesLayer.append($nodeCell); + this.level = level + 1; + that.buildHierarchy($nodeCell, this); + }); + } + }, + // build the child nodes of specific node + buildChildNode: function ($appendTo, data) { + $appendTo.find('td:first').attr('colspan', data.length * 2); + this.buildHierarchy($appendTo, { 'children': data }); + }, + // exposed method + addChildren: function ($node, data) { + this.buildChildNode($node.closest('table'), data); + if (!$node.children('.bottomEdge').length) { + $node.append('<i class="edge verticalEdge bottomEdge fa"></i>'); + } + if (!$node.find('.symbol').length) { + $node.children('.title').prepend('<i class="fa '+ this.options.parentNodeSymbol + ' symbol"></i>'); + } + if (this.isInAction($node)) { + this.switchVerticalArrow($node.children('.bottomEdge')); + } + }, + // build the parent node of specific node + buildParentNode: function ($currentRoot, data) { + data.relationship = data.relationship || '001'; + var $table = $('<table>') + .append($('<tr>').append($('<td colspan="2">').append(this.createNode(data)))) + .append('<tr class="lines"><td colspan="2"><div class="lineDown"></div></td></tr>') + .append('<tr class="lines"><td class="lineRight"></td><td class="lineLeft"></td></tr>'); + this.$chart.prepend($table) + .children('table:first').append('<tr class="nodes"><td colspan="2"></td></tr>') + .children('tr:last').children().append(this.$chart.children('table').last()); + }, + // exposed method + addParent: function ($currentRoot, data) { + this.buildParentNode($currentRoot, data); + if (!$currentRoot.children('.topEdge').length) { + $currentRoot.children('.title').after('<i class="edge verticalEdge topEdge fa"></i>'); + } + if (this.isInAction($currentRoot)) { + this.switchVerticalArrow($currentRoot.children('.topEdge')); + } + }, + // subsequent processing of build sibling nodes + complementLine: function ($oneSibling, siblingCount, existingSibligCount) { + var lines = ''; + for (var i = 0; i < existingSibligCount; i++) { + lines += '<td class="lineLeft lineTop"></td><td class="lineRight lineTop"></td>'; + } + $oneSibling.parent().prevAll('tr:gt(0)').children().attr('colspan', siblingCount * 2) + .end().next().children(':first').after(lines); + }, + // build the sibling nodes of specific node + buildSiblingNode: function ($nodeChart, data) { + var newSiblingCount = $.isArray(data) ? data.length : data.children.length; + var existingSibligCount = $nodeChart.parent().is('td') ? $nodeChart.closest('tr').children().length : 1; + var siblingCount = existingSibligCount + newSiblingCount; + var insertPostion = (siblingCount > 1) ? Math.floor(siblingCount/2 - 1) : 0; + // just build the sibling nodes for the specific node + if ($nodeChart.parent().is('td')) { + var $parent = $nodeChart.closest('tr').prevAll('tr:last'); + $nodeChart.closest('tr').prevAll('tr:lt(2)').remove(); + this.buildChildNode($nodeChart.parent().closest('table'), data); + var $siblingTds = $nodeChart.parent().closest('table').children('tr:last').children('td'); + if (existingSibligCount > 1) { + this.complementLine($siblingTds.eq(0).before($nodeChart.closest('td').siblings().addBack().unwrap()), siblingCount, existingSibligCount); + } else { + this.complementLine($siblingTds.eq(insertPostion).after($nodeChart.closest('td').unwrap()), siblingCount, 1); + } + } else { // build the sibling nodes and parent node for the specific ndoe + this.buildHierarchy($nodeChart.closest('.orgchart'), data); + this.complementLine($nodeChart.next().children('tr:last').children().eq(insertPostion).after($('<td colspan="2">').append($nodeChart)), + siblingCount, 1); + } + }, + // + addSiblings: function ($node, data) { + this.buildSiblingNode($node.closest('table'), data); + $node.closest('.nodes').data('siblingsLoaded', true); + if (!$node.children('.leftEdge').length) { + $node.children('.topEdge').after('<i class="edge horizontalEdge rightEdge fa"></i><i class="edge horizontalEdge leftEdge fa"></i>'); + } + if (this.isInAction($node)) { + this.switchHorizontalArrow($node); + $node.children('.topEdge').removeClass('fa-chevron-up').addClass('fa-chevron-down'); + } + }, + // + removeNodes: function ($node) { + var $parent = $node.closest('table').parent(); + var $sibs = $parent.parent().siblings(); + if ($parent.is('td')) { + if (this.getNodeState($node, 'siblings').exist) { + $sibs.eq(2).children('.lineTop:lt(2)').remove(); + $sibs.slice(0, 2).children().attr('colspan', $sibs.eq(2).children().length); + $parent.remove(); + } else { + $sibs.eq(0).children().removeAttr('colspan') + .find('.bottomEdge').remove() + .end().end().siblings().remove(); + } + } else { + $parent.add($parent.siblings()).remove(); + } + }, + // + export: function (exportFilename, exportFileextension) { + var that = this; + exportFilename = (typeof exportFilename !== 'undefined') ? exportFilename : this.options.exportFilename; + exportFileextension = (typeof exportFileextension !== 'undefined') ? exportFileextension : this.options.exportFileextension; + if ($(this).children('.spinner').length) { + return false; + } + var $chartContainer = this.$chartContainer; + var $mask = $chartContainer.find('.mask'); + if (!$mask.length) { + $chartContainer.append('<div class="mask"><i class="fa fa-circle-o-notch fa-spin spinner"></i></div>'); + } else { + $mask.removeClass('hidden'); + } + var sourceChart = $chartContainer.addClass('canvasContainer').find('.orgchart:not(".hidden")').get(0); + var flag = that.options.direction === 'l2r' || that.options.direction === 'r2l'; + html2canvas(sourceChart, { + 'width': flag ? sourceChart.clientHeight : sourceChart.clientWidth, + 'height': flag ? sourceChart.clientWidth : sourceChart.clientHeight, + 'onclone': function (cloneDoc) { + $(cloneDoc).find('.canvasContainer').css('overflow', 'visible') + .find('.orgchart:not(".hidden"):first').css('transform', ''); + }, + 'onrendered': function (canvas) { + $chartContainer.find('.mask').addClass('hidden'); + if (exportFileextension.toLowerCase() === 'pdf') { + var doc = {}; + var docWidth = Math.floor(canvas.width * 0.2646); + var docHeight = Math.floor(canvas.height * 0.2646); + if (docWidth > docHeight) { + doc = new jsPDF('l', 'mm', [docWidth, docHeight]); + } else { + doc = new jsPDF('p', 'mm', [docHeight, docWidth]); + } + doc.addImage(canvas.toDataURL(), 'png', 0, 0); + doc.save(exportFilename + '.pdf'); + } else { + var isWebkit = 'WebkitAppearance' in document.documentElement.style; + var isFf = !!window.sidebar; + var isEdge = navigator.appName === 'Microsoft Internet Explorer' || (navigator.appName === "Netscape" && navigator.appVersion.indexOf('Edge') > -1); + + if ((!isWebkit && !isFf) || isEdge) { + window.navigator.msSaveBlob(canvas.msToBlob(), exportFilename + '.png'); + } else { + var selector = '.oc-download-btn' + (that.options.chartClass !== '' ? '.' + that.options.chartClass : ''); + if (!$chartContainer.find(selector).length) { + $chartContainer.append('<a class="oc-download-btn' + (that.options.chartClass !== '' ? ' ' + that.options.chartClass : '') + '"' + + ' download="' + exportFilename + '.png"></a>'); + } + $chartContainer.find(selector).attr('href', canvas.toDataURL())[0].click(); + } + } + } + }) + .then(function () { + $chartContainer.removeClass('canvasContainer'); + }, function () { + $chartContainer.removeClass('canvasContainer'); + }); + } + }; + + $.fn.orgchart = function (opts) { + return new OrgChart(this, opts).init(); + }; + +})); |
