(function(root, factory) {
|
if (typeof module === 'object' && module.exports) {
|
var d3 = require('d3');
|
module.exports = factory(d3);
|
} else if(typeof define === 'function' && define.amd) {
|
try {
|
var d3 = require('d3');
|
} catch (e) {
|
d3 = root.d3;
|
}
|
|
d3.contextMenu = factory(d3);
|
define([], function() {
|
return d3.contextMenu;
|
});
|
} else if(root.d3) {
|
root.d3.contextMenu = factory(root.d3);
|
}
|
}(this,
|
function (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}
|
*/
|
return function (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 {*} data
|
* @param {Number} index
|
*/
|
return function (data, index) {
|
var element = this;
|
|
// 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, index)
|
};
|
|
// 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, index));
|
|
// close menu on mousedown outside
|
d3.select('body').on('mousedown.d3-context-menu', closeMenu);
|
|
var list = d3.selectAll('.d3-context-menu')
|
.on('contextmenu', function() {
|
closeMenu();
|
d3.event.preventDefault();
|
d3.event.stopPropagation();
|
})
|
.append('ul');
|
|
list.selectAll('li').data(menuItems.bind(element)(data, index)).enter()
|
.append('li')
|
.attr('class', function(d) {
|
var ret = '';
|
if (utils.toFactory(d.divider).bind(element)(data, index)) {
|
ret += ' is-divider';
|
}
|
if (utils.toFactory(d.disabled).bind(element)(data, index)) {
|
ret += ' is-disabled';
|
}
|
if (!d.action) {
|
ret += ' is-header';
|
}
|
return ret;
|
})
|
.html(function(d) {
|
if (utils.toFactory(d.divider).bind(element)(data, index)) {
|
return '<hr>';
|
}
|
if (!d.title) {
|
console.error('No title attribute set. Check the spelling of your options.');
|
}
|
return utils.toFactory(d.title).bind(element)(data, index);
|
})
|
.on('click', function(d, i) {
|
if (utils.toFactory(d.disabled).bind(element)(data, index)) return; // do nothing if disabled
|
if (!d.action) return; // headers have no "action"
|
d.action.bind(element)(data, index);
|
closeMenu();
|
});
|
|
// 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, index) === false) {
|
return;
|
}
|
|
// get position
|
var position = positionFactory.bind(element)(data, index);
|
|
// display context menu
|
d3.select('.d3-context-menu')
|
.style('left', (position ? position.left : d3.event.pageX - 2) + 'px')
|
.style('top', (position ? position.top : d3.event.pageY - 2) + 'px')
|
.style('display', 'block');
|
|
d3.event.preventDefault();
|
d3.event.stopPropagation();
|
};
|
};
|
}
|
));
|