bumbeishvili / org-chart

Highly customizable org chart. Integrations available for Angular, React, Vue
https://stackblitz.com/edit/web-platform-o5t1ha
MIT License
918 stars 328 forks source link

ContextMenu #282

Closed SamurayYata closed 1 year ago

SamurayYata commented 1 year ago

i'll must implementation the context menu dynamically in my application . is possible to have one guide to reference the code. thak

bumbeishvili commented 1 year ago

Hi, this is how I implemented it 2 years ago

For JS and CSS files I think you can directly embed them, but you will need to modify usage part based on your needs

import { contextMenu } from "./d3-org-chart-context-menu";

let prevClickedMenu = null;;

chart
  .onZoom(() => {
          contextMenu("close", null);
        })
 .nodeUpdate(nodeUpdateHandler)

  function nodeUpdateHandler (this: HTMLElement, d: OrgNode): void {

 d3.select(this)
        .select(".chart-node-more-button")
        .on(
          "click.context",
          function (this: HTMLElement, event, nodeContextData) {
            var menu = [
              {
                title: "Assign Recruiter",
                visible: inProgress,
                action: function (node) {
                  onAssignRecruiters(node);
                },
              },
              {
                title: "Approval Chain",
                visible: inProgress,
                action: function (node) {
                  onApprovalChain(node);
                },
              },
              {
                title: "View Details",
                visible: true,
                action: function (node) {
                  onViewDetails(node);
                },
              },
            ].filter((node) => node.visible);

            if (
              prevClickedMenu &&
              prevClickedMenu === nodeData.data &&
              d3.select(".d3-context-menu").node()
            ) {
              contextMenu("close", null);
            } else {
              const ctxMenu = contextMenu(menu, null);
              const elemBoundContextMenu = ctxMenu?.bind(this);
              elemBoundContextMenu(event, nodeContextData);
              // prevClickedMenu = nodeData.data;
            }
          }
        );
    }

  }

This is context menu css file I've used

/* Layout
------------ */

.d3-context-menu {
    position: absolute;
    min-width: 150px;
    z-index: 1200;
}

.d3-context-menu ul,
.d3-context-menu ul li {
    margin: 0;
    padding: 0;
}

.d3-context-menu ul {
    list-style-type: none;
    cursor: default;
}

.d3-context-menu ul li {
    -webkit-touch-callout: none;
    /* iOS Safari */
    -webkit-user-select: none;
    /* Chrome/Safari/Opera */
    -khtml-user-select: none;
    /* Konqueror */
    -moz-user-select: none;
    /* Firefox */
    -ms-user-select: none;
    /* Internet Explorer/Edge */
    user-select: none;
}

/*
    Disabled
*/

.d3-context-menu ul li.is-disabled,
.d3-context-menu ul li.is-disabled:hover {
    cursor: not-allowed;
}

/*
    Divider
*/

.d3-context-menu ul li.is-divider {
    padding: 0;
}

/* Theming
------------ */

.d3-context-menu-theme {
    background-color: white;
    border-radius: 4px;
    color: #3c4858;

    font-family: Arial, sans-serif;
    font-size: 14px;
    border: 1px solid #d3dce6;
}

.d3-context-menu-theme ul {
    margin: 7px 7px;
}

.d3-context-menu-theme ul li {
    cursor: pointer;
}

.d3-context-menu-theme ul li:hover {
    color: #3c4858;
}

.d3-context-menu-row-wrapper {
    padding: 8px 16px;
}

.d3-context-menu-row-wrapper:hover {
    background-color: #eff2f7;
}

/*
    Header
*/

.d3-context-menu-theme ul li.is-header,
.d3-context-menu-theme ul li.is-header:hover {
    background-color: #f2f2f2;
    color: #444;
    font-weight: bold;
    font-style: italic;
}

/*
    Disabled
*/

.d3-context-menu-theme ul li.is-disabled,
.d3-context-menu-theme ul li.is-disabled:hover {
    background-color: #f2f2f2;
    color: #888;
}

/*
    Divider
*/

.d3-context-menu-theme ul li.is-divider:hover {
    background-color: #f2f2f2;
}

