Skip to content

Commit 3fad104

Browse files
authored
Merge pull request #1214 from n-riesco/pr-20161121-scrollable-dropdown-menus
scrollable dropdown menus
2 parents dc76217 + 79f1107 commit 3fad104

File tree

6 files changed

+1054
-46
lines changed

6 files changed

+1054
-46
lines changed

src/components/drawing/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ drawing.setClipUrl = function(s, localId) {
580580
drawing.getTranslate = function(element) {
581581
// Note the separator [^\d] between x and y in this regex
582582
// We generally use ',' but IE will convert it to ' '
583-
var re = /.*\btranslate\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/,
583+
var re = /.*\btranslate\((-?\d*\.?\d*)[^-\d]*(-?\d*\.?\d*)[^\d].*/,
584584
getter = element.attr ? 'attr' : 'getAttribute',
585585
transform = element[getter]('transform') || '';
586586

src/components/updatemenus/draw.js

+142-44
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var svgTextUtils = require('../../lib/svg_text_utils');
1818
var anchorUtils = require('../legend/anchor_utils');
1919

2020
var constants = require('./constants');
21+
var ScrollBox = require('./scrollbox');
2122

2223
module.exports = function draw(gd) {
2324
var fullLayout = gd._fullLayout,
@@ -82,16 +83,23 @@ module.exports = function draw(gd) {
8283
.classed(constants.dropdownButtonGroupClassName, true)
8384
.style('pointer-events', 'all');
8485

85-
// whenever we add new menu, attach 'state' variable to node
86-
// to keep track of the active menu ('-1' means no menu is active)
87-
// and remove all dropped buttons (if any)
86+
// find dimensions before plotting anything (this mutates menuOpts)
87+
for(var i = 0; i < menuData.length; i++) {
88+
var menuOpts = menuData[i];
89+
findDimensions(gd, menuOpts);
90+
}
91+
92+
// setup scrollbox
93+
var scrollBoxId = 'updatemenus' + fullLayout._uid,
94+
scrollBox = new ScrollBox(gd, gButton, scrollBoxId);
95+
96+
// remove exiting header, remove dropped buttons and reset margins
8897
if(headerGroups.enter().size()) {
8998
gButton
9099
.call(removeAllButtons)
91100
.attr(constants.menuIndexAttrName, '-1');
92101
}
93102

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

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

105-
// find dimensions before plotting anything (this mutates menuOpts)
106-
for(var i = 0; i < menuData.length; i++) {
107-
var menuOpts = menuData[i];
108-
findDimensions(gd, menuOpts);
109-
}
110-
111113
// draw headers!
112114
headerGroups.each(function(menuOpts) {
113115
var gHeader = d3.select(this);
114116

115117
var _gButton = menuOpts.type === 'dropdown' ? gButton : null;
116118
Plots.manageCommandObserver(gd, menuOpts, menuOpts.buttons, function(data) {
117-
setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, data.index, true);
119+
setActive(gd, menuOpts, menuOpts.buttons[data.index], gHeader, _gButton, scrollBox, data.index, true);
118120
});
119121

120122
if(menuOpts.type === 'dropdown') {
121-
drawHeader(gd, gHeader, gButton, menuOpts);
123+
drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);
122124

123-
// update buttons if they are dropped
124-
if(areMenuButtonsDropped(gButton, menuOpts)) {
125-
drawButtons(gd, gHeader, gButton, menuOpts);
125+
// if this menu is active, update the dropdown container
126+
if(isActive(gButton, menuOpts)) {
127+
drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
126128
}
127129
} else {
128-
drawButtons(gd, gHeader, null, menuOpts);
130+
drawButtons(gd, gHeader, null, null, menuOpts);
129131
}
130132

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

151153
// Note that '_index' is set at the default step,
152154
// it corresponds to the menu index in the user layout update menu container.
153-
// This is a more 'consistent' field than e.g. the index in the menuData.
154-
function keyFunction(opts) {
155-
return opts._index;
155+
// Because a menu can b set invisible,
156+
// this is a more 'consistent' field than the index in the menuData.
157+
function keyFunction(menuOpts) {
158+
return menuOpts._index;
156159
}
157160

158-
function areMenuButtonsDropped(gButton, menuOpts) {
159-
var droppedIndex = +gButton.attr(constants.menuIndexAttrName);
161+
function isFolded(gButton) {
162+
return +gButton.attr(constants.menuIndexAttrName) === -1;
163+
}
160164

161-
return droppedIndex === menuOpts._index;
165+
function isActive(gButton, menuOpts) {
166+
return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index;
162167
}
163168

164-
function drawHeader(gd, gHeader, gButton, menuOpts) {
169+
function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex, isSilentUpdate) {
170+
// update 'active' attribute in menuOpts
171+
menuOpts._input.active = menuOpts.active = buttonIndex;
172+
173+
if(menuOpts.type === 'buttons') {
174+
drawButtons(gd, gHeader, null, null, menuOpts);
175+
}
176+
else if(menuOpts.type === 'dropdown') {
177+
// fold up buttons and redraw header
178+
gButton.attr(constants.menuIndexAttrName, '-1');
179+
180+
drawHeader(gd, gHeader, gButton, scrollBox, menuOpts);
181+
182+
if(!isSilentUpdate) {
183+
drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
184+
}
185+
}
186+
}
187+
188+
function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) {
165189
var header = gHeader.selectAll('g.' + constants.headerClassName)
166190
.data([0]);
167191

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

203-
// if clicked index is same as dropped index => fold
204-
// otherwise => drop buttons associated with header
227+
228+
// if this menu is active, fold the dropdown container
229+
// otherwise, make this menu active
205230
gButton.attr(
206231
constants.menuIndexAttrName,
207-
areMenuButtonsDropped(gButton, menuOpts) ? '-1' : String(menuOpts._index)
232+
isActive(gButton, menuOpts) ?
233+
-1 :
234+
String(menuOpts._index)
208235
);
209236

210-
drawButtons(gd, gHeader, gButton, menuOpts);
237+
drawButtons(gd, gHeader, gButton, scrollBox, menuOpts);
211238
});
212239

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

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

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

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

260-
261287
var x0 = 0;
262288
var y0 = 0;
263289

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

282308
var posOpts = {
283-
x: x0 + menuOpts.pad.l,
284-
y: y0 + menuOpts.pad.t,
309+
x: menuOpts.lx + x0 + menuOpts.pad.l,
310+
y: menuOpts.ly + y0 + menuOpts.pad.t,
285311
yPad: constants.gapButton,
286312
xPad: constants.gapButton,
287313
index: 0,
288314
};
289315

316+
var scrollBoxPosition = {
317+
l: posOpts.x + menuOpts.borderwidth,
318+
t: posOpts.y + menuOpts.borderwidth
319+
};
320+
290321
buttons.each(function(buttonOpts, buttonIndex) {
291322
var button = d3.select(this);
292323

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

297328
button.on('click', function() {
298-
setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex);
329+
// skip `dragend` events
330+
if(d3.event.defaultPrevented) return;
331+
332+
setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex);
299333

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

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

315349
buttons.call(styleButtons, menuOpts);
316350

317-
// translate button group
318-
Drawing.setTranslate(gButton, menuOpts.lx, menuOpts.ly);
351+
if(isVertical) {
352+
scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth);
353+
scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t;
354+
}
355+
else {
356+
scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l;
357+
scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight);
358+
}
359+
360+
scrollBoxPosition.direction = menuOpts.direction;
361+
362+
if(scrollBox) {
363+
if(buttons.size()) {
364+
drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, scrollBoxPosition);
365+
}
366+
else {
367+
hideScrollBox(scrollBox);
368+
}
369+
}
319370
}
320371

321-
function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex, isSilentUpdate) {
322-
// update 'active' attribute in menuOpts
323-
menuOpts._input.active = menuOpts.active = buttonIndex;
372+
function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) {
373+
// enable the scrollbox
374+
var direction = menuOpts.direction,
375+
isVertical = (direction === 'up' || direction === 'down');
324376

325-
if(menuOpts.type === 'dropdown') {
326-
// fold up buttons and redraw header
327-
gButton.attr(constants.menuIndexAttrName, '-1');
377+
var active = menuOpts.active,
378+
translateX, translateY,
379+
i;
380+
if(isVertical) {
381+
translateY = 0;
382+
for(i = 0; i < active; i++) {
383+
translateY += menuOpts.heights[i] + constants.gapButton;
384+
}
385+
}
386+
else {
387+
translateX = 0;
388+
for(i = 0; i < active; i++) {
389+
translateX += menuOpts.widths[i] + constants.gapButton;
390+
}
391+
}
328392

329-
drawHeader(gd, gHeader, gButton, menuOpts);
393+
scrollBox.enable(position, translateX, translateY);
394+
395+
if(scrollBox.hbar) {
396+
scrollBox.hbar
397+
.attr('opacity', '0')
398+
.transition()
399+
.attr('opacity', '1');
330400
}
331401

332-
if(!isSilentUpdate || menuOpts.type === 'buttons') {
333-
drawButtons(gd, gHeader, gButton, menuOpts);
402+
if(scrollBox.vbar) {
403+
scrollBox.vbar
404+
.attr('opacity', '0')
405+
.transition()
406+
.attr('opacity', '1');
407+
}
408+
}
409+
410+
function hideScrollBox(scrollBox) {
411+
var hasHBar = !!scrollBox.hbar,
412+
hasVBar = !!scrollBox.vbar;
413+
414+
if(hasHBar) {
415+
scrollBox.hbar
416+
.transition()
417+
.attr('opacity', '0')
418+
.each('end', function() {
419+
hasHBar = false;
420+
if(!hasVBar) scrollBox.disable();
421+
});
422+
}
423+
424+
if(hasVBar) {
425+
scrollBox.vbar
426+
.transition()
427+
.attr('opacity', '0')
428+
.each('end', function() {
429+
hasVBar = false;
430+
if(!hasHBar) scrollBox.disable();
431+
});
334432
}
335433
}
336434

0 commit comments

Comments
 (0)