blob: 3d384e268720ba75196c1ebe3f0b492c39706e39 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var mercury = require('mercury');
var insertCss = require('insert-css');
var d3 = require('d3');
var namespaceService = require('../../../../services/namespace/service');
var browseRoute = require('../../../../routes/browse');
var getServiceIcon = require('../../get-service-icon');
var log = require('../../../../lib/log')(
'components:browse:items:visualize-view'
);
var css = require('./index.css');
var h = mercury.h;
module.exports = create;
module.exports.render = render;
module.exports.clearCache = clearCache;
var DURATION = 500; // d3 animation duration
var STAGGER = 5; // mS delay for each node
var MIN_ZOOM = 0.5; // minimum zoom allowed
var MAX_ZOOM = 20; // maximum zoom allowed
var SYMBOL_STROKE_COLOR = '#00838F';
var SELECTED_COLOR = '#E65100'; // color of selected node
var ZOOM_INC = 0.06; // zoom factor per animation frame
var PAN_INC = 3; // pan per animation frame
var ROT_INC = 0.5; // rotation per animation frame
var RELATIVE_ROOT = '<Home>';
// keyboard key codes
var KEY_PLUS = 187; // + (zoom in)
var KEY_MINUS = 189; // - (zoom out)
var KEY_PAGEUP = 33; // (rotate CCW)
var KEY_PAGEDOWN = 34; // (rotate CW)
var KEY_LEFT = 37; // left arrow
var KEY_UP = 38; // up arrow
var KEY_RIGHT = 39; // right arrow
var KEY_DOWN = 40; // down arrow
var KEY_SPACE = 32; // (expand node)
var KEY_RETURN = 13; // (expand tree)
var KEY_HOME = 36; // (center root)
var KEY_END = 35; // (center selection)
function create() {}
var widget;
function clearCache() {
widget = null;
}
function render(itemsState, browseState, browseEvents, navEvents) {
insertCss(css);
if (!widget) {
widget = createWidget(browseState, browseEvents, navEvents);
} else {
widget.update(browseState, browseEvents);
}
return [
widget,
h('div.vismenu', { // visualization menu
}, [
h('paper-fab.zoom', {
attributes: {
mini: true,
raised: true,
icon: 'add',
title: 'Zoom In (+)',
'aria-label': 'zoom in'
},
'ev-down': widget.polydown.bind(undefined, KEY_PLUS),
'ev-up': widget.polyup.bind(undefined, KEY_PLUS)
}),
h('paper-fab.zoom', {
attributes: {
mini: true,
icon: 'remove',
title: 'Zoom Out (\u2212)',
'aria-label': 'zoom out'
},
'ev-down': widget.polydown.bind(undefined, KEY_MINUS),
'ev-up': widget.polyup.bind(undefined, KEY_MINUS),
}),
h('paper-fab.rotate', {
attributes: {
mini: true,
icon: 'image:rotate-left',
title: 'Rotate CCW (Page Up)',
'aria-label': 'rotate counterclockwise'
},
'ev-down': widget.polydown.bind(undefined, KEY_PAGEUP),
'ev-up': widget.polyup.bind(undefined, KEY_PAGEUP)
}),
h('paper-fab.rotate', {
attributes: {
mini: true,
icon: 'image:rotate-right',
title: 'Rotate CW (Page Down)',
'aria-label': 'rotate clockwise'
},
'ev-down': widget.polydown.bind(undefined, KEY_PAGEDOWN),
'ev-up': widget.polyup.bind(undefined, KEY_PAGEDOWN)
}),
h('paper-fab.expand', {
attributes: {
mini: true,
icon: 'unfold-more', // 'expand-less',
title: 'Load +1 Level (space bar)',
'aria-label': 'load +1 level'
},
'ev-click': widget.menu.bind(undefined, KEY_SPACE, false)
})
]),
h('paper-shadow.contextmenu', { // context menu
attributes: {
z: 3 // height above background
}
}, [ // context menu
h('paper-item', {
'ev-mouseup': widget.menu.bind(undefined, KEY_RETURN, false),
'ev-mousedown': widget.menu.bind(undefined, KEY_RETURN, false)
}, [h('div.ecnode', 'Expand Node'), h('div.ksc', 'Return')]),
h('paper-item', {
'ev-mouseup': widget.menu.bind(undefined, KEY_RETURN, true),
'ev-mousedown': widget.menu.bind(undefined, KEY_RETURN, true)
}, [h('div', 'Show Loaded'), h('div.ksc', '\u21E7 Return')]),
h('paper-item', {
'ev-mouseup': widget.menu.bind(undefined, KEY_SPACE, false),
'ev-mousedown': widget.menu.bind(undefined, KEY_SPACE, false)
}, [h('div', 'Load +1 Level'), h('div.ksc', 'space bar')]),
h('paper-item', {
'ev-mouseup': widget.menu.bind(undefined, KEY_SPACE, true),
'ev-mousedown': widget.menu.bind(undefined, KEY_SPACE, true)
}, [h('div', 'Browse Into'), h('div.ksc', '\u21E7 space bar')]),
h('paper-item', {
'ev-mouseup': widget.menu.bind(undefined, KEY_END, false),
'ev-mousedown': widget.menu.bind(undefined, KEY_END, false)
}, [h('div', 'Center Selected'), h('div.ksc', 'End')]),
h('paper-item', {
'ev-mouseup': widget.menu.bind(undefined, KEY_HOME, false),
'ev-mousedown': widget.menu.bind(undefined, KEY_HOME, false)
}, [h('div', 'Center Root'), h('div.ksc', 'Home')])
])
];
}
function createWidget(browseState, browseEvents, navEvents) {
var networkElem; // DOM element for visualization
var selNode; // currently selected node
var selectItem; // function to select item in app
var width, height; // size of the visualization diagram
var curX, curY, curR, curZ; // transforms (x, y, rotate, zoom)
var modZ; // modified zoom for nodes and text
var diagonal; // d3 diagonal projection for use by the node paths
var treeD3; // d3 tree layout
var svgBase, svgGroup; // svg elements
var root; // data trees
var rootIndex = {}; // index id to nodes
browseInto.browseState = browseState;
browseInto.navEvents = navEvents;
// Constructor for mercury widget for d3 element
function D3Widget(browseState, browseEvents) {
this.browseState = browseState;
this.browseEvents = browseEvents;
}
D3Widget.prototype.type = 'Widget';
D3Widget.prototype.init = function() {
if (!networkElem) {
networkElem = document.createElement('div');
networkElem.className = 'network';
networkElem.setAttribute('tabindex', 0); // allow focus
selectItem = this.browseEvents.selectItem.bind(this.browseEvents);
requestAnimationFrame(initD3);
}
// wrap in a new element, needed for Mercury vdom to patch properly.
var wrapper = document.createElement('div');
wrapper.className = 'networkParent';
wrapper.appendChild(networkElem);
requestAnimationFrame(this.updateRoot.bind(this));
return wrapper;
};
// Keep track of previous namespace that was browsed to so we can
// know when navigating to a different namespace happens.
var previousNamespace;
D3Widget.prototype.polyup = polyup;
D3Widget.prototype.polydown = polydown;
D3Widget.prototype.menu = menu;
D3Widget.prototype.update = function(browseState, browseEvents) {
// handle changed selection
selNode = rootIndex[browseState.selectedItemName] || selNode;
this.browseState = browseState;
this.browseEvents = browseEvents;
// check to see if window was resized while we were away
if (width !== networkElem.offsetWidth) {
resize();
}
this.updateRoot();
networkElem.focus();
};
// build new data tree
D3Widget.prototype.updateRoot = function() {
var rootNodeId = this.browseState.namespace;
if (previousNamespace !== rootNodeId) {
previousNamespace = rootNodeId;
// parse root id
var parts = namespaceService.util.parseName(rootNodeId);
var isRooted = namespaceService.util.isRooted(rootNodeId);
var buildId = '';
if (!isRooted) {
parts.unshift('');
} // create <Home> node
var parent; // used to connect each new node to their parent
parts.forEach(function(v) {
buildId += (buildId.length > 0 || isRooted ? '/' : '') + v;
var nn = rootIndex[buildId] || {};
var isNew = (nn.id === undefined);
nn.id = buildId;
nn.name = v || RELATIVE_ROOT;
nn.isLeaf = false;
nn.hasServer = true; // assume true, fix later
nn.hasMountPoint = true; // assume true, fix later
if (parent !== undefined) {
nn.parent = parent;
if (parent.children === undefined && parent._children === undefined) {
parent._children = [nn]; // initially hidden
}
}
if (isNew) {
rootIndex[buildId] = nn;
}
parent = nn;
});
root = parent; // new root
if (selNode === undefined) {
selectNode(root);
}
loadItem(root); // load rest of information for this node
loadSubItems(root); // load the children
}
updateD3(root, true); // always animate
};
// initialize d3 HTML elements
function initD3() {
// size of the diagram
width = networkElem.offsetWidth;
height = networkElem.offsetHeight;
// current pan, zoom, and rotation
curX = width / 2; // center
curY = height / 2;
curZ = modZ = 1.0; // current zoom
curR = 270; // current rotation
// d3 diagonal projection for use by the node paths
diagonal = d3.svg.diagonal.radial().
projection(function(d) {
return [d.y, d.x / 180 * Math.PI];
});
// d3 tree layout
treeD3 = d3.layout.tree().
// circular coordinates to fit in window
// 120 is to allow space for text strings
size([360, Math.min(width, height) / 2 - 120]).
// space between nodes, depends on if they have same parent
// dividing by a.depth is for radial coordinates
separation(function(a, b) {
return (a.parent === b.parent ? 1 : 2) / (a.depth + 1);
});
// define the svgBase, attaching a class for styling and the zoomListener
svgBase = d3.select('.network').append('svg').
attr('width', width).
attr('height', height).
attr('class', 'overlay').
on('mousedown', mousedown);
// Group which holds all nodes and manages pan, zoom, rotate
svgGroup = svgBase.append('g').
attr('transform', 'translate(' + curX + ',' + curY + ')');
networkElem.focus();
d3.select('.network'). // set up document events
on('wheel', wheel). // zoom, rotate
on('keydown', keydown).
on('keyup', keyup).
on('mouseover', function() {
networkElem.focus();
});
d3.select(window).on('resize', resize);
}
// draw tree using d3js
// subroot - source node of the update
// doAni - whether to do a transition animation
function updateD3(subroot, doAni) {
// length of d3 animation
var duration = (d3.event && d3.event.altKey ? DURATION * 4 : DURATION);
// Compute the new tree layout.
var d3nodes = treeD3.nodes(root);
var d3links = treeD3.links(d3nodes);
// Update the view
var view = doAni ? svgGroup.transition().duration(duration) : svgGroup;
view.attr('transform',
'rotate(' + curR + ' ' + curX + ' ' + curY +
')translate(' + curX + ' ' + curY +
')scale(' + curZ + ')');
var gnode = svgGroup.selectAll('g.node').
data(d3nodes, function(d) {
return d.id;
});
// Enter any new nodes at the parent's previous position
var nodeEnter = gnode.enter().insert('g', ':first-child').
attr('class', 'node').
attr('opacity', 0).
attr('transform', 'rotate(' + (subroot.x - 90) +
')translate(' + subroot.y + ')').
on('click', click).on('dblclick', dblclick).
on('contextmenu', showContextMenu);
nodeEnter.append('title').text(function(d) {
return getServiceIcon(d).title;
});
nodeEnter.filter(function(d) { // Mount Point
return d.hasMountPoint;
}).append('path').attr('class', 'mountpointicon').
attr('d', d3.svg.symbol().type('square'));
nodeEnter.filter(function(d) { // Server
return d.hasServer;
}).append('path').attr('class', 'servericon').
attr('d', d3.svg.symbol().type('circle').size(60));
nodeEnter.append('text').
text(function(d) {
return d.name;
}).
attr('transform', ((subroot.x + curR) % 360 <= 180 ?
'rotate(-7)translate(8)scale(' : 'rotate(187)translate(-8)scale('
) + modZ + ')');
// update existing graph nodes
// set path style for selection, loading, and scale
gnode.select('path').
attr('transform', 'scale(' + modZ + ')').
classed('loading', function(d) {
return d.loading;
}).
attr('stroke', function(d) {
return d === selNode ? SELECTED_COLOR : SYMBOL_STROKE_COLOR;
}).
attr('stroke-width', function(d) {
return d === selNode ? 3 : 2;
});
gnode.select('title').text(function(d) {
return getServiceIcon(d).title;
});
// remove icons if assumed true but turn out to be false
gnode.select('path.servericon').filter(function(d) {
return !d.hasServer;
}).remove();
gnode.select('path.mountpointicon').filter(function(d) {
return !d.hasMountPoint;
}).remove();
gnode.select('text').
attr('text-anchor', function(d) {
return (d.x + curR) % 360 <= 180 ? 'start' : 'end';
}).
attr('transform', function(d) {
return ((d.x + curR) % 360 <= 180 ?
'rotate(-7)translate(8)scale(' :
'rotate(187)translate(-8)scale('
) + modZ + ')';
}).
attr('fill', function(d) {
return d === selNode ? SELECTED_COLOR : 'black';
}).
attr('dy', '5px');
var nodeUpdate = (doAni ?
gnode.transition().duration(duration).delay(function(d, i) {
return i * STAGGER + Math.max(0, d.depth - selNode.depth);
}) : gnode);
nodeUpdate.attr('transform', function(d) {
return 'rotate(' + (d.x - 90) + ')translate(' + d.y + ')';
}).style('opacity', 1);
nodeUpdate.select('path').
attr('transform', 'scale(' + modZ + ')');
// Transition exiting nodes to the parent's new position and remove
var nodeExit = doAni ? gnode.exit().transition().duration(duration).
delay(function(d, i) {
return i * STAGGER;
}): gnode.exit();
nodeExit.attr('transform', function(d) {
return 'rotate(' + (subroot.x - 90) + ')translate(' + subroot.y + ')';
}).
attr('opacity', 0).
remove();
nodeExit.select('.node path').attr('transform', 'scale(0)');
nodeExit.select('.node text').style('fill-opacity', 0);
// Update the links…
var glink = svgGroup.selectAll('path.link').
data(d3links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position
glink.enter().insert('path', 'g').
attr('class', 'link').
attr('d', function() {
var o = {
x: subroot.x,
y: subroot.y
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position
(doAni ? glink.transition().duration(duration).delay(function(d, i) {
return i * STAGGER + Math.max(0, d.source.depth - selNode.depth);
}) : glink).
attr('d', diagonal);
// Transition exiting nodes to the parent's new position
(doAni ? glink.exit().transition().duration(duration) : glink.exit()).
attr('d', function() {
var o = {
x: subroot.x,
y: subroot.y
};
return diagonal({
source: o,
target: o
});
}).
remove(); // remove edge at end of animation
} // end updateD3
// find place to insert new node in children
var bisectfun = d3.bisector(function(d) {
return d.name;
}).right;
// create node or merge new item data into it
function mergeNode(item, parent) {
var nn = rootIndex[item.objectName] || {};
var isNew = nn.id === undefined; // not found in rootIndex
nn.id = item.objectName;
nn.name = item.mountedName || RELATIVE_ROOT;
nn.parent = parent || nn.parent;
nn.isLeaf = item.isLeaf;
nn.hasMountPoint = item.hasMountPoint;
nn.hasServer = item.hasServer;
if (isNew && parent !== undefined) { // insert node in proper place
rootIndex[nn.id] = nn;
if (parent.children === undefined) {
parent.children = [nn];
} else {
parent.children.splice(bisectfun(parent.children, nn.name), 0, nn);
}
}
return isNew; // need to animate it in
} // end mergeNode
function loadItem(node) { // load a single item (used for root of tree)
namespaceService.getNamespaceItem(node.id).then(function(observable) {
mercury.watch(observable, updateItem);
function updateItem(item) { // currently only gets called once
mergeNode(item, node.parent); // update elsewhere
}
});
}
// load children items asynchronously
function loadSubItems(node) {
if (node.subNodesLoaded) {
return;
}
var namespace = node.id;
if (node._children) {
node.children = node._children;
node._children = null;
}
node.subNodesLoaded = true;
showLoading(node, true); // node is loading
namespaceService.getChildren(namespace).then(function(resultObservable) {
var initialValues = resultObservable();
initialValues.forEach(function(item) {
batchUpdate(node, mergeNode(item, node));
});
resultObservable.events.once('end', function() {
showLoading(node, false); // node no longer loading
});
resultObservable(updatedValues);
function updatedValues(results) {
// TODO(wmleler) support removed and updated nodes for watchGlob
var item = results._diff[0][2]; // changed item from Mercury
batchUpdate(node, mergeNode(item, node));
}
}).catch(function(err) {
log.error('glob failed', err);
});
} // end loadSubItems
// batch up groups of updates to speed up transitions
var batchNode = null;
var batchId = null;
function batchUpdate(node, doAni) {
if (node !== batchNode) {
if (batchNode !== null) {
updateD3(batchNode, doAni);
}
batchNode = node;
if (batchId !== null) {
clearTimeout(batchId);
batchId = null;
}
} else {
batchId = setTimeout(function() {
updateD3(batchNode, doAni);
batchId = null;
}, DURATION);
}
}
function selectNode(node) { // highlight node and show details
if (node === selNode) {
return;
}
selNode = node;
selectItem({
name: node.id
}); // notify rest of app
}
function browseInto(node) { // make this node the root
var browseUrl = browseRoute.createUrl(browseInto.browseState, {
namespace: node.id
});
browseInto.navEvents.navigate({
path: browseUrl
});
}
// set view with no animation
function setview() {
svgGroup.attr('transform',
'rotate(' + curR + ' ' + curX + ' ' + curY +
')translate(' + curX + ' ' + curY +
')scale(' + curZ + ')');
svgGroup.selectAll('text').
attr('text-anchor', function(d) {
return (d.x + curR) % 360 <= 180 ? 'start' : 'end';
}).
attr('transform', function(d) {
return ((d.x + curR) % 360 <= 180 ?
'rotate(-7)translate(8)scale(' :
'rotate(187)translate(-8)scale('
) + modZ + ')';
});
svgGroup.selectAll('.node path').
attr('transform', 'scale(' + modZ + ')').
attr('stroke-width', function(d) {
return d === selNode ? 3 : 2;
});
}
// show nodes that are loading
function showLoading(node, v) {
node.loading = v;
svgGroup.selectAll('.node path').
classed('loading', function(d) {
return d.loading;
});
}
//
// Helper functions for collapsing and expanding nodes
//
// Toggle expand / collapse
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else if (d._children && d.subNodesLoaded) {
d.children = d._children;
d._children = null;
} else {
loadSubItems(d);
}
}
function collapse(d) { // collapse one level
if (d.children) {
d._children = d.children;
d.children = null;
}
}
// expand all loaded children and descendents
function expandTree(d) {
if (d._children) {
d.children = d._children;
d._children = null;
}
if (d.children) {
d.children.forEach(expandTree);
}
}
// expand one level of tree using breadth first search
function expand1Level(d) {
var q = [d]; // non-recursive using queue
var cn;
var done = null;
while (q.length > 0) {
cn = q.shift();
if (done !== null && done < cn.depth) {
return;
}
if (cn._children) {
done = cn.depth;
cn.children = cn._children;
cn._children = null;
cn.children.forEach(collapse);
} else if (!(cn.isLeaf || cn.subNodesLoaded)) {
done = cn.depth;
loadSubItems(cn);
}
if (cn.children) {
q = q.concat(cn.children);
}
}
// no nodes to open
}
var moveX = 0,
moveY = 0,
moveZ = 0,
moveR = 0; // animations
var keysdown = []; // which keys are currently down
var animation = null;
var aniTime = null; // time since last animation frame
// update animation frame
function frame(frametime) {
var diff = aniTime ? (frametime - aniTime) / 16 : 0;
aniTime = frametime;
var dz = Math.pow(1.2, diff * moveZ);
var newZ = limitZ(curZ * dz);
dz = newZ / curZ;
curZ = newZ;
modZ = Math.pow(1.1, -curZ); // limit text and node size as scale increases
curX += diff * moveX - (width / 2 - curX) * (dz - 1);
curY += diff * moveY - (height / 2 - curY) * (dz - 1);
curR = limitR(curR + diff * moveR);
setview();
animation = requestAnimationFrame(frame);
}
// enforce zoom extent
function limitZ(z) {
return Math.max(Math.min(z, MAX_ZOOM), MIN_ZOOM);
}
// keep rotation between 0 and 360
function limitR(r) {
return (r + 360) % 360;
}
//
// d3 event handlers
//
function resize() { // window resize
if (networkElem.offsetWidth === 0) {
return;
}
var oldwidth = width;
var oldheight = height;
width = networkElem.offsetWidth;
height = networkElem.offsetHeight;
treeD3.size([360, Math.min(width, height) / 2 - 120]);
svgBase.attr('width', width).attr('height', height);
curX += (width - oldwidth) / 2;
curY += (height - oldheight) / 2;
svgGroup.attr('transform',
'rotate(' + curR + ' ' + curX + ' ' + curY +
')translate(' + curX + ' ' + curY +
')scale(' + curZ + ')');
updateD3(root, false);
}
function click(d) { // Select node
if (d3.event.defaultPrevented || d === selNode) {
return;
}
selectNode(d);
updateD3(d, false);
d3.event.preventDefault();
}
function dblclick(d) { // Toggle children of node
if (d3.event.defaultPrevented) {
return;
} // click suppressed
d3.event.preventDefault();
if (d3.event.shiftKey) {
expand1Level(d);
} else {
toggle(d);
}
updateD3(d, true);
}
var startposX, startposY; // initial position on mouse button down for pan
function mousedown() { // pan action from mouse drag
if (d3.event.which !== 1) {
return;
} // ingore other mouse buttons
startposX = curX - d3.event.clientX;
startposY = curY - d3.event.clientY;
d3.select(document).on('mousemove', mousemove, true);
d3.select(document).on('mouseup', mouseup, true);
networkElem.focus();
d3.event.preventDefault();
}
function mousemove() { // drag
curX = startposX + d3.event.clientX;
curY = startposY + d3.event.clientY;
setview();
d3.event.preventDefault();
}
function mouseup() { // cleanup
d3.select(document).on('mousemove', null);
d3.select(document).on('mouseup', null);
}
function wheel() { // mousewheel (including left-right)
var dz, newZ;
var slow = (d3.event && d3.event.altKey) ? 0.25 : 1;
if (d3.event.wheelDeltaY !== 0) { // up-down = zoom
dz = Math.pow(1.2, d3.event.wheelDeltaY * 0.001 * slow);
newZ = limitZ(curZ * dz);
dz = newZ / curZ;
curZ = newZ;
// zoom around mouse position
curX -= (d3.event.clientX - curX) * (dz - 1);
curY -= (d3.event.clientY - curY) * (dz - 1);
setview();
}
if (d3.event.wheelDeltaX !== 0) { // left-right = rotate
curR = limitR(curR + d3.event.wheelDeltaX * 0.01 * slow);
updateD3(root, false);
}
}
function polydown(key, evt) { // polymer ev-mousedown event
actionDown(key, evt.shiftKey, evt.altKey);
}
function polyup(key, evt) { // polymer ev-mouseup event
actionUp(key);
}
function menu(key, shift, evt) { // context menu selection event
if (evt === undefined) { // shiftkey not supplied
evt = shift;
shift = evt.shiftKey;
}
actionDown(key, shift, evt.altKey);
networkElem.focus();
}
function keydown() { // d3 keydown event
var evt = d3.event;
if (evt.repeat) {
return;
}
actionDown(evt.which, evt.shiftKey, evt.altKey);
}
function keyup() { // d3 keyup event
var evt = d3.event;
actionUp(evt.which);
}
// right click, show context menu and select this node
function showContextMenu(d) {
d3.event.preventDefault();
d3.select('.ecnode').text(
(d.children ? 'Collapse ' : 'Expand ') + 'Node');
var cmenu = d3.select('.contextmenu');
cmenu.style({
left: Math.min(d3.event.offsetX + 3,
width - cmenu.style('width').replace('px', '') - 5) + 'px',
top: (d3.event.offsetY + 8) + 'px',
display: 'block'
});
var doc = d3.select(document);
doc.on('mousedown.cm', hideContextMenu, true);
setTimeout(function() {
doc.on('mouseup.cm', hideContextMenu, true);
}, 500);
selectNode(d);
}
function hideContextMenu() {
var doc = d3.select(document);
d3.select('.contextmenu').style('display', 'none');
doc.on('mouseup.cm', null);
doc.on('mousedown.cm', null);
networkElem.focus();
}
// Event actions
// Almost all UI actions pass through here,
// even if they are not originally generated from the keyboard
// There are two types of actions:
// * Press-and-Hold actions perform some action while they are pressed,
// until they are released, like pan, zoom, and rotate. These actions end
// with "break", so the key can be saved, and actionUp can stop the action.
// * Click actions mostly happen on keydown, like toggling children.
function actionDown(key, shift, alt) {
var parch; // parent's children
var slow = alt ? 0.25 : 1;
if (keysdown.indexOf(key) >= 0) {
return;
} // defeat auto repeat
switch (key) {
case KEY_PLUS: // zoom in
moveZ = ZOOM_INC * slow;
break;
case KEY_MINUS: // zoom out
moveZ = -ZOOM_INC * slow;
break;
case KEY_PAGEUP: // rotate counterclockwise
moveR = -ROT_INC * slow;
break;
case KEY_PAGEDOWN: // rotate clockwise
moveR = ROT_INC * slow;
break;
case KEY_LEFT:
if (shift) { // move selection to parent
if (!selNode) {
selectNode(root);
} else if (selNode.parent) {
selectNode(selNode.parent);
updateD3(selNode, false);
}
return;
}
moveX = -PAN_INC * slow; // pan left
break;
case KEY_UP:
if (shift) { // move selection to previous child
if (!selNode) {
selectNode(root);
} else if (selNode.parent) {
parch = selNode.parent.children;
selectNode(parch[(parch.indexOf(selNode) +
parch.length - 1) % parch.length]);
updateD3(selNode, false);
}
return;
}
moveY = -PAN_INC * slow; // pan up
break;
case KEY_RIGHT:
if (shift) { // move selection to first/last child
if (!selNode) {
selectNode(root);
} else {
if (selNode.children && selNode.children.length > 0) {
selectNode(selNode.children[0]);
updateD3(selNode, false);
}
}
return;
}
moveX = PAN_INC * slow; // pan right
break;
case KEY_DOWN:
if (shift) { // move selection to next child
if (!selNode) {
selectNode(root);
} else if (selNode.parent) {
parch = selNode.parent.children;
selectNode(parch[(parch.indexOf(selNode) + 1) % parch.length]);
updateD3(selNode, false);
}
return;
}
moveY = PAN_INC * slow; // pan down
break;
case KEY_RETURN:
if (!selNode) {
selectNode(root);
}
if (shift) { // show loaded
expandTree(selNode);
loadSubItems(selNode);
} else {
toggle(selNode); // expand/collapse node
}
updateD3(selNode, true);
return;
case KEY_SPACE:
if (!selNode) {
selectNode(root);
}
if (shift) { // browse into
browseInto(selNode);
} else { // load +1 level
expand1Level(selNode);
updateD3(selNode, true);
}
return;
case KEY_HOME: // reset transform
curX = width / 2;
curY = height / 2;
curR = limitR(90 - root.x);
curZ = 1;
updateD3(root, true);
return;
case KEY_END: // zoom to selection
if (!selNode) {
return;
}
curX = width / 2 - selNode.y * curZ;
curY = height / 2;
curR = limitR(90 - selNode.x);
updateD3(selNode, true);
return;
default:
return; // ignore other keys
}
keysdown.push(key);
// start animation if anything happening
if (keysdown.length > 0 && animation === null) {
animation = requestAnimationFrame(frame);
}
}
function actionUp(key) {
var pos = keysdown.indexOf(key);
if (pos < 0) {
return;
}
switch (key) {
case KEY_PLUS: // - = zoom out
case KEY_MINUS: // + = zoom in
moveZ = 0;
break;
case KEY_PAGEUP: // page up = rotate CCW
case KEY_PAGEDOWN: // page down = rotate CW
moveR = 0;
break;
case KEY_LEFT: // left arrow
case KEY_RIGHT: // right arrow
moveX = 0;
break;
case KEY_UP: // up arrow
case KEY_DOWN: // down arrow
moveY = 0;
break;
}
keysdown.splice(pos, 1); // remove key
if (keysdown.length > 0 || animation === null) {
return;
}
cancelAnimationFrame(animation);
animation = aniTime = null;
networkElem.focus();
}
return new D3Widget(browseState, browseEvents);
}