.d3-context-menu-theme ul hr {
    border: 0;
    height: 0;
    border-top: 1px solid rgba(0, 0, 0, 0.1);
    border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}

/*
    Nested Menu
*/
.d3-context-menu-theme ul li.is-parent:after {
    border-left: 7px solid transparent;
    border-top: 7px solid red;
    content: "";
    height: 0;
    position: absolute;
    right: 8px;
    top: 35%;
    transform: rotate(45deg);
    width: 0;
}

.d3-context-menu-theme ul li.is-parent {
    padding-right: 20px;
    position: relative;
}

.d3-context-menu-theme ul.is-children {
    background-color: #f2f2f2;
    border: 1px solid #d4d4d4;
    color: black;
    display: none;
    left: 100%;
    margin: -5px 0;
    padding: 4px 0;
    position: absolute;
    top: 0;
    width: 100%;
}

.d3-context-menu-theme li.is-parent:hover > ul.is-children {
    display: block;
}

This is context menu js file I've used

import * as d3 from "d3";

var utils = {
    noop: function () { },

    /**
     * @param {*} value
     * @returns {Boolean}
     */
    isFn: function (value) {
        return typeof value === 'function';
    },

    /**
     * @param {*} value
     * @returns {Function}
     */
    const: function (value) {
        return function () { return value; };
    },

    /**
     * @param {Function|*} value
     * @param {*} [fallback]
     * @returns {Function}
     */
    toFactory: function (value, fallback) {
        value = (value === undefined) ? fallback : value;
        return utils.isFn(value) ? value : utils.const(value);
    }
};

// global state for d3-context-menu
var d3ContextMenu = null;

var closeMenu = function () {
    // global state is populated if a menu is currently opened
    if (d3ContextMenu) {
        d3.select('.d3-context-menu').remove();
        d3.select('body').on('mousedown.d3-context-menu', null);
        d3ContextMenu.boundCloseCallback();
        d3ContextMenu = null;
    }
};

/**
 * Calls API method (e.g. `close`) or
 * returns handler function for the `contextmenu` event
 * @param {Function|Array|String} menuItems
 * @param {Function|Object} config
 * @returns {?Function}
 */
