Skip to content

Instantly share code, notes, and snippets.

@jo
Created November 13, 2012 21:46
Show Gist options
  • Save jo/4068610 to your computer and use it in GitHub Desktop.
Save jo/4068610 to your computer and use it in GitHub Desktop.
Selectable elements

Provide selection capabilities for DOM elements, geared to <select multiple>.

  • click: select element
  • ctrl + click: add element to current selection
  • click + move: select elements while dragging
  • ctrl + a: select all elements within focused list
  • ctrl + shift + a: deselect all elements within focused list
  • ctrl + click + move: toggle selection while dragging
  • shift + click: select range from nearest last selected element to clicked element
  • shift + ctrl + click: add range to current selection
  • shift + click between selected elements: select all between first and last selected element
  • home: select first element
  • ctrl + home: add first element to selection
  • end: select last element
  • ctrl + end: add last element to selection
  • up: select element before first selected element
  • ctrl + up: add element before first selected element to selection
  • down: select element after last selected element
  • ctrl + down: add element after last selected element to selection
// D3 Selectable
//
// Bind selection functionality to `ul`, an ancestor node selection
// with its corresponding child selection 'li'.
// Selection state update rendering takes place in the `update` callback.
//
// (c) 2012 Johannes J. Schmidt, TF
d3.selectable = function selection(ul, li, update) {
function isParentNode(parentNode, node) {
if (!node) return false;
if (node === parentNode) return true;
return isParentNode(parentNode, node.parentNode);
}
function selectFirst(selection) {
selection.each(function(d, i) {
if (i === 0) d._selected = true;
});
}
function selectLast(selection) {
selection.each(function(d, i, j) {
if (i === selection[j].length - 1) d._selected = true;
});
}
var lastDecision;
function select(d, node) {
var parentNode = ul.filter(function() { return isParentNode(this, node); }).node(),
lis = li.filter(function() { return isParentNode(parentNode, this); });
// select ranges via `shift` key
if (d3.event.shiftKey) {
var firstSelectedIndex, lastSelectedIndex, currentIndex;
lis.each(function(dl, i) {
if (dl._selected) {
firstSelectedIndex || (firstSelectedIndex = i);
lastSelectedIndex = i;
}
if (this === node) currentIndex = i;
});
var min = Math.min(firstSelectedIndex, lastSelectedIndex, currentIndex);
var max = Math.max(firstSelectedIndex, lastSelectedIndex, currentIndex);
// select all between first and last selected
// when clicked inside a selection
lis.each(function(d, i) {
// preserve state for additive selection
d._selected = (d3.event.ctrlKey && d._selected) || (i >= min && i <= max);
});
} else {
// additive select with `ctrl` key
if (!d3.event.ctrlKey) {
lis.each(function(d) { d._selected = false; });
}
d._selected = !d._selected;
}
// remember decision
lastDecision = d._selected;
update();
}
li.on('mousedown', function(d) {
select(d, this);
});
li.on('mouseover', function(d) {
// dragging over items toggles selection
if (d3.event.which) {
d._selected = lastDecision;
update();
}
});
var keyCodes = {
up: 38,
down: 40,
home: 36,
end: 35,
a: 65
};
ul.on('keydown', function() {
if (d3.values(keyCodes).indexOf(d3.event.keyCode) === -1) return;
if (d3.event.keyCode === keyCodes.a && !d3.event.ctrlKey) return;
var focus = ul.filter(':focus').node();
if (!focus) return;
d3.event.preventDefault();
var scope = li.filter(function(d) { return isParentNode(focus, this); });
var selecteds = scope.select(function(d) { return d._selected; });
if (!d3.event.ctrlKey) {
scope.each(function(d) { d._selected = false; });
}
var madeSelection = false;
switch (d3.event.keyCode) {
case keyCodes.up:
selecteds.each(function(d, i, j) {
if (scope[j][i - 1]) madeSelection = d3.select(scope[j][i - 1]).data()[0]._selected = true;
});
if (!madeSelection) selectLast(scope);
break;
case keyCodes.down:
selecteds.each(function(d, i, j) {
if (scope[j][i + 1]) madeSelection = d3.select(scope[j][i + 1]).data()[0]._selected = true;
});
if (!madeSelection) selectFirst(scope);
break;
case keyCodes.home:
selectFirst(scope);
break;
case keyCodes.end:
selectLast(scope);
break;
case keyCodes.a:
scope.each(function(d) { d._selected = !d3.event.shiftKey; });
break;
}
update();
});
}
<!DOCTYPE html>
<html lang=de>
<style>
body {
font: 13px sans-serif;
}
ul, table {
display: inline-block;
margin: 0 3em 3ex 0;
vertical-align: top;
}
ul {
padding: 0;
list-style-type: none;
}
table {
border-collapse: collapse;
}
li, th, td, text {
padding: 1ex 1em;
cursor: default;
-webkit-user-select: none;
}
th {
text-align: left;
}
.selected {
background-color: #84a9d7;
color: white;
}
text.selected {
font-weight: bold;
}
</style>
<body>
<!--
<script src="../d3.js"></script>
-->
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="d3.selectable.js"></script>
<script>
var groups = [
[
{ "name": "Black Russian" },
{ "name": "Bloody Mary" },
{ "name": "Gin Fizz" },
{ "name": "Gin Tonic" },
{ "name": "Grasshopper" },
{ "name": "Mai Tai", "_selected": true },
{ "name": "Margarita" },
{ "name": "Martini" },
{ "name": "Mojito" },
{ "name": "Pina Colada" },
{ "name": "Ruby Relaxer" },
{ "name": "Sex On The Beach" },
{ "name": "Tequila Sunrise" },
{ "name": "White Russian" }
],
[
{ "name": "Anselm" },
{ "name": "Bernd" },
{ "name": "Elvis" },
{ "name": "Eva" },
{ "name": "Godot" },
{ "name": "Jutta" }
]
];
// create lists
var ul = d3.select('body').selectAll('ul')
.data(groups)
.enter()
.append('ul')
.attr('tabindex', 1); // enable focus
var li = ul.selectAll('li')
.data(function(d) { return d; })
.enter()
.append('li')
.classed('selected', function(d) { return d._selected; })
.text(function(d) { return d.name; });
// create table
var table = d3.select('body').selectAll('table')
.data([groups[0]])
.enter()
.append('table')
.attr('tabindex', 1); // enable focus
var tr = table.append('tr');
tr.append('th').text('ID');
tr.append('th').text('Name');
tr = table.selectAll('tr')
.data(function(d) { return d; })
.enter()
.append('tr')
.classed('selected', function(d) { return d._selected; })
tr.append('td').text(function(d, i) { return i; });
tr.append('td').text(function(d) { return d.name; });
// create svg
var svg = d3.select('body').selectAll('svg')
.data([groups[1]])
.enter()
.append('svg')
.attr("width", 400)
.attr("height", 400);
circle = svg.selectAll('circle')
.data(function(d) { return d; })
.enter()
.append('g')
.append('text')
.attr('x', function() { return 20 + Math.random() * 360; })
.attr('y', function() { return 20 + Math.random() * 360; })
.text(function(d) { return d.name; })
.classed('selected', function(d) { return d._selected; })
function update() {
ul.selectAll('li')
.classed('selected', function(d) { return d._selected; })
table.selectAll('tr')
.classed('selected', function(d) { return d._selected; })
svg.selectAll('text')
.classed('selected', function(d) { return d._selected; })
}
d3.selectable(ul, li, update);
d3.selectable(table, tr, update);
d3.selectable(svg, circle, update);
</script>
@jensnilsson
Copy link

Thanks for a great script.

It did not work when updating to v4 of d3.
I fixed that and also added support for cmd on mac.

https://github.com/jensnilsson/d3-selectable-v4-and-mac

// Jens Nilsson

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment