diff --git a/css/d3-context-menu.css b/css/d3-context-menu.css
new file mode 100644
index 0000000..334513c
--- /dev/null
+++ b/css/d3-context-menu.css
@@ -0,0 +1,78 @@
+.d3-context-menu {
+ position: absolute;
+ display: none;
+ background-color: #f2f2f2;
+ border-radius: 4px;
+
+ font-family: Arial, sans-serif;
+ font-size: 14px;
+ min-width: 150px;
+ border: 1px solid #d4d4d4;
+
+ z-index:1200;
+}
+
+.d3-context-menu ul {
+ list-style-type: none;
+ margin: 4px 0px;
+ padding: 0px;
+ cursor: default;
+}
+
+.d3-context-menu ul li {
+ padding: 4px 16px;
+
+ -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;
+}
+
+.d3-context-menu ul li:hover {
+ background-color: #4677f8;
+ color: #fefefe;
+}
+
+/*
+ Header
+*/
+
+.d3-context-menu ul li.is-header,
+.d3-context-menu ul li.is-header:hover {
+ background-color: #f2f2f2;
+ color: #444;
+ font-weight: bold;
+ font-style: italic;
+}
+
+/*
+ Disabled
+*/
+
+.d3-context-menu ul li.is-disabled,
+.d3-context-menu ul li.is-disabled:hover {
+ background-color: #f2f2f2;
+ color: #888;
+ cursor: not-allowed;
+}
+
+/*
+ Divider
+*/
+
+.d3-context-menu ul li.is-divider {
+ padding: 0px 0px;
+}
+
+.d3-context-menu ul li.is-divider:hover {
+ background-color: #f2f2f2;
+}
+
+.d3-context-menu 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);
+}
\ No newline at end of file
diff --git a/index.html b/index.html
index f9741ea..a02d0c1 100644
--- a/index.html
+++ b/index.html
@@ -7,6 +7,7 @@
+
diff --git a/js/d3-context-menu.js b/js/d3-context-menu.js
new file mode 100644
index 0000000..366b64d
--- /dev/null
+++ b/js/d3-context-menu.js
@@ -0,0 +1,99 @@
+(function(root, factory) {
+ if (typeof module === 'object' && module.exports) {
+ module.exports = function(d3) {
+ d3.contextMenu = factory(d3);
+ return d3.contextMenu;
+ };
+ } else {
+ root.d3.contextMenu = factory(root.d3);
+ }
+}( this,
+ function(d3) {
+ return function (menu, opts) {
+
+ var openCallback,
+ closeCallback;
+
+ if (typeof opts === 'function') {
+ openCallback = opts;
+ } else {
+ opts = opts || {};
+ openCallback = opts.onOpen;
+ closeCallback = opts.onClose;
+ }
+
+ // 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');
+
+ // close menu
+ d3.select('body').on('click.d3-context-menu', function() {
+ d3.select('.d3-context-menu').style('display', 'none');
+ if (closeCallback) {
+ closeCallback();
+ }
+ });
+
+ // this gets executed when a contextmenu event occurs
+ return function(data, index) {
+ var elm = this;
+
+ d3.selectAll('.d3-context-menu').html('');
+ var list = d3.selectAll('.d3-context-menu').append('ul');
+ list.selectAll('li').data(typeof menu === 'function' ? menu(data) : menu).enter()
+ .append('li')
+ .attr('class', function(d) {
+ var ret = '';
+ if (d.divider) {
+ ret += ' is-divider';
+ }
+ if (d.disabled) {
+ ret += ' is-disabled';
+ }
+ if (!d.action) {
+ ret += ' is-header';
+ }
+ return ret;
+ })
+ .html(function(d) {
+ if (d.divider) {
+ return '
';
+ }
+ if (!d.title) {
+ console.error('No title attribute set. Check the spelling of your options.');
+ }
+ return (typeof d.title === 'string') ? d.title : d.title(data);
+ })
+ .on('click', function(d, i) {
+ if (d.disabled) return; // do nothing if disabled
+ if (!d.action) return; // headers have no "action"
+ d.action(elm, data, index);
+ d3.select('.d3-context-menu').style('display', 'none');
+
+ if (closeCallback) {
+ closeCallback();
+ }
+ });
+
+ // the openCallback allows an action to fire before the menu is displayed
+ // an example usage would be closing a tooltip
+ if (openCallback) {
+ if (openCallback(data, index) === false) {
+ return;
+ }
+ }
+
+ // display context menu
+ d3.select('.d3-context-menu')
+ .style('left', (d3.event.pageX - 2) + 'px')
+ .style('top', (d3.event.pageY - 2) + 'px')
+ .style('display', 'block');
+
+ d3.event.preventDefault();
+ d3.event.stopPropagation();
+ };
+ };
+ }
+));
diff --git a/js/explaingit.js b/js/explaingit.js
index 9501ee3..c5de28e 100644
--- a/js/explaingit.js
+++ b/js/explaingit.js
@@ -1,4 +1,4 @@
-define(['historyview', 'controlbox', 'd3'], function (HistoryView, ControlBox, d3) {
+define(['historyview', 'controlbox', 'd3', 'd3.contextMenu'], function (HistoryView, ControlBox, d3, contextMenu) {
var prefix = 'ExplainGit',
openSandBoxes = [],
open,
@@ -37,6 +37,16 @@ define(['historyview', 'controlbox', 'd3'], function (HistoryView, ControlBox, d
originView: originView,
initialMessage: args.initialMessage
});
+
+ if (originView) {
+ originView.svg.on('contextmenu', function() {
+ contextMenu([
+ {title: 'Fetch', action: function() { controlBox.command('git fetch');}},
+ {title: 'Push', action: function() { controlBox.command('git push origin'); }},
+ {title: 'Pull', action: function() { controlBox.command('git pull'); }}
+ ])();
+ })
+ }
controlBox.render(playground);
historyView.render(playground);
diff --git a/js/historyview.js b/js/historyview.js
index 683201e..f3ce159 100644
--- a/js/historyview.js
+++ b/js/historyview.js
@@ -1,4 +1,4 @@
-define(['d3'], function () {
+define(['d3', 'd3.contextMenu'], function (d3, contextMenu) {
"use strict";
var REG_MARKER_END = 'url(#triangle)',
@@ -7,6 +7,7 @@ define(['d3'], function () {
preventOverlap,
applyBranchlessClass,
+ openContextMenu,
cx, cy, fixCirclePosition,
px1, py1, fixPointerStartPosition,
px2, py2, fixPointerEndPosition,
@@ -61,6 +62,23 @@ define(['d3'], function () {
});
}
};
+
+ openContextMenu = function(canCommit, ref, view) {
+ var menu = [];
+ if (canCommit) {
+ menu = menu.concat([
+ {title: "Commit", action: function() { view.commit(); }},
+ {divider: true}
+ ]);
+ }
+ menu = menu.concat([
+ {title: 'Reset', action: function() { view.reset(ref); }},
+ {title: 'Checkout', action: function() { view.checkout(ref); }},
+ {title: 'Merge', action: function() { view.merge(ref); }},
+ {title: 'Rebase', action: function() { view.rebase(ref); }},
+ ]);
+ contextMenu(menu)();
+ }
cx = function (commit, view) {
var parent = view.getCommit(commit.parent),
@@ -486,6 +504,9 @@ define(['d3'], function () {
.classed('merge-commit', function (d) {
return typeof d.parent2 === 'string';
})
+ .on('contextmenu', function(d) {
+ openContextMenu(d3.select(this).classed('checked-out'), d.id, view);
+ })
.call(fixCirclePosition)
.attr('r', 1)
.transition("inflate")
@@ -708,6 +729,9 @@ define(['d3'], function () {
classes += ' head-tag';
}
return classes;
+ })
+ .on('contextmenu', function(d) {
+ openContextMenu(d3.select(this).classed('head-tag'), d.name, view);
});
newTags.append('svg:rect')
diff --git a/js/main.js b/js/main.js
index 40e03d7..ab4ba65 100644
--- a/js/main.js
+++ b/js/main.js
@@ -45,11 +45,16 @@ if (!Array.prototype.indexOf) {
require.config({
paths: {
- 'd3': 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min'
+ 'd3': 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min',
+ 'd3.contextMenu' : 'd3-context-menu'
},
shim: {
'd3': {
exports: 'd3'
+ },
+ 'd3.contextMenu': {
+ deps: ['d3'],
+ exports: 'd3.contextMenu'
}
}
});
\ No newline at end of file