Skip to content

scrollable dropdown menus #1214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2ec79e7
updatemenus: don't setTranslate button container
n-riesco Nov 25, 2016
5a4ecf3
updatemenus: add scroll bars if needed
n-riesco Nov 24, 2016
66e503c
updatemenus: Fix image test failure
n-riesco Jan 26, 2017
9a0aa64
Merge branch 'master' into pr-20161121-scrollable-dropdown-menus
n-riesco Jan 26, 2017
8df8fd6
updatemenus: Update copyright notice
n-riesco Jan 26, 2017
7057bc7
updatemenus: make ScrollBox#setTranslate public
n-riesco Jan 30, 2017
8d1f333
updatemenus: fix positioning of scrollbars
n-riesco Jan 30, 2017
563009b
updatemenus: center dropmenu on active option
n-riesco Jan 30, 2017
ef5210e
updatemenus: hide scrollbar if header clicked
n-riesco Jan 30, 2017
f17773f
updatemenu: ScrollBox#setTranslate to take pixels
n-riesco Jan 31, 2017
9e3ac1b
updatemenus: fix smooth dropdown folding
n-riesco Jan 31, 2017
13508da
updatemenus: handle mouse wheel events
n-riesco Jan 31, 2017
8ab3ebe
updatemenus: refactor where scrollbox is created
n-riesco Feb 1, 2017
4975401
updatemenus: add <rect> background to scrollbox
n-riesco Feb 1, 2017
3b17d1d
updatemenus: remove un/foldDropdownMenu
n-riesco Feb 2, 2017
b155a8a
updatemenu: fix computation of scrollbox size
n-riesco Feb 2, 2017
109f284
updatemenus: fix positioning of scrollbox
n-riesco Feb 3, 2017
e7c3ae3
Lib: Fix regexp in getTranslate
n-riesco Feb 3, 2017
89c615d
updatemenus: test scrollbox
n-riesco Feb 3, 2017
a554ea6
Merge branch 'master' into PR #1214
n-riesco Feb 3, 2017
79f1107
Drawing: test setTranslate works with negatives
n-riesco Feb 3, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/drawing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ drawing.setClipUrl = function(s, localId) {
drawing.getTranslate = function(element) {
// Note the separator [^\d] between x and y in this regex
// We generally use ',' but IE will convert it to ' '
var re = /.*\btranslate\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/,
var re = /.*\btranslate\((-?\d*\.?\d*)[^-\d]*(-?\d*\.?\d*)[^\d].*/,
getter = element.attr ? 'attr' : 'getAttribute',
transform = element[getter]('transform') || '';

Expand Down
186 changes: 142 additions & 44 deletions src/components/updatemenus/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var svgTextUtils = require('../../lib/svg_text_utils');
var anchorUtils = require('../legend/anchor_utils');

var constants = require('./constants');
var ScrollBox = require('./scrollbox');

module.exports = function draw(gd) {
var fullLayout = gd._fullLayout,
Expand Down Expand Up @@ -82,16 +83,23 @@ module.exports = function draw(gd) {
.classed(constants.dropdownButtonGroupClassName, true)
.style('pointer-events', 'all');

// whenever we add new menu, attach 'state' variable to node
// to keep track of the active menu ('-1' means no menu is active)
// and remove all dropped buttons (if any)
// find dimensions before plotting anything (this mutates menuOpts)
for(var i = 0; i < menuData.length; i++) {
var menuOpts = menuData[i];
findDimensions(gd, menuOpts);
}

// setup scrollbox
var scrollBoxId = 'updatemenus' + fullLayout._uid,
scrollBox = new ScrollBox(gd, gButton, scrollBoxId);

// remove exiting header, remove dropped buttons and reset margins
if(headerGroups.enter().size()) {
gButton
.call(removeAllButtons)
.attr(constants.menuIndexAttrName, '-1');
}

// remove exiting header, remove dropped buttons and reset margins
headerGroups.exit().each(function(menuOpts) {
d3.select(this).remove();

Expand All @@ -102,30 +110,24 @@ module.exports = function draw(gd) {
Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index);
});

// find dimensions before plotting anything (this mutates menuOpts)
for(var i = 0; i < menuData.length; i++) {
var menuOpts = menuData[i];
findDimensions(gd, menuOpts);
}

// draw headers!
headerGroups.each(function(menuOpts) {
var gHeader = d3.select(this);

var _gButton = menuOpts.type === 'dropdown' ? gButton : null;
Plots.manageCommandObserver(gd, menuOpts, menuOpts.buttons, function(data) {
setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, data.index, true);
setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, scrollBox, data.index, true);
});

if(menuOpts.type === 'dropdown') {
drawHeader(gd, gHeader, gButton, menuOpts);
drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);

// update buttons if they are dropped
if(areMenuButtonsDropped(gButton, menuOpts)) {
drawButtons(gd, gHeader, gButton, menuOpts);
// if this menu is active, update the dropdown container
if(isActive(gButton, menuOpts)) {
drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
}
} else {
drawButtons(gd, gHeader, null, menuOpts);
drawButtons(gd, gHeader, null, null, menuOpts);
}

});
Expand All @@ -150,18 +152,40 @@ function makeMenuData(fullLayout) {

// Note that '_index' is set at the default step,
// it corresponds to the menu index in the user layout update menu container.
// This is a more 'consistent' field than e.g. the index in the menuData.
function keyFunction(opts) {
return opts._index;
// Because a menu can b set invisible,
// this is a more 'consistent' field than the index in the menuData.
function keyFunction(menuOpts) {
return menuOpts._index;
}

function areMenuButtonsDropped(gButton, menuOpts) {
var droppedIndex = +gButton.attr(constants.menuIndexAttrName);
function isFolded(gButton) {
return +gButton.attr(constants.menuIndexAttrName) === -1;
}

return droppedIndex === menuOpts._index;
function isActive(gButton, menuOpts) {
return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index;
}

function drawHeader(gd, gHeader, gButton, menuOpts) {
function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex, isSilentUpdate) {
// update 'active' attribute in menuOpts
menuOpts._input.active = menuOpts.active = buttonIndex;

if(menuOpts.type === 'buttons') {
drawButtons(gd, gHeader, null, null, menuOpts);
}
else if(menuOpts.type === 'dropdown') {
// fold up buttons and redraw header
gButton.attr(constants.menuIndexAttrName, '-1');

drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);

if(!isSilentUpdate) {
drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
}
}
}

function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
var header = gHeader.selectAll('g.' + constants.headerClassName)
.data([0]);

Expand Down Expand Up @@ -200,14 +224,17 @@ function drawHeader(gd, gHeader, gButton, menuOpts) {
header.on('click', function() {
gButton.call(removeAllButtons);

// if clicked index is same as dropped index => fold
// otherwise => drop buttons associated with header

// if this menu is active, fold the dropdown container
// otherwise, make this menu active
gButton.attr(
constants.menuIndexAttrName,
areMenuButtonsDropped(gButton, menuOpts) ? '-1' : String(menuOpts._index)
isActive(gButton, menuOpts) ?
-1 :
String(menuOpts._index)
);

drawButtons(gd, gHeader, gButton, menuOpts);
drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
});

header.on('mouseover', function() {
Expand All @@ -222,7 +249,7 @@ function drawHeader(gd, gHeader, gButton, menuOpts) {
Drawing.setTranslate(gHeader, menuOpts.lx, menuOpts.ly);
}

function drawButtons(gd, gHeader, gButton, menuOpts) {
function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) {
// If this is a set of buttons, set pointer events = all since we play
// some minor games with which container is which in order to simplify
// the drawing of *either* buttons or menus
Expand All @@ -231,7 +258,7 @@ function drawButtons(gd, gHeader, gButton, menuOpts) {
gButton.attr('pointer-events', 'all');
}

var buttonData = (gButton.attr(constants.menuIndexAttrName) !== '-1' || menuOpts.type === 'buttons') ?
var buttonData = (!isFolded(gButton) || menuOpts.type === 'buttons') ?
menuOpts.buttons :
[];

Expand All @@ -257,7 +284,6 @@ function drawButtons(gd, gHeader, gButton, menuOpts) {
exit.remove();
}


var x0 = 0;
var y0 = 0;

Expand All @@ -280,13 +306,18 @@ function drawButtons(gd, gHeader, gButton, menuOpts) {
}

var posOpts = {
x: x0 + menuOpts.pad.l,
y: y0 + menuOpts.pad.t,
x: menuOpts.lx + x0 + menuOpts.pad.l,
y: menuOpts.ly + y0 + menuOpts.pad.t,
yPad: constants.gapButton,
xPad: constants.gapButton,
index: 0,
};

var scrollBoxPosition = {
l: posOpts.x + menuOpts.borderwidth,
t: posOpts.y + menuOpts.borderwidth
};

buttons.each(function(buttonOpts, buttonIndex) {
var button = d3.select(this);

Expand All @@ -295,7 +326,10 @@ function drawButtons(gd, gHeader, gButton, menuOpts) {
.call(setItemPosition, menuOpts, posOpts);

button.on('click', function() {
setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex);
// skip `dragend` events
if(d3.event.defaultPrevented) return;

setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex);

Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args);

Expand All @@ -314,23 +348,87 @@ function drawButtons(gd, gHeader, gButton, menuOpts) {

buttons.call(styleButtons, menuOpts);

// translate button group
Drawing.setTranslate(gButton, menuOpts.lx, menuOpts.ly);
if(isVertical) {
scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth);
scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t;
}
else {
scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l;
scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight);
}

scrollBoxPosition.direction = menuOpts.direction;

if(scrollBox) {
if(buttons.size()) {
drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, scrollBoxPosition);
}
else {
hideScrollBox(scrollBox);
}
}
}

function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex, isSilentUpdate) {
// update 'active' attribute in menuOpts
menuOpts._input.active = menuOpts.active = buttonIndex;
function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) {
// enable the scrollbox
var direction = menuOpts.direction,
isVertical = (direction === 'up' || direction === 'down');

if(menuOpts.type === 'dropdown') {
// fold up buttons and redraw header
gButton.attr(constants.menuIndexAttrName, '-1');
var active = menuOpts.active,
translateX, translateY,
i;
if(isVertical) {
translateY = 0;
for(i = 0; i < active; i++) {
translateY += menuOpts.heights[i] + constants.gapButton;
}
}
else {
translateX = 0;
for(i = 0; i < active; i++) {
translateX += menuOpts.widths[i] + constants.gapButton;
}
}

drawHeader(gd, gHeader, gButton, menuOpts);
scrollBox.enable(position, translateX, translateY);

if(scrollBox.hbar) {
scrollBox.hbar
.attr('opacity', '0')
.transition()
.attr('opacity', '1');
}

if(!isSilentUpdate || menuOpts.type === 'buttons') {
drawButtons(gd, gHeader, gButton, menuOpts);
if(scrollBox.vbar) {
scrollBox.vbar
.attr('opacity', '0')
.transition()
.attr('opacity', '1');
}
}

function hideScrollBox(scrollBox) {
var hasHBar = !!scrollBox.hbar,
hasVBar = !!scrollBox.vbar;

if(hasHBar) {
scrollBox.hbar
.transition()
.attr('opacity', '0')
.each('end', function() {
hasHBar = false;
if(!hasVBar) scrollBox.disable();
});
}

if(hasVBar) {
scrollBox.vbar
.transition()
.attr('opacity', '0')
.each('end', function() {
hasVBar = false;
if(!hasHBar) scrollBox.disable();
});
}
}

Expand Down
Loading