2
votes

I have a D3 SVG content with circles and on Right Click it opens an context menu displaying list of items from an array. I want to show specific images beside every element in the context menu.

The code for context menu is here ContextMenu Example

var fruits = ["Apple", "Orange", "Banana", "Grape"];

var svgContainer = d3.select("body")
	.append("svg")
    .attr("width", 200)
    .attr("height", 200);

var circle = svgContainer
	.append("circle")
    .attr("cx", 30)
    .attr("cy", 30)
    .attr("r", 20)
    .on('contextmenu', function(d,i) {
          // create the div element that will hold the context menu
          d3.selectAll('.context-menu').data([1])
            .enter()
            .append('div')
            .attr('class', 'context-menu');
          // close menu
          d3.select('body').on('click.context-menu', function() {
            d3.select('.context-menu').style('display', 'none');
          });
          // this gets executed when a contextmenu event occurs
          d3.selectAll('.context-menu')
            .html('')
            .append('ul')
            .selectAll('li')
              .data(fruits).enter()
              .append('li')
              
          .on('click' , function(d) { console.log(d); return d; })
              

              .text(function(d) { return d; });
          d3.select('.context-menu').style('display', 'none');
          // show the context menu
          d3.select('.context-menu')
            .style('left', (d3.event.pageX - 2) + 'px')
            .style('top', (d3.event.pageY - 2) + 'px')
            .style('display', 'block');
          d3.event.preventDefault();
      });
.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;
}

.context-menu ul {
  list-style-type: none;
  margin: 4px 0px;
  padding: 0px;
  cursor: default;
}

.context-menu ul li {
  padding: 4px 16px;
}

.context-menu ul li:hover {
  background-color: #4677f8;
  color: #fefefe;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.10/d3.min.js"></script>

Is it possible to show Images beside menu items? If yes how can I show it.

2

2 Answers

3
votes

If we begin by changing your data structure to associate the text with an icon, then we have:

var data = [{
  text: 'Apple',
  icon: 'https://i.stack.imgur.com/5mrSI.jpg?s=32&g=1'
}, {
  text: "Orange",
  icon: 'https://i.stack.imgur.com/5mrSI.jpg?s=32&g=1',
}, {
  text: "Banana",
  icon: 'https://i.stack.imgur.com/5mrSI.jpg?s=32&g=1'
}, {
  text: "Grape",
  icon: 'https://i.stack.imgur.com/5mrSI.jpg?s=32&g=1'
}];

Now we can change how we create the context menu:

d3.selectAll('.context-menu')
    .html('')
    .append('ul')
    .selectAll('li')
    .data(data)
    .enter()
    .append('li')
    .append(function(d) {
        // Create the image element.
        const icon = document.createElement('img');
        // Change its source to the icon.
        icon.src = d.icon;
        // Return the image element.
        return icon;
    })
    // Go back up a node, so that we can append the span.
    .select(function () {
       return this.parentNode;
    })
    .append('span')
    // Change the text of the span element to the current 
    // data's text property.
    .text(function (d) {
        return d.text;
    });

The bottom half of the above snippet is where the work gets done. After creating the list element through .append('li') you then want to append an image element that represents the icon associated with the current context menu item. After the image element is appended, the text node should be added. Since the image element was just appended, the current node refers to this image node. We want to move up one node so we're at the li element again via .select(function () { return this.parentNode; }). Finally, a span element can be appended with its text set to the text in the data. This approach adds greater flexibility to your context menu as you can specify the structure of the HTML instead of relying on styling the list element bullets.

Complete fiddle here.

1
votes

Probably the easiest way to achieve this is to use the list-style-image property of your li items to style the bullet which precedes each item. This property can be set to whatever image you like to use while appending the list items. For the sake of simplicity my code refers to a bunch of sample images:

.selectAll('li')
  .data(fruits).enter()
  .append('li')
    .style('list-style-image', function(d,i) {
      return 'url("http://loremflickr.com/48/48?i=' + i + '")';
    })

I rearranged your snippet a bit to make life a little easier and it might need further refinement, but the following code should be enough to get you started:

var fruits = ["Apple", "Orange", "Banana", "Grape"];

var svgContainer = d3.select("body")
	.append("svg")
    .attr("width", 200)
    .attr("height", 200);

var ctxMenu = d3.select("body")
  .append('div')
    .style('display', 'none')
    .attr('class', 'context-menu');

d3.select('body').on('click.context-menu', function() {
  ctxMenu.style('display', 'none');
});

var circle = svgContainer
	.append("circle")
    .attr("cx", 60)
    .attr("cy", 60)
    .attr("r", 20)
    .on('contextmenu', function(d,i) {
          ctxMenu.selectAll("ul").remove();
          
          // create the div element that will hold the context menu
          // this gets executed when a contextmenu event occurs
          ctxMenu
  					.append('ul')
            .selectAll('li')
            .data(fruits).enter()
            .append('li')
              .style('list-style-image', function(d,i) {
                return 'url("http://loremflickr.com/48/48?i=' + i + '")';
              })
              .on('click' , function(d) { console.log(d); return d; })
              .text(function(d) { return d; });

         // show the context menu
          ctxMenu
            .style('left', (d3.event.pageX - 2) + 'px')
            .style('top', (d3.event.pageY - 2) + 'px')
            .style('display', 'block');
          d3.event.preventDefault();
      });
.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;
}

.context-menu ul {
  margin: 4px 0px;
  padding: 0px;
  cursor: default;
}

.context-menu ul li {
  padding: 4px 12px;
  margin-left: 65px;
}

.context-menu ul li:hover {
  background-color: #4677f8;
  color: #fefefe;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.10/d3.min.js"></script>