From 2ec79e72811b4f1f8415e62a4fc3ae494f907337 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 25 Nov 2016 14:09:51 +0000 Subject: [PATCH 01/19] updatemenus: don't setTranslate button container * Don't `Lib.setTranslate` the button container and update the call to `Lib.setTranslate` for each button accordingly. * This change will let us use `Lib.setTranslate` on the button container to implement a scroll box. --- src/components/updatemenus/draw.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 1ef44e1294b..8a1c3860b51 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -281,8 +281,8 @@ 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, @@ -314,9 +314,6 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { }); buttons.call(styleButtons, menuOpts); - - // translate button group - Lib.setTranslate(gButton, menuOpts.lx, menuOpts.ly); } function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex, isSilentUpdate) { From 5a4ecf3c08ab9fadbf9eec869244d1b2fdc0a468 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Thu, 24 Nov 2016 19:27:49 +0000 Subject: [PATCH 02/19] updatemenus: add scroll bars if needed --- src/components/updatemenus/draw.js | 18 ++ src/components/updatemenus/scrollbox.js | 381 ++++++++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 src/components/updatemenus/scrollbox.js diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 8a1c3860b51..67308362422 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -19,6 +19,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, @@ -288,6 +289,16 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { index: 0, }; + var fullLayout = gd._fullLayout, + scrollBoxId = 'updatemenus' + fullLayout._uid + menuOpts._index, + scrollBoxPosition = { + l: menuOpts.lx + menuOpts.borderwidth + x0 + menuOpts.pad.l, + t: menuOpts.ly + menuOpts.borderwidth + y0 + menuOpts.pad.t, + w: Math.max(menuOpts.openWidth, menuOpts.headerWidth), + h: menuOpts.openHeight + }, + scrollBox = new ScrollBox(gd, gButton, scrollBoxPosition, scrollBoxId); + buttons.each(function(buttonOpts, buttonIndex) { var button = d3.select(this); @@ -296,10 +307,15 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { .call(setItemPosition, menuOpts, posOpts); button.on('click', function() { + // skip `dragend` events + if (d3.event.defaultPrevented) return; + setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex); Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args); + scrollBox.disable(); + gd.emit('plotly_buttonclicked', {menu: menuOpts, button: buttonOpts, active: menuOpts.active}); }); @@ -314,6 +330,8 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { }); buttons.call(styleButtons, menuOpts); + + scrollBox.enable(); } function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex, isSilentUpdate) { diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js new file mode 100644 index 00000000000..1c0d8a0a19a --- /dev/null +++ b/src/components/updatemenus/scrollbox.js @@ -0,0 +1,381 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = ScrollBox; + +var d3 = require('d3'); + +var Color = require('../color'); +var Drawing = require('../drawing'); + +var Lib = require('../../lib'); + +/** + * Helper class to setup a scroll box + * + * @class + * @param gd Plotly's graph div + * @param container Container to be scroll-boxed (as a D3 selection) + * @param {Object} position + * @param {number} position.l Left side position (in pixels) + * @param {number} position.t Top side (in pixels) + * @param {number} position.w Width (in pixels) + * @param {number} position.h Height (in pixels) + * @param {string} id Id for the clip path to implement the scroll box + */ +function ScrollBox(gd, container, position, id) { + this.gd = gd; + this.container = container; + this.position = position; + this.id = id; +} + +// scroll bar dimensions +ScrollBox.barWidth = 2; +ScrollBox.barLength = 20; +ScrollBox.barRadius = 2; +ScrollBox.barPad = 1; +ScrollBox.barColor = '#808BA4'; + +/** + * If needed, setup a clip path and scrollbars + * + * @method + */ +ScrollBox.prototype.enable = function enable() { + var fullLayout = this.gd._fullLayout, + fullWidth = fullLayout.width, + fullHeight = fullLayout.height, + graphSize = fullLayout._size; + + // compute position of scroll box + var l = this.position.l, + w = this.position.w, + t = this.position.t, + h = this.position.h, + boxW = (w <= fullWidth) ? w : fullWidth - graphSize.r - l, + boxH = (h <= fullHeight) ? h : fullHeight - graphSize.b - t, + boxL, boxR, + boxT, boxB; + + var minSize = 2 * (ScrollBox.barLength + 2 * ScrollBox.barPad); + if(boxW <= minSize || fullWidth < boxW) boxW = fullWidth / 4; + if(boxH <= minSize || fullHeight < boxH) boxH = fullHeight / 4; + + if(0 <= l && l <= fullWidth) { + boxL = l; + boxR = boxL + boxW; + } + else { + // align left + boxL = 0; + boxR = boxL + boxW; + } + + if(0 <= t && t <= fullHeight) { + boxT = t; + boxB = boxT + boxH; + } + else { + // align top + boxT = 0; + boxB = boxT + boxH; + } + + if(boxR > fullWidth) { + // align right + boxR = fullWidth; + boxL = boxR - boxW; + } + + if(boxB > fullHeight) { + // align bottom + boxB = fullHeight; + boxT = boxB - boxH; + } + + this._box = { + l: boxL, + t: boxT, + w: boxW, + h: boxH + }; + + // compute position of horizontal scroll bar + var needsHorizontalScrollBar = (w > boxW), + hbarW = ScrollBox.barLength + 2 * ScrollBox.barPad, + hbarH = ScrollBox.barWidth + 2 * ScrollBox.barPad, + hbarL = boxL, + hbarT = (boxB + hbarH < fullHeight) ? boxB : fullHeight - hbarH; + + var hbar = this.container.selectAll('rect.scrollbar-horizontal').data( + (needsHorizontalScrollBar) ? [0] : []); + + hbar.exit() + .on('.drag', null) + .remove(); + + hbar.enter().append('rect') + .classed('scrollbar-horizontal', true) + .call(Color.fill, ScrollBox.barColor); + + if(needsHorizontalScrollBar) { + this._hbar = hbar.attr({ + 'rx': ScrollBox.barRadius, + 'ry': ScrollBox.barRadius, + 'x': hbarL, + 'y': hbarT, + 'width': hbarW, + 'height': hbarH + }); + + // hbar center moves between hbarXMin and hbarXMin + hbarTranslateMax + this._hbarXMin = hbarL + hbarW / 2; + this._hbarTranslateMax = boxW - hbarW; + } + else { + delete this._hbar; + delete this._hbarXMin; + delete this._hbarTranslateMax; + } + + // compute position of vertical scroll bar + var needsVerticalScrollBar = (h > boxH), + vbarW = ScrollBox.barWidth + 2 * ScrollBox.barPad, + vbarH = ScrollBox.barLength + 2 * ScrollBox.barPad, + vbarL = (boxR + vbarW < fullWidth) ? boxR : fullWidth - vbarW, + vbarT = boxT; + + var vbar = this.container.selectAll('rect.scrollbar-vertical').data( + (needsVerticalScrollBar) ? [0] : []); + + vbar.exit() + .on('.drag', null) + .remove(); + + vbar.enter().append('rect') + .classed('scrollbar-vertical', true) + .call(Color.fill, ScrollBox.barColor); + + if(needsVerticalScrollBar) { + this._vbar = vbar.attr({ + 'rx': ScrollBox.barRadius, + 'ry': ScrollBox.barRadius, + 'x': vbarL, + 'y': vbarT, + 'width': vbarW, + 'height': vbarH + }); + + // vbar center moves between vbarYMin and vbarYMin + vbarTranslateMax + this._vbarYMin = vbarT + vbarH / 2; + this._vbarTranslateMax = boxH - vbarH; + } + else { + delete this._vbar; + delete this._vbarYMin; + delete this._vbarTranslateMax; + } + + // setup a clip path (if scroll bars are needed) + var clipId = this.id, + clipL = boxL - 0.5, + clipR = (needsVerticalScrollBar) ? boxR + vbarW + 0.5 : boxR + 0.5, + clipT = boxT - 0.5, + clipB = (needsHorizontalScrollBar) ? boxB + hbarH + 0.5 : boxB + 0.5; + + var clipPath = fullLayout._topdefs.selectAll('#' + clipId) + .data((needsHorizontalScrollBar || needsVerticalScrollBar) ? [0] : []); + + clipPath.exit().remove(); + + clipPath.enter() + .append('clipPath').attr('id', clipId) + .append('rect'); + + if(needsHorizontalScrollBar || needsVerticalScrollBar) { + this._clipRect = clipPath.select('rect').attr({ + x: Math.floor(clipL), + y: Math.floor(clipT), + width: Math.ceil(clipR) - Math.floor(clipL), + height: Math.ceil(clipB) - Math.floor(clipT) + }); + this.container.call(Drawing.setClipUrl, clipId); + } + else { + delete this._clipRect; + this.container.call(Drawing.setClipUrl, null); + } + + // set up drag listeners (if scroll bars are needed) + if(needsHorizontalScrollBar || needsVerticalScrollBar) { + var onBoxDrag = d3.behavior.drag() + .on('dragstart', function() { + d3.event.sourceEvent.preventDefault(); + }) + .on('drag', this._onBoxDrag.bind(this)); + + this.container + .on('.drag', null) + .call(onBoxDrag); + + var onBarDrag = d3.behavior.drag() + .on('dragstart', function() { + d3.event.sourceEvent.preventDefault(); + d3.event.sourceEvent.stopPropagation(); + }) + .on('drag', this._onBarDrag.bind(this)); + + if(needsHorizontalScrollBar) { + this._hbar + .on('.drag', null) + .call(onBarDrag); + } + + if(needsVerticalScrollBar) { + this._vbar + .on('.drag', null) + .call(onBarDrag); + } + } + + // set initial position + this._setTranslate(0, 0); +}; + +/** + * If present, remove clip-path and scrollbars + * + * @method + */ +ScrollBox.prototype.disable = function disable() { + if(this._hbar || this._vbar) { + this.container.call(Drawing.setClipUrl, null); + this.container.on('.drag', null); + delete this._clipRect; + } + + if(this._hbar) { + this._hbar.on('.drag', null); + this._hbar.remove(); + delete this._hbar; + delete this._hbarXMin; + delete this._hbarTranslateMax; + } + + if(this._vbar) { + this._vbar.on('.drag', null); + this._vbar.remove(); + delete this._vbar; + delete this._vbarYMin; + delete this._vbarTranslateMax; + } +}; + +/** + * Handles scroll box drag events + * + * @method + */ +ScrollBox.prototype._onBoxDrag = function onBarDrag() { + var xf = this._xf, + yf = this._yf; + + if(this._hbar) { + var translateXMax = this.position.w - this._box.w; + + xf = Lib.constrain(xf - d3.event.dx / translateXMax, 0, 1); + } + else xf = 0; + + if(this._vbar) { + var translateYMax = this.position.h - this._box.h; + + yf = Lib.constrain(yf - d3.event.dy / translateYMax, 0, 1); + } + else yf = 0; + + this._setTranslate(xf, yf); +}; + +/** + * Handles scroll bar drag events + * + * @method + */ +ScrollBox.prototype._onBarDrag = function onBarDrag() { + var xf = this._xf, + yf = this._yf; + + if(this._hbar) { + var translateXMax = this.position.w - this._box.w, + translateX = xf * translateXMax, + xMin = translateX + this._hbarXMin, + xMax = xMin + this._hbarTranslateMax, + x = Lib.constrain(d3.event.x, xMin, xMax); + + xf = (x - xMin) / (xMax - xMin); + } + else xf = 0; + + if(this._vbar) { + var translateYMax = this.position.h - this._box.h, + translateY = yf * translateYMax, + yMin = translateY + this._vbarYMin, + yMax = yMin + this._vbarTranslateMax, + y = Lib.constrain(d3.event.y, yMin, yMax); + + yf = (y - yMin) / (yMax - yMin); + } + else yf = 0; + + this._setTranslate(xf, yf); +}; + +/** + * Set clip path and scroll bar translate transform + * + * @method + * @param {number} xf Horizontal position as a container fraction + * @param {number} yf Vertical position as a container fraction + */ +ScrollBox.prototype._setTranslate = function _setTranslate(xf, yf) { + // store xf and yf (needed by ScrollBox.prototype._on*Drag) + this._xf = xf; + this._yf = yf; + + var translateXMax = this.position.w - this._box.w, + translateYMax = this.position.h - this._box.h, + translateX = xf * translateXMax, + translateY = yf * translateYMax; + + this.container.call(Lib.setTranslate, + this._box.l - this.position.l - translateX, + this._box.t - this.position.t - translateY); + + if(this._clipRect) { + this._clipRect.attr({ + x: Math.floor(this.position.l + translateX - 0.5), + y: Math.floor(this.position.t + translateY - 0.5) + }); + } + + if(this._hbar) { + this._hbar.call(Lib.setTranslate, + translateX + xf * this._hbarTranslateMax, + 0); + } + + if(this._vbar) { + this._vbar.call(Lib.setTranslate, + 0, + translateY + yf * this._vbarTranslateMax); + } +}; From 66e503ca5b260e55663214315f376efeb2b4fe8c Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Thu, 26 Jan 2017 14:37:53 +0000 Subject: [PATCH 03/19] updatemenus: Fix image test failure * Reduce scroll box minimum size from 44 to 25 pixels. * Allow scroll boxes to use the full width and height. --- src/components/updatemenus/draw.js | 2 +- src/components/updatemenus/scrollbox.js | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 67308362422..cb9b31254ff 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -308,7 +308,7 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { button.on('click', function() { // skip `dragend` events - if (d3.event.defaultPrevented) return; + if(d3.event.defaultPrevented) return; setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex); diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 1c0d8a0a19a..329bf8c2e39 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -52,22 +52,24 @@ ScrollBox.barColor = '#808BA4'; ScrollBox.prototype.enable = function enable() { var fullLayout = this.gd._fullLayout, fullWidth = fullLayout.width, - fullHeight = fullLayout.height, - graphSize = fullLayout._size; + fullHeight = fullLayout.height; // compute position of scroll box var l = this.position.l, w = this.position.w, t = this.position.t, h = this.position.h, - boxW = (w <= fullWidth) ? w : fullWidth - graphSize.r - l, - boxH = (h <= fullHeight) ? h : fullHeight - graphSize.b - t, + boxW = w, + boxH = h, boxL, boxR, boxT, boxB; - var minSize = 2 * (ScrollBox.barLength + 2 * ScrollBox.barPad); - if(boxW <= minSize || fullWidth < boxW) boxW = fullWidth / 4; - if(boxH <= minSize || fullHeight < boxH) boxH = fullHeight / 4; + if(boxW > fullWidth) boxW = fullWidth / 4; + if(boxH > fullHeight) boxH = fullHeight / 4; + + var minSize = 4 + (ScrollBox.barLength + 2 * ScrollBox.barPad); + boxW = Math.max(boxW, minSize); + boxH = Math.max(boxH, minSize); if(0 <= l && l <= fullWidth) { boxL = l; From 8df8fd68b63c61c6da0dac40b42292cf2d159191 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Thu, 26 Jan 2017 15:33:45 +0000 Subject: [PATCH 04/19] updatemenus: Update copyright notice --- src/components/updatemenus/scrollbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 329bf8c2e39..596737e23e4 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -1,5 +1,5 @@ /** -* Copyright 2012-2016, Plotly, Inc. +* Copyright 2012-2017, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the From 7057bc76b14cf18ebab3cd06b249c40d02dc213b Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Mon, 30 Jan 2017 12:08:18 +0000 Subject: [PATCH 05/19] updatemenus: make ScrollBox#setTranslate public --- src/components/updatemenus/scrollbox.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 596737e23e4..4c53e744f06 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -249,7 +249,7 @@ ScrollBox.prototype.enable = function enable() { } // set initial position - this._setTranslate(0, 0); + this.setTranslate(); }; /** @@ -304,7 +304,7 @@ ScrollBox.prototype._onBoxDrag = function onBarDrag() { } else yf = 0; - this._setTranslate(xf, yf); + this.setTranslate(xf, yf); }; /** @@ -338,17 +338,20 @@ ScrollBox.prototype._onBarDrag = function onBarDrag() { } else yf = 0; - this._setTranslate(xf, yf); + this.setTranslate(xf, yf); }; /** * Set clip path and scroll bar translate transform * * @method - * @param {number} xf Horizontal position as a container fraction - * @param {number} yf Vertical position as a container fraction + * @param {number} [xf=0] Horizontal position as a container fraction + * @param {number} [yf=0] Vertical position as a container fraction */ -ScrollBox.prototype._setTranslate = function _setTranslate(xf, yf) { +ScrollBox.prototype.setTranslate = function setTranslate(xf, yf) { + xf = Lib.constrain(xf || 0, 0, 1); + yf = Lib.constrain(yf || 0, 0, 1); + // store xf and yf (needed by ScrollBox.prototype._on*Drag) this._xf = xf; this._yf = yf; From 8d1f333551094e411ef85e3339d712ef802194ee Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Mon, 30 Jan 2017 12:24:00 +0000 Subject: [PATCH 06/19] updatemenus: fix positioning of scrollbars * Do not assume that only one of the scrollbars is present. --- src/components/updatemenus/scrollbox.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 4c53e744f06..09fdf99f8c0 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -375,12 +375,12 @@ ScrollBox.prototype.setTranslate = function setTranslate(xf, yf) { if(this._hbar) { this._hbar.call(Lib.setTranslate, translateX + xf * this._hbarTranslateMax, - 0); + translateY); } if(this._vbar) { this._vbar.call(Lib.setTranslate, - 0, + translateX, translateY + yf * this._vbarTranslateMax); } }; From 563009b4e1a7cf141fd485915f90d626d9582dff Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Mon, 30 Jan 2017 13:55:59 +0000 Subject: [PATCH 07/19] updatemenus: center dropmenu on active option --- src/components/updatemenus/draw.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 8e16e6c7664..a53c62da8ee 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -332,6 +332,33 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { buttons.call(styleButtons, menuOpts); scrollBox.enable(); + + var active = menuOpts.active, + i; + if(isVertical) { + if(scrollBox._vbar) { + var translateY = 0; + for(i = 0; i < active; i++) { + translateY += menuOpts.heights[i] + constants.gapButton; + } + translateY -= constants.gapButton; + + var yf = translateY / (scrollBox.position.h - scrollBox._box.h); + scrollBox.setTranslate(0, yf); + } + } + else { + if(scrollBox._hbar) { + var translateX = 0; + for(i = 0; i < active; i++) { + translateX += menuOpts.widths[i] + constants.gapButton; + } + translateX -= constants.gapButton; + + var xf = translateX / (scrollBox.position.w - scrollBox._box.w); + scrollBox.setTranslate(xf, 0); + } + } } function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex, isSilentUpdate) { From ef5210e97f583beaf23ef99e3fbaa62e9717ed00 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Mon, 30 Jan 2017 15:49:18 +0000 Subject: [PATCH 08/19] updatemenus: hide scrollbar if header clicked --- src/components/updatemenus/draw.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index a53c62da8ee..01b84517721 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -259,6 +259,8 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { exit.remove(); } + // if folding a dropdown menu, don't draw the buttons + if(!buttons.size()) return; var x0 = 0; var y0 = 0; @@ -607,6 +609,11 @@ function setItemPosition(item, menuOpts, posOpts, overrideOpts) { function removeAllButtons(gButton) { gButton.selectAll('g.' + constants.dropdownButtonClassName).remove(); + + // remove scrollbox + gButton.selectAll('rect.scrollbar-horizontal').remove(); + gButton.selectAll('rect.scrollbar-vertical').remove(); + gButton.call(Drawing.setClipUrl, null); } function clearPushMargins(gd) { From f17773f1fe4235ac0e36c02d7265571f00d307a4 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 31 Jan 2017 12:52:51 +0000 Subject: [PATCH 09/19] updatemenu: ScrollBox#setTranslate to take pixels * Changed `ScrollBox#setTranslate(translateX, translateY)` to take pixels. * Previously, invoking `ScrollBox#setTranslate(xf, yf)` would require to access `ScrollBox#_box` to compute `xf` and `yf`. * Now, it's possible to drag the buttons in the scrollbox beyond the scrollbox. --- src/components/updatemenus/draw.js | 6 +-- src/components/updatemenus/scrollbox.js | 72 ++++++++++++------------- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 01b84517721..a8ebdbd0d4b 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -345,8 +345,7 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { } translateY -= constants.gapButton; - var yf = translateY / (scrollBox.position.h - scrollBox._box.h); - scrollBox.setTranslate(0, yf); + scrollBox.setTranslate(0, translateY); } } else { @@ -357,8 +356,7 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { } translateX -= constants.gapButton; - var xf = translateX / (scrollBox.position.w - scrollBox._box.w); - scrollBox.setTranslate(xf, 0); + scrollBox.setTranslate(translateX, 0); } } } diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 09fdf99f8c0..50bcd0cdc98 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -287,24 +287,18 @@ ScrollBox.prototype.disable = function disable() { * @method */ ScrollBox.prototype._onBoxDrag = function onBarDrag() { - var xf = this._xf, - yf = this._yf; + var translateX = this._translateX, + translateY = this._translateY; if(this._hbar) { - var translateXMax = this.position.w - this._box.w; - - xf = Lib.constrain(xf - d3.event.dx / translateXMax, 0, 1); + translateX -= d3.event.dx; } - else xf = 0; if(this._vbar) { - var translateYMax = this.position.h - this._box.h; - - yf = Lib.constrain(yf - d3.event.dy / translateYMax, 0, 1); + translateY -= d3.event.dy; } - else yf = 0; - this.setTranslate(xf, yf); + this.setTranslate(translateX, translateY); }; /** @@ -313,53 +307,51 @@ ScrollBox.prototype._onBoxDrag = function onBarDrag() { * @method */ ScrollBox.prototype._onBarDrag = function onBarDrag() { - var xf = this._xf, - yf = this._yf; + var translateX = this._translateX, + translateY = this._translateY; if(this._hbar) { - var translateXMax = this.position.w - this._box.w, - translateX = xf * translateXMax, - xMin = translateX + this._hbarXMin, + var xMin = translateX + this._hbarXMin, xMax = xMin + this._hbarTranslateMax, - x = Lib.constrain(d3.event.x, xMin, xMax); + x = Lib.constrain(d3.event.x, xMin, xMax), + xf = (x - xMin) / (xMax - xMin); - xf = (x - xMin) / (xMax - xMin); + var translateXMax = this.position.w - this._box.w; + + translateX = xf * translateXMax; } - else xf = 0; if(this._vbar) { - var translateYMax = this.position.h - this._box.h, - translateY = yf * translateYMax, - yMin = translateY + this._vbarYMin, + var yMin = translateY + this._vbarYMin, yMax = yMin + this._vbarTranslateMax, - y = Lib.constrain(d3.event.y, yMin, yMax); + y = Lib.constrain(d3.event.y, yMin, yMax), + yf = (y - yMin) / (yMax - yMin); + + var translateYMax = this.position.h - this._box.h; - yf = (y - yMin) / (yMax - yMin); + translateY = yf * translateYMax; } - else yf = 0; - this.setTranslate(xf, yf); + this.setTranslate(translateX, translateY); }; /** * Set clip path and scroll bar translate transform * * @method - * @param {number} [xf=0] Horizontal position as a container fraction - * @param {number} [yf=0] Vertical position as a container fraction + * @param {number} [translateX=0] Horizontal offset (in pixels) + * @param {number} [translateY=0] Vertical offset (in pixels) */ -ScrollBox.prototype.setTranslate = function setTranslate(xf, yf) { - xf = Lib.constrain(xf || 0, 0, 1); - yf = Lib.constrain(yf || 0, 0, 1); +ScrollBox.prototype.setTranslate = function setTranslate(translateX, translateY) { + // store translateX and translateY (needed by mouse event handlers) + var translateXMax = this.position.w - this._box.w, + translateYMax = this.position.h - this._box.h; - // store xf and yf (needed by ScrollBox.prototype._on*Drag) - this._xf = xf; - this._yf = yf; + translateX = Lib.constrain(translateX || 0, 0, translateXMax); + translateY = Lib.constrain(translateY || 0, 0, translateYMax); - var translateXMax = this.position.w - this._box.w, - translateYMax = this.position.h - this._box.h, - translateX = xf * translateXMax, - translateY = yf * translateYMax; + this._translateX = translateX; + this._translateY = translateY; this.container.call(Lib.setTranslate, this._box.l - this.position.l - translateX, @@ -373,12 +365,16 @@ ScrollBox.prototype.setTranslate = function setTranslate(xf, yf) { } if(this._hbar) { + var xf = translateX / translateXMax; + this._hbar.call(Lib.setTranslate, translateX + xf * this._hbarTranslateMax, translateY); } if(this._vbar) { + var yf = translateY / translateYMax; + this._vbar.call(Lib.setTranslate, translateX, translateY + yf * this._vbarTranslateMax); From 9e3ac1b73bad1ae13a4ad41b1ee23d9d8aab528c Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 31 Jan 2017 16:14:46 +0000 Subject: [PATCH 10/19] updatemenus: fix smooth dropdown folding --- src/components/updatemenus/draw.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index a8ebdbd0d4b..0b01a690325 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -254,7 +254,13 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { exit.transition() .attr('opacity', '0') - .remove(); + .remove() + .each('end', function() { + // remove the scrollbox, if all the buttons have been removed + if(gButton.selectAll('g.' + klass).size() === 0) { + gButton.call(removeAllButtons); + } + }); } else { exit.remove(); } @@ -316,8 +322,6 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args); - scrollBox.disable(); - gd.emit('plotly_buttonclicked', {menu: menuOpts, button: buttonOpts, active: menuOpts.active}); }); From 13508daf68fcb77499d2b3975448675386753af3 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 31 Jan 2017 16:35:35 +0000 Subject: [PATCH 11/19] updatemenus: handle mouse wheel events --- src/components/updatemenus/scrollbox.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 50bcd0cdc98..b2d141485f6 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -246,6 +246,8 @@ ScrollBox.prototype.enable = function enable() { .on('.drag', null) .call(onBarDrag); } + + this.container.on('wheel', this._onBoxWheel.bind(this)); } // set initial position @@ -279,6 +281,8 @@ ScrollBox.prototype.disable = function disable() { delete this._vbarYMin; delete this._vbarTranslateMax; } + + this.container.on('wheel', null); }; /** @@ -301,6 +305,26 @@ ScrollBox.prototype._onBoxDrag = function onBarDrag() { this.setTranslate(translateX, translateY); }; +/** + * Handles scroll box wheel events + * + * @method + */ +ScrollBox.prototype._onBoxWheel = function onBarWheel() { + var translateX = this._translateX, + translateY = this._translateY; + + if(this._hbar) { + translateX += d3.event.deltaY; + } + + if(this._vbar) { + translateY += d3.event.deltaY; + } + + this.setTranslate(translateX, translateY); +}; + /** * Handles scroll bar drag events * From 8ab3ebec10982a02f4284652b111e162b4559033 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 1 Feb 2017 13:26:18 +0000 Subject: [PATCH 12/19] updatemenus: refactor where scrollbox is created * Previously, the scrollbox was created every time the dropmenu was unfolded. * Now, the scrollbox is created right after creating the dropdown container. * Moved the logic to fold and unfold the dropdown menu to functions foldDropdownMenu and unfoldDropdownMenu. * Moved all the logic to compute the position and size of the scrollbox to function unfoldDropdownMenu. * Moved logic to disable the scrollbox to function foldDropdownMenu. * This refactor will help introduce a background element in the dropdown container before any dropdown buttons. * It will also help introduce a transition to show and hide the scrollbars. --- src/components/updatemenus/draw.js | 229 +++++++++++++----------- src/components/updatemenus/scrollbox.js | 98 +++++----- 2 files changed, 182 insertions(+), 145 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 0b01a690325..8b1c276bdd7 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -84,50 +84,47 @@ 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) - if(headerGroups.enter().size()) { - gButton - .call(removeAllButtons) - .attr(constants.menuIndexAttrName, '-1'); + // 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()) { + foldDropdownMenu(gButton, scrollBox); + } + headerGroups.exit().each(function(menuOpts) { d3.select(this).remove(); - gButton - .call(removeAllButtons) - .attr(constants.menuIndexAttrName, '-1'); + foldDropdownMenu(gButton, scrollBox); 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); + // update dropdown buttons if this menu is active + if(isActive(gButton, menuOpts)) { + unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts); } } else { - drawButtons(gd, gHeader, null, menuOpts); + drawButtons(gd, gHeader, null, scrollBox, menuOpts); } }); @@ -152,18 +149,104 @@ 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 isFolded(gButton) { + return +gButton.attr(constants.menuIndexAttrName) === -1; +} + +function isActive(gButton, menuOpts) { + return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index; +} + +function unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts) { + // enable the scrollbox + var direction = menuOpts.direction, + isUp = (direction === 'up'), + isDown = (direction === 'down'), + isLeft = (direction === 'left'), + isRight = (direction === 'right'), + isVertical = (isUp || isDown); + + var x0, y0; + if(isDown) { + x0 = 0; + y0 = menuOpts.headerHeight + constants.gapButtonHeader; + } + else if(isUp) { + x0 = 0; + y0 = menuOpts.headerHeight + constants.gapButton - menuOpts.openHeight; + } + else if(isRight) { + x0 = menuOpts.headerWidth + constants.gapButtonHeader; + y0 = 0; + } + else if(isLeft) { + x0 = menuOpts.headerWidth + constants.gapButton - menuOpts.openWidth; + y0 = 0; + } + + var position = { + l: menuOpts.lx + menuOpts.borderwidth + x0 + menuOpts.pad.l, + t: menuOpts.ly + menuOpts.borderwidth + y0 + menuOpts.pad.t, + w: Math.max(menuOpts.openWidth, menuOpts.headerWidth), + h: menuOpts.openHeight + }; + + 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; + } + } + + scrollBox.enable(position, translateX, translateY); + + // store index of active menu (-1 means dropdown menu is folded) + gButton.attr(constants.menuIndexAttrName, menuOpts._index); + + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); } -function areMenuButtonsDropped(gButton, menuOpts) { - var droppedIndex = +gButton.attr(constants.menuIndexAttrName); +function foldDropdownMenu(gButton, scrollBox) { + scrollBox.disable(); - return droppedIndex === menuOpts._index; + // -1 means dropdown menu is folded + gButton + .attr(constants.menuIndexAttrName, '-1') + .call(removeAllButtons); } -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 === 'dropdown') { + // fold up buttons and redraw header + gButton.attr(constants.menuIndexAttrName, '-1'); + + drawHeader(gd, gHeader, gButton, scrollBox, menuOpts); + } + + if(!isSilentUpdate || menuOpts.type === 'buttons') { + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); + } +} + +function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { var header = gHeader.selectAll('g.' + constants.headerClassName) .data([0]); @@ -200,16 +283,18 @@ 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 - gButton.attr( - constants.menuIndexAttrName, - areMenuButtonsDropped(gButton, menuOpts) ? '-1' : String(menuOpts._index) - ); - - drawButtons(gd, gHeader, gButton, menuOpts); + if(isFolded(gButton)) { + unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts); + } + else if(isActive(gButton, menuOpts)) { + foldDropdownMenu(gButton, scrollBox); + } + else { + // the dropdown menu is unfolded, + // but the clicked header is not the active header + foldDropdownMenu(gButton, scrollBox); + unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts); + } }); header.on('mouseover', function() { @@ -224,7 +309,7 @@ function drawHeader(gd, gHeader, gButton, menuOpts) { Lib.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 @@ -233,7 +318,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 : []; @@ -258,7 +343,7 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { .each('end', function() { // remove the scrollbox, if all the buttons have been removed if(gButton.selectAll('g.' + klass).size() === 0) { - gButton.call(removeAllButtons); + foldDropdownMenu(gButton, scrollBox); } }); } else { @@ -297,16 +382,6 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { index: 0, }; - var fullLayout = gd._fullLayout, - scrollBoxId = 'updatemenus' + fullLayout._uid + menuOpts._index, - scrollBoxPosition = { - l: menuOpts.lx + menuOpts.borderwidth + x0 + menuOpts.pad.l, - t: menuOpts.ly + menuOpts.borderwidth + y0 + menuOpts.pad.t, - w: Math.max(menuOpts.openWidth, menuOpts.headerWidth), - h: menuOpts.openHeight - }, - scrollBox = new ScrollBox(gd, gButton, scrollBoxPosition, scrollBoxId); - buttons.each(function(buttonOpts, buttonIndex) { var button = d3.select(this); @@ -318,7 +393,7 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { // skip `dragend` events if(d3.event.defaultPrevented) return; - setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex); + setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex); Plots.executeAPICommand(gd, buttonOpts.method, buttonOpts.args); @@ -336,49 +411,6 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { }); buttons.call(styleButtons, menuOpts); - - scrollBox.enable(); - - var active = menuOpts.active, - i; - if(isVertical) { - if(scrollBox._vbar) { - var translateY = 0; - for(i = 0; i < active; i++) { - translateY += menuOpts.heights[i] + constants.gapButton; - } - translateY -= constants.gapButton; - - scrollBox.setTranslate(0, translateY); - } - } - else { - if(scrollBox._hbar) { - var translateX = 0; - for(i = 0; i < active; i++) { - translateX += menuOpts.widths[i] + constants.gapButton; - } - translateX -= constants.gapButton; - - scrollBox.setTranslate(translateX, 0); - } - } -} - -function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, buttonIndex, isSilentUpdate) { - // update 'active' attribute in menuOpts - menuOpts._input.active = menuOpts.active = buttonIndex; - - if(menuOpts.type === 'dropdown') { - // fold up buttons and redraw header - gButton.attr(constants.menuIndexAttrName, '-1'); - - drawHeader(gd, gHeader, gButton, menuOpts); - } - - if(!isSilentUpdate || menuOpts.type === 'buttons') { - drawButtons(gd, gHeader, gButton, menuOpts); - } } function drawItem(item, menuOpts, itemOpts) { @@ -611,11 +643,6 @@ function setItemPosition(item, menuOpts, posOpts, overrideOpts) { function removeAllButtons(gButton) { gButton.selectAll('g.' + constants.dropdownButtonClassName).remove(); - - // remove scrollbox - gButton.selectAll('rect.scrollbar-horizontal').remove(); - gButton.selectAll('rect.scrollbar-vertical').remove(); - gButton.call(Drawing.setClipUrl, null); } function clearPushMargins(gd) { diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index b2d141485f6..1e16c45d79a 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -23,18 +23,19 @@ var Lib = require('../../lib'); * @class * @param gd Plotly's graph div * @param container Container to be scroll-boxed (as a D3 selection) - * @param {Object} position - * @param {number} position.l Left side position (in pixels) - * @param {number} position.t Top side (in pixels) - * @param {number} position.w Width (in pixels) - * @param {number} position.h Height (in pixels) * @param {string} id Id for the clip path to implement the scroll box */ -function ScrollBox(gd, container, position, id) { +function ScrollBox(gd, container, id) { this.gd = gd; this.container = container; - this.position = position; this.id = id; + + // See ScrollBox.prototype.enable for further definition + this.position = null; // scrollbox position + this.translateX = null; // scrollbox horizontal translation + this.translateY = null; // scrollbox vertical translation + this.hbar = null; // horizontal scrollbar D3 selection + this.vbar = null; // vertical scrollbar D3 selection } // scroll bar dimensions @@ -48,13 +49,22 @@ ScrollBox.barColor = '#808BA4'; * If needed, setup a clip path and scrollbars * * @method + * @param {Object} position + * @param {number} position.l Left side position (in pixels) + * @param {number} position.t Top side (in pixels) + * @param {number} position.w Width (in pixels) + * @param {number} position.h Height (in pixels) + * @param {number} [translateX=0] Horizontal offset (in pixels) + * @param {number} [translateY=0] Vertical offset (in pixels) */ -ScrollBox.prototype.enable = function enable() { +ScrollBox.prototype.enable = function enable(position, translateX, translateY) { var fullLayout = this.gd._fullLayout, fullWidth = fullLayout.width, fullHeight = fullLayout.height; - // compute position of scroll box + // compute position of scrollbox + this.position = position; + var l = this.position.l, w = this.position.w, t = this.position.t, @@ -129,7 +139,7 @@ ScrollBox.prototype.enable = function enable() { .call(Color.fill, ScrollBox.barColor); if(needsHorizontalScrollBar) { - this._hbar = hbar.attr({ + this.hbar = hbar.attr({ 'rx': ScrollBox.barRadius, 'ry': ScrollBox.barRadius, 'x': hbarL, @@ -143,7 +153,7 @@ ScrollBox.prototype.enable = function enable() { this._hbarTranslateMax = boxW - hbarW; } else { - delete this._hbar; + delete this.hbar; delete this._hbarXMin; delete this._hbarTranslateMax; } @@ -167,7 +177,7 @@ ScrollBox.prototype.enable = function enable() { .call(Color.fill, ScrollBox.barColor); if(needsVerticalScrollBar) { - this._vbar = vbar.attr({ + this.vbar = vbar.attr({ 'rx': ScrollBox.barRadius, 'ry': ScrollBox.barRadius, 'x': vbarL, @@ -181,7 +191,7 @@ ScrollBox.prototype.enable = function enable() { this._vbarTranslateMax = boxH - vbarH; } else { - delete this._vbar; + delete this.vbar; delete this._vbarYMin; delete this._vbarTranslateMax; } @@ -236,13 +246,13 @@ ScrollBox.prototype.enable = function enable() { .on('drag', this._onBarDrag.bind(this)); if(needsHorizontalScrollBar) { - this._hbar + this.hbar .on('.drag', null) .call(onBarDrag); } if(needsVerticalScrollBar) { - this._vbar + this.vbar .on('.drag', null) .call(onBarDrag); } @@ -250,8 +260,8 @@ ScrollBox.prototype.enable = function enable() { this.container.on('wheel', this._onBoxWheel.bind(this)); } - // set initial position - this.setTranslate(); + // set scrollbox translation + this.setTranslate(translateX, translateY); }; /** @@ -260,24 +270,24 @@ ScrollBox.prototype.enable = function enable() { * @method */ ScrollBox.prototype.disable = function disable() { - if(this._hbar || this._vbar) { + if(this.hbar || this.vbar) { this.container.call(Drawing.setClipUrl, null); this.container.on('.drag', null); delete this._clipRect; } - if(this._hbar) { - this._hbar.on('.drag', null); - this._hbar.remove(); - delete this._hbar; + if(this.hbar) { + this.hbar.on('.drag', null); + this.hbar.remove(); + delete this.hbar; delete this._hbarXMin; delete this._hbarTranslateMax; } - if(this._vbar) { - this._vbar.on('.drag', null); - this._vbar.remove(); - delete this._vbar; + if(this.vbar) { + this.vbar.on('.drag', null); + this.vbar.remove(); + delete this.vbar; delete this._vbarYMin; delete this._vbarTranslateMax; } @@ -291,14 +301,14 @@ ScrollBox.prototype.disable = function disable() { * @method */ ScrollBox.prototype._onBoxDrag = function onBarDrag() { - var translateX = this._translateX, - translateY = this._translateY; + var translateX = this.translateX, + translateY = this.translateY; - if(this._hbar) { + if(this.hbar) { translateX -= d3.event.dx; } - if(this._vbar) { + if(this.vbar) { translateY -= d3.event.dy; } @@ -311,14 +321,14 @@ ScrollBox.prototype._onBoxDrag = function onBarDrag() { * @method */ ScrollBox.prototype._onBoxWheel = function onBarWheel() { - var translateX = this._translateX, - translateY = this._translateY; + var translateX = this.translateX, + translateY = this.translateY; - if(this._hbar) { + if(this.hbar) { translateX += d3.event.deltaY; } - if(this._vbar) { + if(this.vbar) { translateY += d3.event.deltaY; } @@ -331,10 +341,10 @@ ScrollBox.prototype._onBoxWheel = function onBarWheel() { * @method */ ScrollBox.prototype._onBarDrag = function onBarDrag() { - var translateX = this._translateX, - translateY = this._translateY; + var translateX = this.translateX, + translateY = this.translateY; - if(this._hbar) { + if(this.hbar) { var xMin = translateX + this._hbarXMin, xMax = xMin + this._hbarTranslateMax, x = Lib.constrain(d3.event.x, xMin, xMax), @@ -345,7 +355,7 @@ ScrollBox.prototype._onBarDrag = function onBarDrag() { translateX = xf * translateXMax; } - if(this._vbar) { + if(this.vbar) { var yMin = translateY + this._vbarYMin, yMax = yMin + this._vbarTranslateMax, y = Lib.constrain(d3.event.y, yMin, yMax), @@ -374,8 +384,8 @@ ScrollBox.prototype.setTranslate = function setTranslate(translateX, translateY) translateX = Lib.constrain(translateX || 0, 0, translateXMax); translateY = Lib.constrain(translateY || 0, 0, translateYMax); - this._translateX = translateX; - this._translateY = translateY; + this.translateX = translateX; + this.translateY = translateY; this.container.call(Lib.setTranslate, this._box.l - this.position.l - translateX, @@ -388,18 +398,18 @@ ScrollBox.prototype.setTranslate = function setTranslate(translateX, translateY) }); } - if(this._hbar) { + if(this.hbar) { var xf = translateX / translateXMax; - this._hbar.call(Lib.setTranslate, + this.hbar.call(Lib.setTranslate, translateX + xf * this._hbarTranslateMax, translateY); } - if(this._vbar) { + if(this.vbar) { var yf = translateY / translateYMax; - this._vbar.call(Lib.setTranslate, + this.vbar.call(Lib.setTranslate, translateX, translateY + yf * this._vbarTranslateMax); } From 497540124e8c824559963b55e92468a5588d25dc Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 1 Feb 2017 19:50:56 +0000 Subject: [PATCH 13/19] updatemenus: add background to scrollbox * Added a background to the scrollbox, so that events on the gaps between buttons aren't dropped. --- src/components/updatemenus/scrollbox.js | 52 +++++++++++++++++++++---- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index 1e16c45d79a..da813ea13ae 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -36,6 +36,25 @@ function ScrollBox(gd, container, id) { this.translateY = null; // scrollbox vertical translation this.hbar = null; // horizontal scrollbar D3 selection this.vbar = null; // vertical scrollbar D3 selection + + // element to capture pointer events + this.bg = this.container.selectAll('rect.scrollbox-bg').data([0]); + + this.bg.exit() + .on('.drag', null) + .on('wheel', null) + .remove(); + + this.bg.enter().append('rect') + .classed('scrollbox-bg', true) + .style('pointer-events', 'all') + .attr({ + opacity: 0, + x: 0, + y: 0, + width: 0, + height: 0 + }); } // scroll bar dimensions @@ -219,11 +238,26 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { width: Math.ceil(clipR) - Math.floor(clipL), height: Math.ceil(clipB) - Math.floor(clipT) }); + this.container.call(Drawing.setClipUrl, clipId); + + this.bg.attr({ + x: l, + y: t, + width: w, + height: h + }); } else { + this.bg.attr({ + width: 0, + height: 0 + }); + this.container + .on('wheel', null) + .on('.drag', null) + .call(Drawing.setClipUrl, null); delete this._clipRect; - this.container.call(Drawing.setClipUrl, null); } // set up drag listeners (if scroll bars are needed) @@ -235,6 +269,8 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { .on('drag', this._onBoxDrag.bind(this)); this.container + .on('wheel', null) + .on('wheel', this._onBoxWheel.bind(this)) .on('.drag', null) .call(onBoxDrag); @@ -256,8 +292,6 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { .on('.drag', null) .call(onBarDrag); } - - this.container.on('wheel', this._onBoxWheel.bind(this)); } // set scrollbox translation @@ -271,8 +305,14 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { */ ScrollBox.prototype.disable = function disable() { if(this.hbar || this.vbar) { - this.container.call(Drawing.setClipUrl, null); - this.container.on('.drag', null); + this.bg.attr({ + width: 0, + height: 0 + }); + this.container + .on('wheel', null) + .on('.drag', null) + .call(Drawing.setClipUrl, null); delete this._clipRect; } @@ -291,8 +331,6 @@ ScrollBox.prototype.disable = function disable() { delete this._vbarYMin; delete this._vbarTranslateMax; } - - this.container.on('wheel', null); }; /** From 3b17d1d632a3b77851ff31e54116731eba3cba52 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Thu, 2 Feb 2017 13:14:28 +0000 Subject: [PATCH 14/19] updatemenus: remove un/foldDropdownMenu * Fixes issues caused by the relayout tests, where `updatemenus[1]` ended up without a buttons field. * A transition has been added to show and hide the scroll bars. --- src/components/updatemenus/draw.js | 229 +++++++++++++++++------------ 1 file changed, 131 insertions(+), 98 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 8b1c276bdd7..719e4b89e7b 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -96,13 +96,17 @@ module.exports = function draw(gd) { // remove exiting header, remove dropped buttons and reset margins if(headerGroups.enter().size()) { - foldDropdownMenu(gButton, scrollBox); + gButton + .call(removeAllButtons) + .attr(constants.menuIndexAttrName, '-1'); } headerGroups.exit().each(function(menuOpts) { d3.select(this).remove(); - foldDropdownMenu(gButton, scrollBox); + gButton + .call(removeAllButtons) + .attr(constants.menuIndexAttrName, '-1'); Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index); }); @@ -119,12 +123,12 @@ module.exports = function draw(gd) { if(menuOpts.type === 'dropdown') { drawHeader(gd, gHeader, gButton, scrollBox, menuOpts); - // update dropdown buttons if this menu is active + // if this menu is active, update the dropdown container if(isActive(gButton, menuOpts)) { - unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts); + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); } } else { - drawButtons(gd, gHeader, null, scrollBox, menuOpts); + drawButtons(gd, gHeader, null, null, menuOpts); } }); @@ -163,86 +167,22 @@ function isActive(gButton, menuOpts) { return +gButton.attr(constants.menuIndexAttrName) === menuOpts._index; } -function unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts) { - // enable the scrollbox - var direction = menuOpts.direction, - isUp = (direction === 'up'), - isDown = (direction === 'down'), - isLeft = (direction === 'left'), - isRight = (direction === 'right'), - isVertical = (isUp || isDown); - - var x0, y0; - if(isDown) { - x0 = 0; - y0 = menuOpts.headerHeight + constants.gapButtonHeader; - } - else if(isUp) { - x0 = 0; - y0 = menuOpts.headerHeight + constants.gapButton - menuOpts.openHeight; - } - else if(isRight) { - x0 = menuOpts.headerWidth + constants.gapButtonHeader; - y0 = 0; - } - else if(isLeft) { - x0 = menuOpts.headerWidth + constants.gapButton - menuOpts.openWidth; - y0 = 0; - } - - var position = { - l: menuOpts.lx + menuOpts.borderwidth + x0 + menuOpts.pad.l, - t: menuOpts.ly + menuOpts.borderwidth + y0 + menuOpts.pad.t, - w: Math.max(menuOpts.openWidth, menuOpts.headerWidth), - h: menuOpts.openHeight - }; - - 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; - } - } - - scrollBox.enable(position, translateX, translateY); - - // store index of active menu (-1 means dropdown menu is folded) - gButton.attr(constants.menuIndexAttrName, menuOpts._index); - - drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); -} - -function foldDropdownMenu(gButton, scrollBox) { - scrollBox.disable(); - - // -1 means dropdown menu is folded - gButton - .attr(constants.menuIndexAttrName, '-1') - .call(removeAllButtons); -} - function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex, isSilentUpdate) { // update 'active' attribute in menuOpts menuOpts._input.active = menuOpts.active = buttonIndex; - if(menuOpts.type === 'dropdown') { + 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 || menuOpts.type === 'buttons') { - drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); + if(!isSilentUpdate) { + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); + } } } @@ -283,18 +223,19 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { }); header.on('click', function() { - if(isFolded(gButton)) { - unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts); - } - else if(isActive(gButton, menuOpts)) { - foldDropdownMenu(gButton, scrollBox); - } - else { - // the dropdown menu is unfolded, - // but the clicked header is not the active header - foldDropdownMenu(gButton, scrollBox); - unfoldDropdownMenu(gd, gHeader, gButton, scrollBox, menuOpts); - } + gButton.call(removeAllButtons); + + + // if this menu is active, fold the dropdown container + // otherwise, make this menu active + gButton.attr( + constants.menuIndexAttrName, + isActive(gButton, menuOpts) ? + -1 : + String(menuOpts._index) + ); + + drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); }); header.on('mouseover', function() { @@ -339,20 +280,11 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { exit.transition() .attr('opacity', '0') - .remove() - .each('end', function() { - // remove the scrollbox, if all the buttons have been removed - if(gButton.selectAll('g.' + klass).size() === 0) { - foldDropdownMenu(gButton, scrollBox); - } - }); + .remove(); } else { exit.remove(); } - // if folding a dropdown menu, don't draw the buttons - if(!buttons.size()) return; - var x0 = 0; var y0 = 0; @@ -411,6 +343,107 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { }); buttons.call(styleButtons, menuOpts); + + if(scrollBox) { + if(buttons.size()) { + drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts); + } + else { + hideScrollBox(scrollBox); + } + } +} + +function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts) { + // enable the scrollbox + var direction = menuOpts.direction, + isUp = (direction === 'up'), + isDown = (direction === 'down'), + isLeft = (direction === 'left'), + isRight = (direction === 'right'), + isVertical = (isUp || isDown); + + var x0, y0; + if(isDown) { + x0 = 0; + y0 = menuOpts.headerHeight + constants.gapButtonHeader; + } + else if(isUp) { + x0 = 0; + y0 = menuOpts.headerHeight + constants.gapButton - menuOpts.openHeight; + } + else if(isRight) { + x0 = menuOpts.headerWidth + constants.gapButtonHeader; + y0 = 0; + } + else if(isLeft) { + x0 = menuOpts.headerWidth + constants.gapButton - menuOpts.openWidth; + y0 = 0; + } + + var position = { + l: menuOpts.lx + menuOpts.borderwidth + x0 + menuOpts.pad.l, + t: menuOpts.ly + menuOpts.borderwidth + y0 + menuOpts.pad.t, + w: Math.max(menuOpts.openWidth, menuOpts.headerWidth), + h: menuOpts.openHeight + }; + + 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; + } + } + + scrollBox.enable(position, translateX, translateY); + + if(scrollBox.hbar) { + scrollBox.hbar + .attr('opacity', '0') + .transition() + .attr('opacity', '1'); + } + + 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(); + }); + } } function drawItem(item, menuOpts, itemOpts) { From b155a8a63949025343c1f3143bffd6d29513f631 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Thu, 2 Feb 2017 15:54:36 +0000 Subject: [PATCH 15/19] updatemenu: fix computation of scrollbox size * Ensure scrollbox size is recomputed every time, drawButtons is called. --- src/components/updatemenus/draw.js | 49 +++++++++++------------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 719e4b89e7b..fbcbddedb50 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -314,6 +314,11 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { index: 0, }; + var scrollBoxPosition = { + l: posOpts.x + menuOpts.borderwidth, + t: posOpts.y + menuOpts.borderwidth + }; + buttons.each(function(buttonOpts, buttonIndex) { var button = d3.select(this); @@ -344,9 +349,18 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { buttons.call(styleButtons, menuOpts); + 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); + } + if(scrollBox) { if(buttons.size()) { - drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts); + drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, scrollBoxPosition); } else { hideScrollBox(scrollBox); @@ -354,39 +368,10 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { } } -function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts) { +function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) { // enable the scrollbox var direction = menuOpts.direction, - isUp = (direction === 'up'), - isDown = (direction === 'down'), - isLeft = (direction === 'left'), - isRight = (direction === 'right'), - isVertical = (isUp || isDown); - - var x0, y0; - if(isDown) { - x0 = 0; - y0 = menuOpts.headerHeight + constants.gapButtonHeader; - } - else if(isUp) { - x0 = 0; - y0 = menuOpts.headerHeight + constants.gapButton - menuOpts.openHeight; - } - else if(isRight) { - x0 = menuOpts.headerWidth + constants.gapButtonHeader; - y0 = 0; - } - else if(isLeft) { - x0 = menuOpts.headerWidth + constants.gapButton - menuOpts.openWidth; - y0 = 0; - } - - var position = { - l: menuOpts.lx + menuOpts.borderwidth + x0 + menuOpts.pad.l, - t: menuOpts.ly + menuOpts.borderwidth + y0 + menuOpts.pad.t, - w: Math.max(menuOpts.openWidth, menuOpts.headerWidth), - h: menuOpts.openHeight - }; + isVertical = (direction === 'up' || direction === 'down'); var active = menuOpts.active, translateX, translateY, From 109f2849c45b062cd5deea830b8d2c93309c1754 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 3 Feb 2017 10:00:27 +0000 Subject: [PATCH 16/19] updatemenus: fix positioning of scrollbox * Account for the direction of the scrollbox to determine to which side the scrollbox will be anchored. --- src/components/updatemenus/draw.js | 2 + src/components/updatemenus/scrollbox.js | 81 +++++++++++++++---------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index fbcbddedb50..159a0cb81c1 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -358,6 +358,8 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight); } + scrollBoxPosition.direction = menuOpts.direction; + if(scrollBox) { if(buttons.size()) { drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, scrollBoxPosition); diff --git a/src/components/updatemenus/scrollbox.js b/src/components/updatemenus/scrollbox.js index da813ea13ae..6e324d64c71 100644 --- a/src/components/updatemenus/scrollbox.js +++ b/src/components/updatemenus/scrollbox.js @@ -73,6 +73,8 @@ ScrollBox.barColor = '#808BA4'; * @param {number} position.t Top side (in pixels) * @param {number} position.w Width (in pixels) * @param {number} position.h Height (in pixels) + * @param {string} [position.direction='down'] + * Either 'down', 'left', 'right' or 'up' * @param {number} [translateX=0] Horizontal offset (in pixels) * @param {number} [translateY=0] Vertical offset (in pixels) */ @@ -88,48 +90,55 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { w = this.position.w, t = this.position.t, h = this.position.h, + direction = this.position.direction, + isDown = (direction === 'down'), + isLeft = (direction === 'left'), + isRight = (direction === 'right'), + isUp = (direction === 'up'), boxW = w, boxH = h, boxL, boxR, boxT, boxB; - if(boxW > fullWidth) boxW = fullWidth / 4; - if(boxH > fullHeight) boxH = fullHeight / 4; - - var minSize = 4 + (ScrollBox.barLength + 2 * ScrollBox.barPad); - boxW = Math.max(boxW, minSize); - boxH = Math.max(boxH, minSize); + if(!isDown && !isLeft && !isRight && !isUp) { + this.position.direction = 'down'; + isDown = true; + } - if(0 <= l && l <= fullWidth) { + var isVertical = isDown || isUp; + if(isVertical) { boxL = l; boxR = boxL + boxW; - } - else { - // align left - boxL = 0; - boxR = boxL + boxW; - } - if(0 <= t && t <= fullHeight) { - boxT = t; - boxB = boxT + boxH; + if(isDown) { + // anchor to top side + boxT = t; + boxB = Math.min(boxT + boxH, fullHeight); + boxH = boxB - boxT; + } + else { + // anchor to bottom side + boxB = t + boxH; + boxT = Math.max(boxB - boxH, 0); + boxH = boxB - boxT; + } } else { - // align top - boxT = 0; + boxT = t; boxB = boxT + boxH; - } - - if(boxR > fullWidth) { - // align right - boxR = fullWidth; - boxL = boxR - boxW; - } - if(boxB > fullHeight) { - // align bottom - boxB = fullHeight; - boxT = boxB - boxH; + if(isLeft) { + // anchor to right side + boxR = l + boxW; + boxL = Math.max(boxR - boxW, 0); + boxW = boxR - boxL; + } + else { + // anchor to left side + boxL = l; + boxR = Math.min(boxL + boxW, fullWidth); + boxW = boxR - boxL; + } } this._box = { @@ -143,8 +152,11 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { var needsHorizontalScrollBar = (w > boxW), hbarW = ScrollBox.barLength + 2 * ScrollBox.barPad, hbarH = ScrollBox.barWidth + 2 * ScrollBox.barPad, - hbarL = boxL, - hbarT = (boxB + hbarH < fullHeight) ? boxB : fullHeight - hbarH; + // draw horizontal scrollbar on the bottom side + hbarL = l, + hbarT = t + h; + + if(hbarT + hbarH > fullHeight) hbarT = fullHeight - hbarH; var hbar = this.container.selectAll('rect.scrollbar-horizontal').data( (needsHorizontalScrollBar) ? [0] : []); @@ -181,8 +193,11 @@ ScrollBox.prototype.enable = function enable(position, translateX, translateY) { var needsVerticalScrollBar = (h > boxH), vbarW = ScrollBox.barWidth + 2 * ScrollBox.barPad, vbarH = ScrollBox.barLength + 2 * ScrollBox.barPad, - vbarL = (boxR + vbarW < fullWidth) ? boxR : fullWidth - vbarW, - vbarT = boxT; + // draw vertical scrollbar on the right side + vbarL = l + w, + vbarT = t; + + if(vbarL + vbarW > fullWidth) vbarL = fullWidth - vbarW; var vbar = this.container.selectAll('rect.scrollbar-vertical').data( (needsVerticalScrollBar) ? [0] : []); From e7c3ae38d321c4de1341d98a397b326ca8d7e2ec Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 3 Feb 2017 16:00:14 +0000 Subject: [PATCH 17/19] Lib: Fix regexp in getTranslate * Fixed regexp in Lib.getTranslate to accept negative values. --- src/lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/index.js b/src/lib/index.js index b7d1bdefbd2..b9f5dcf0960 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -453,7 +453,7 @@ lib.addStyleRule = function(selector, styleString) { lib.getTranslate = function(element) { - 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') || ''; From 89c615dc35494aff58816c113fa4facdfd8c8c27 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 3 Feb 2017 17:26:27 +0000 Subject: [PATCH 18/19] updatemenus: test scrollbox --- test/jasmine/tests/updatemenus_test.js | 404 +++++++++++++++++++++++++ 1 file changed, 404 insertions(+) diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index a85947445f4..827628b4882 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -9,6 +9,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var TRANSITION_DELAY = 100; var fail = require('../assets/fail_test'); +var getBBox = require('../assets/get_bbox'); describe('update menus defaults', function() { 'use strict'; @@ -800,3 +801,406 @@ describe('update menus interaction with other components:', function() { .then(done); }); }); + + +describe('update menus interaction with scrollbox:', function() { + 'use strict'; + + var gd, + mock, + menuDown, + menuLeft, + menuRight, + menuUp; + + // Adapted from https://github.com/plotly/plotly.js/pull/770#issuecomment-234669383 + mock = { + data: [], + layout: { + width: 1100, + height: 450, + updatemenus: [{ + buttons: [{ + method: 'restyle', + args: ['line.color', 'red'], + label: 'red' + }, { + method: 'restyle', + args: ['line.color', 'blue'], + label: 'blue' + }, { + method: 'restyle', + args: ['line.color', 'green'], + label: 'green' + }] + }, { + x: 0.5, + xanchor: 'left', + y: 0.5, + yanchor: 'top', + direction: 'down', + buttons: [] + }, { + x: 0.5, + xanchor: 'right', + y: 0.5, + yanchor: 'top', + direction: 'left', + buttons: [] + }, { + x: 0.5, + xanchor: 'left', + y: 0.5, + yanchor: 'bottom', + direction: 'right', + buttons: [] + }, { + x: 0.5, + xanchor: 'right', + y: 0.5, + yanchor: 'bottom', + direction: 'up', + buttons: [] + }] + } + }; + + for(var i = 0, n = 20; i < n; i++) { + var j; + + var y; + for(j = 0, y = []; j < 10; j++) y.push(Math.random); + + mock.data.push({ + y: y, + line: { + shape: 'spline', + color: 'red' + }, + visible: i === 0, + name: 'Data set ' + i, + }); + + var visible; + for(j = 0, visible = []; j < n; j++) visible.push(i === j); + + for(j = 1; j <= 4; j++) { + mock.layout.updatemenus[j].buttons.push({ + method: 'restyle', + args: ['visible', visible], + label: 'Data set ' + i + }); + } + } + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + var menus = document.getElementsByClassName('updatemenu-header'); + + expect(menus.length).toBe(5); + + menuDown = menus[1]; + menuLeft = menus[2]; + menuRight = menus[3]; + menuUp = menus[4]; + }).catch(fail).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('scrollbox can be dragged', function() { + var deltaX = -50, + deltaY = -100, + scrollBox, + scrollBar, + scrollBoxTranslate0, + scrollBarTranslate0, + scrollBoxTranslate1, + scrollBarTranslate1; + + scrollBox = getScrollBox(); + expect(scrollBox).toBeDefined(); + + // down menu + click(menuDown); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + dragScrollBox(scrollBox, 0, deltaY); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y + deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); + + // left menu + click(menuLeft); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + dragScrollBox(scrollBox, deltaX, 0); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x + deltaX); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); + + // right menu + click(menuRight); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + dragScrollBox(scrollBox, deltaX, 0); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x + deltaX); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); + + // up menu + click(menuUp); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + dragScrollBox(scrollBox, 0, deltaY); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y + deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); + }); + + it('scrollbox handles wheel events', function() { + var deltaY = 100, + scrollBox, + scrollBar, + scrollBoxTranslate0, + scrollBarTranslate0, + scrollBoxTranslate1, + scrollBarTranslate1; + + scrollBox = getScrollBox(); + expect(scrollBox).toBeDefined(); + + // down menu + click(menuDown); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y - deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); + + // left menu + click(menuLeft); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x - deltaY); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); + + // right menu + click(menuRight); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.x).toEqual(scrollBoxTranslate0.x - deltaY); + expect(scrollBarTranslate1.x).toBeGreaterThan(scrollBarTranslate0.x); + + // up menu + click(menuUp); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxTranslate0 = Lib.getTranslate(scrollBox); + scrollBarTranslate0 = Lib.getTranslate(scrollBar); + wheel(scrollBox, deltaY); + scrollBoxTranslate1 = Lib.getTranslate(scrollBox); + scrollBarTranslate1 = Lib.getTranslate(scrollBar); + + expect(scrollBoxTranslate1.y).toEqual(scrollBoxTranslate0.y - deltaY); + expect(scrollBarTranslate1.y).toBeGreaterThan(scrollBarTranslate0.y); + }); + + it('scrollbar can be dragged', function() { + var deltaX = 20, + deltaY = 10, + scrollBox, + scrollBar, + scrollBoxPosition0, + scrollBarPosition0, + scrollBoxPosition1, + scrollBarPosition1; + + scrollBox = getScrollBox(); + expect(scrollBox).toBeDefined(); + + // down menu + click(menuDown); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Lib.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, 0, deltaY); + scrollBoxPosition1 = Lib.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.y).toBeLessThan(scrollBoxPosition0.y); + expect(scrollBarPosition1.y).toEqual(scrollBarPosition0.y + deltaY); + + // left menu + click(menuLeft); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Lib.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, deltaX, 0); + scrollBoxPosition1 = Lib.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.x).toBeLessThan(scrollBoxPosition0.x); + expect(scrollBarPosition1.x).toEqual(scrollBarPosition0.x + deltaX); + + // right menu + click(menuRight); + + scrollBar = getHorizontalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Lib.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, deltaX, 0); + scrollBoxPosition1 = Lib.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.x).toBeLessThan(scrollBoxPosition0.x); + expect(scrollBarPosition1.x).toEqual(scrollBarPosition0.x + deltaX); + + // up menu + click(menuUp); + + scrollBar = getVerticalScrollBar(); + expect(scrollBar).toBeDefined(); + + scrollBoxPosition0 = Lib.getTranslate(scrollBox); + scrollBarPosition0 = getScrollBarCenter(scrollBox, scrollBar); + dragScrollBar(scrollBar, scrollBarPosition0, 0, deltaY); + scrollBoxPosition1 = Lib.getTranslate(scrollBox); + scrollBarPosition1 = getScrollBarCenter(scrollBox, scrollBar); + + expect(scrollBoxPosition1.y).toBeLessThan(scrollBoxPosition0.y); + expect(scrollBarPosition1.y).toEqual(scrollBarPosition0.y + deltaY); + }); + + function getScrollBox() { + return document.getElementsByClassName('updatemenu-dropdown-button-group')[0]; + } + + function getHorizontalScrollBar() { + return document.getElementsByClassName('scrollbar-horizontal')[0]; + } + + function getVerticalScrollBar() { + return document.getElementsByClassName('scrollbar-vertical')[0]; + } + + function getCenter(node) { + var bbox = getBBox(node), + x = bbox.x + 0.5 * bbox.width, + y = bbox.y + 0.5 * bbox.height; + + return { x: x, y: y }; + } + + function getScrollBarCenter(scrollBox, scrollBar) { + var scrollBoxTranslate = Lib.getTranslate(scrollBox), + scrollBarTranslate = Lib.getTranslate(scrollBar), + translateX = scrollBoxTranslate.x + scrollBarTranslate.x, + translateY = scrollBoxTranslate.y + scrollBarTranslate.y, + center = getCenter(scrollBar), + x = center.x + translateX, + y = center.y + translateY; + + return { x: x, y: y }; + } + + function click(node) { + node.dispatchEvent(new MouseEvent('click'), { + bubbles: true + }); + } + + function drag(node, x, y, deltaX, deltaY) { + node.dispatchEvent(new MouseEvent('mousedown', { + bubbles: true, + clientX: x, + clientY: y + })); + node.dispatchEvent(new MouseEvent('mousemove', { + bubbles: true, + clientX: x + deltaX, + clientY: y + deltaY + })); + } + + function dragScrollBox(node, deltaX, deltaY) { + var position = getCenter(node); + + drag(node, position.x, position.y, deltaX, deltaY); + } + + function dragScrollBar(node, position, deltaX, deltaY) { + drag(node, position.x, position.y, deltaX, deltaY); + } + + function wheel(node, deltaY) { + node.dispatchEvent(new WheelEvent('wheel', { + bubbles: true, + deltaY: deltaY + })); + } +}); From 79f11076a5d7aa3954f3007a1e600cc4801b5a2d Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 3 Feb 2017 19:47:46 +0000 Subject: [PATCH 19/19] Drawing: test setTranslate works with negatives --- test/jasmine/tests/drawing_test.js | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/jasmine/tests/drawing_test.js b/test/jasmine/tests/drawing_test.js index 61671060119..3c20e719a1a 100644 --- a/test/jasmine/tests/drawing_test.js +++ b/test/jasmine/tests/drawing_test.js @@ -113,6 +113,42 @@ describe('Drawing', function() { el.attr('transform', 'rotate(20)'); expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); }); + + it('should work with negative values', function() { + var el = document.createElement('div'), + el3 = d3.select(document.createElement('div')); + + expect(Drawing.getTranslate(el)).toEqual({ x: 0, y: 0 }); + + var testCases = [ + { transform: 'translate(-123.45px, -67)', x: -123.45, y: -67 }, + { transform: 'translate(-123.45px, 67)', x: -123.45, y: 67 }, + { transform: 'translate(123.45px, -67)', x: 123.45, y: -67 }, + { transform: 'translate(-123.45)', x: -123.45, y: 0 }, + { transform: 'translate(-1 -2)', x: -1, y: -2 }, + { transform: 'translate(-1 2)', x: -1, y: 2 }, + { transform: 'translate(1 -2)', x: 1, y: -2 }, + { transform: 'translate(-1 -2); rotate(20deg)', x: -1, y: -2 }, + { transform: 'translate(-1 2); rotate(20deg)', x: -1, y: 2 }, + { transform: 'translate(1 -2); rotate(20deg)', x: 1, y: -2 }, + { transform: 'rotate(20deg) translate(-1 -2);', x: -1, y: -2 }, + { transform: 'rotate(20deg) translate(-1 2);', x: -1, y: 2 }, + { transform: 'rotate(20deg) translate(1 -2);', x: 1, y: -2 } + ]; + + for(var i = 0; i < testCases.length; i++) { + var testCase = testCases[i], + transform = testCase.transform, + x = testCase.x, + y = testCase.y; + + el.setAttribute('transform', transform); + expect(Drawing.getTranslate(el)).toEqual({ x: x, y: y }); + + el3.attr('transform', transform); + expect(Drawing.getTranslate(el)).toEqual({ x: x, y: y }); + } + }); }); describe('setTranslate', function() {