export function contextMenu(menuItems, config) {
    // allow for `d3.contextMenu('close');` calls
    // to programatically close the menu
    if (menuItems === 'close') {
        return closeMenu();
    }

    // for convenience, make `menuItems` a factory
    // and `config` an object
    menuItems = utils.toFactory(menuItems);

    if (utils.isFn(config)) {
        config = { onOpen: config };
    }
    else {
        config = config || {};
    }

    // resolve config
    var openCallback = config.onOpen || utils.noop;
    var closeCallback = config.onClose || utils.noop;
    var positionFactory = utils.toFactory(config.position);
    var themeFactory = utils.toFactory(config.theme, 'd3-context-menu-theme');

    /**
     * Context menu event handler
     * @param {*} param1
     * @param {*} param2
     */
    return function (param1, param2) {
        var element = this;

        // eventOrIndex is the second argument that will be passed to 
        // the context menu callbacks: for D3 6.x or above it will be 
        // the event, since the global d3.event is not available.
        // For D3 5.x or below it will be the index, for backward 
        // compatibility reasons.
        var event, data, index, eventOrIndex;
        if (d3.event === undefined) {
            // Using D3 6.x or above
            event = param1;
            data = param2;

            // We cannot tell the index properly in new D3 versions,
            // since it is not possible to access the original selection.
            index = undefined;
            eventOrIndex = event;
        } else {
            // Using D3 5.x or below
            event = d3.event;
            data = param1;
            index = param2;
            eventOrIndex = index;
        }

        // close any menu that's already opened
        closeMenu();

        // store close callback already bound to the correct args and scope
        d3ContextMenu = {
            boundCloseCallback: closeCallback.bind(element, data, eventOrIndex)
        };

        // create the div element that will hold the context menu
        d3.selectAll('.d3-context-menu').data([1])
            .enter()
            .append('div')
            .attr('class', 'd3-context-menu ' + themeFactory.bind(element)(data, eventOrIndex));

        // close menu on mousedown outside
        d3.select('body').on('mousedown.d3-context-menu', closeMenu);
        d3.select('body').on('click.d3-context-menu', closeMenu);

        var parent = d3.selectAll('.d3-context-menu')
            .on('contextmenu', function () {
                closeMenu();
                event.preventDefault();
                event.stopPropagation();
            })
            .append('ul');

        parent.call(createNestedMenu, element);

        // the openCallback allows an action to fire before the menu is displayed
        // an example usage would be closing a tooltip
        if (openCallback.bind(element)(data, eventOrIndex) === false) {
            return;
        }

        //console.log(this.parentNode.parentNode.parentNode);//.getBoundingClientRect());   Use this if you want to align your menu from the containing element, otherwise aligns towards center of window

        // get position
        var position = element.getBoundingClientRect(); // positionFactory.bind(element)(data, eventOrIndex);

        var doc = document.documentElement;
        var pageWidth = window.innerWidth || doc.clientWidth;
        var pageHeight = window.innerHeight || doc.clientHeight;

        var horizontalAlignment = 'left';
        var horizontalAlignmentReset = 'right';
        var horizontalValue = position ? position.left + 30 : event.pageX - 2;
        // if (event.pageX > pageWidth / 2) {
        //     horizontalAlignment = 'right';
        //     horizontalAlignmentReset = 'left';
        //     horizontalValue = position ? pageWidth - position.left : pageWidth - event.pageX - 2;
        // }

        var verticalAlignment = 'top';
        var verticalAlignmentReset = 'bottom';
        var verticalValue = position ? position.top + 33 : event.pageY - 2;
        // if (event.pageY > pageHeight / 2) {
        //     verticalAlignment = 'bottom';
        //     verticalAlignmentReset = 'top';
        //     verticalValue = position ? pageHeight - position.top : pageHeight - event.pageY - 2;
        // }

        // display context menu
        d3.select('.d3-context-menu')
            .style(horizontalAlignment, (horizontalValue) + 'px')
            .style(horizontalAlignmentReset, null)
            .style(verticalAlignment, (verticalValue) + 'px')
            .style(verticalAlignmentReset, null)
            .style('display', 'block');

        event.preventDefault();
        event.stopPropagation();

        function createNestedMenu(parent, root, depth = 0) {
            var resolve = function (value) {
                return utils.toFactory(value).call(root, data, eventOrIndex);
            };

            parent.selectAll('li')
                .data(function (d) {
                    var baseData = depth === 0 ? menuItems : d.children;
                    return resolve(baseData);
                })
                .enter()
                .append('li')
                .each(function (d) {
                    // get value of each data
                    var isDivider = !!resolve(d.divider);
                    var isDisabled = !!resolve(d.disabled);
                    var hasChildren = !!resolve(d.children);
                    var hasAction = !!d.action;
                    var hasCls = !!d.className;
                    var text = isDivider ? '<hr>' : resolve(d.title);

                    var listItem = d3.select(this)
                        .classed('is-divider', isDivider)
                        .classed('is-disabled', isDisabled)
                        .classed('is-header', !hasChildren && !hasAction)
                        .classed('is-parent', hasChildren)
                        .classed(resolve(d.className), hasCls)
                        .html(`<div class="d3-context-menu-row-wrapper">` + text + `</div>`)
                        .on('click', function () {
                            // do nothing if disabled or no action
                            if (isDisabled || !hasAction) return;

                            d.action.call(root, data, eventOrIndex);
                            closeMenu();
                        });

                    if (hasChildren) {
                        // create children(`next parent`) and call recursive
                        var children = listItem.append('ul').classed('is-children', true);
                        createNestedMenu(children, root, ++depth)
                    }
                });
        }
    };
};
SamurayYata commented 1 year ago

Well . but i don't use tTypeScript file . i use js . the notation HTMLElement is used only on typescript

bumbeishvili commented 1 year ago

I know, you have to clear that file from ts notations. Just copy paste won't work , treat it as general guide