Closed SamurayYata closed 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)
}
});
}
};
};
Well . but i don't use tTypeScript file . i use js . the notation HTMLElement is used only on typescript
I know, you have to clear that file from ts notations. Just copy paste won't work , treat it as general guide
i'll must implementation the context menu dynamically in my application . is possible to have one guide to reference the code. thak