diff --git a/src/traces/table/data_preparation_helper.js b/src/traces/table/data_preparation_helper.js index 34a158a9584..9ccc81602b4 100644 --- a/src/traces/table/data_preparation_helper.js +++ b/src/traces/table/data_preparation_helper.js @@ -58,7 +58,8 @@ module.exports = function calc(gd, trace) { var maxLineWidth = Math.max(arrayMax(trace.header.line.width), arrayMax(trace.cells.line.width)); var calcdata = { - key: trace.index, + // include staticPlot in the key so if it changes we delete and redraw + key: trace.uid + gd._context.staticPlot, translateX: domain.x[0] * gd._fullLayout._size.w, translateY: gd._fullLayout._size.h * (1 - domain.y[1]), size: gd._fullLayout._size, diff --git a/src/traces/table/plot.js b/src/traces/table/plot.js index f17a926d7cd..18c6f7f6867 100644 --- a/src/traces/table/plot.js +++ b/src/traces/table/plot.js @@ -20,6 +20,7 @@ var splitData = require('./data_split_helpers'); var Color = require('../../components/color'); module.exports = function plot(gd, wrappedTraceHolders) { + var dynamic = !gd._context.staticPlot; var table = gd._fullLayout._paper.selectAll('.' + c.cn.table) .data(wrappedTraceHolders.map(function(wrappedTraceHolder) { @@ -51,20 +52,30 @@ module.exports = function plot(gd, wrappedTraceHolders) { var tableControlView = table.selectAll('.' + c.cn.tableControlView) .data(gup.repeat, gup.keyFun); - tableControlView.enter() + var cvEnter = tableControlView.enter() .append('g') .classed(c.cn.tableControlView, true) - .style('box-sizing', 'content-box') - .on('mousemove', function(d) {tableControlView.filter(function(dd) {return d === dd;}).call(renderScrollbarKit, gd);}) - .on('mousewheel', function(d) { - if(d.scrollbarState.wheeling) return; - d.scrollbarState.wheeling = true; - d3.event.stopPropagation(); - d3.event.preventDefault(); - makeDragRow(gd, tableControlView, null, d.scrollY + d3.event.deltaY)(d); - d.scrollbarState.wheeling = false; - }) - .call(renderScrollbarKit, gd, true); + .style('box-sizing', 'content-box'); + if(dynamic) { + cvEnter + .on('mousemove', function(d) { + tableControlView + .filter(function(dd) {return d === dd;}) + .call(renderScrollbarKit, gd); + }) + .on('mousewheel', function(d) { + if(d.scrollbarState.wheeling) return; + d.scrollbarState.wheeling = true; + var newY = d.scrollY + d3.event.deltaY; + var noChange = makeDragRow(gd, tableControlView, null, newY)(d); + if(!noChange) { + d3.event.stopPropagation(); + d3.event.preventDefault(); + } + d.scrollbarState.wheeling = false; + }) + .call(renderScrollbarKit, gd, true); + } tableControlView .attr('transform', function(d) {return 'translate(' + d.size.l + ' ' + d.size.t + ')';}); @@ -96,9 +107,10 @@ module.exports = function plot(gd, wrappedTraceHolders) { yColumn.exit().remove(); - yColumn - .attr('transform', function(d) {return 'translate(' + d.x + ' 0)';}) - .call(d3.behavior.drag() + yColumn.attr('transform', function(d) {return 'translate(' + d.x + ' 0)';}); + + if(dynamic) { + yColumn.call(d3.behavior.drag() .origin(function(d) { var movedColumn = d3.select(this); easeColumn(movedColumn, d, -c.uplift); @@ -137,6 +149,7 @@ module.exports = function plot(gd, wrappedTraceHolders) { columnMoved(gd, p, p.columns.map(function(dd) {return dd.xIndex;})); }) ); + } yColumn.each(function(d) { Drawing.setClipUrl(d3.select(this), columnBoundaryClipKey(gd, d), gd); @@ -158,8 +171,8 @@ module.exports = function plot(gd, wrappedTraceHolders) { var headerColumnBlock = columnBlock.filter(headerBlock); var cellsColumnBlock = columnBlock.filter(cellsBlock); - cellsColumnBlock - .call(d3.behavior.drag() + if(dynamic) { + cellsColumnBlock.call(d3.behavior.drag() .origin(function(d) { d3.event.stopPropagation(); return d; @@ -169,6 +182,7 @@ module.exports = function plot(gd, wrappedTraceHolders) { // fixme emit plotly notification }) ); + } // initial rendering: header is rendered first, as it may may have async LaTeX (show header first) // but blocks are _entered_ the way they are due to painter's algo (header on top) @@ -711,13 +725,20 @@ function updateBlockYPosition(gd, cellsColumnBlock, tableControlView) { function makeDragRow(gd, allTableControlView, optionalMultiplier, optionalPosition) { return function dragRow(eventD) { - // may come from whicever DOM event target: drag, wheel, bar... eventD corresponds to event target + // may come from whichever DOM event target: drag, wheel, bar... eventD corresponds to event target var d = eventD.calcdata ? eventD.calcdata : eventD; var tableControlView = allTableControlView.filter(function(dd) {return d.key === dd.key;}); var multiplier = optionalMultiplier || d.scrollbarState.dragMultiplier; + + var initialScrollY = d.scrollY; + d.scrollY = optionalPosition === void(0) ? d.scrollY + multiplier * d3.event.dy : optionalPosition; var cellsColumnBlock = tableControlView.selectAll('.' + c.cn.yColumn).selectAll('.' + c.cn.columnBlock).filter(cellsBlock); updateBlockYPosition(gd, cellsColumnBlock, tableControlView); + + // return false if we've "used" the scroll, ie it did something, + // so the event shouldn't bubble (if appropriate) + return d.scrollY === initialScrollY; }; } diff --git a/test/jasmine/assets/mouse_event.js b/test/jasmine/assets/mouse_event.js index f8316c5c564..647063fe3cd 100644 --- a/test/jasmine/assets/mouse_event.js +++ b/test/jasmine/assets/mouse_event.js @@ -36,8 +36,11 @@ module.exports = function(type, x, y, opts) { var el = (opts && opts.element) || document.elementFromPoint(x, y), ev; - if(type === 'scroll') { - ev = new window.WheelEvent('wheel', Lib.extendFlat({}, fullOpts, opts)); + if(type === 'scroll' || type === 'mousewheel') { + // somehow table needs this to be mouswheel but others need wheel. + // yet they all work the same in the browser? + type = (type === 'scroll') ? 'wheel' : 'mousewheel'; + ev = new window.WheelEvent(type, Lib.extendFlat({}, fullOpts, opts)); } else { ev = new window.MouseEvent(type, fullOpts); } diff --git a/test/jasmine/tests/table_test.js b/test/jasmine/tests/table_test.js index 54b6f3dbe39..4bcaaf8d369 100644 --- a/test/jasmine/tests/table_test.js +++ b/test/jasmine/tests/table_test.js @@ -9,6 +9,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); var supplyAllDefaults = require('../assets/supply_defaults'); +var mouseEvent = require('../assets/mouse_event'); var mockMulti = require('@mocks/table_latex_multitrace_scatter.json'); @@ -385,8 +386,7 @@ describe('table', function() { }); describe('more restyling tests with scenegraph queries', function() { - var mockCopy, - gd; + var mockCopy, gd; beforeEach(function(done) { mockCopy = Lib.extendDeep({}, mock2); @@ -425,4 +425,102 @@ describe('table', function() { .then(done); }); }); + + describe('scroll effects', function() { + var mockCopy, gd, gdWheelEventCount; + + beforeEach(function(done) { + mockCopy = Lib.extendDeep({}, mockMulti); + gd = createGraphDiv(); + gdWheelEventCount = 0; + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .then(function() { + gd.addEventListener('mousewheel', function(evt) { + gdWheelEventCount++; + // make sure we don't *really* scroll the page. + // that would be annoying! + evt.stopPropagation(); + evt.preventDefault(); + }); + }) + .then(done); + }); + + function assertBubbledEvents(cnt) { + expect(gdWheelEventCount).toBe(cnt); + gdWheelEventCount = 0; + } + + function getCenter(el) { + var bBox = el.getBoundingClientRect(); + // actually above center, since these bboxes are bigger than the + // visible table area + return {x: bBox.left + bBox.width / 2, y: bBox.top + bBox.height / 4}; + } + + function scroll(pos, dy) { + mouseEvent('mousemove', pos.x, pos.y); + mouseEvent('mousewheel', pos.x, pos.y, {deltaX: 0, deltaY: dy}); + } + + it('bubbles scroll events iff they did not scroll a table', function() { + var allTableControls = document.querySelectorAll('.' + cn.tableControlView); + var smallCenter = getCenter(allTableControls[0]); + var bigCenter = getCenter(allTableControls[1]); + + // table with no scroll bars - events always bubble + scroll(smallCenter, -20); + assertBubbledEvents(1); + scroll(smallCenter, 20); + assertBubbledEvents(1); + + // table with scrollbars - events bubble if we don't use them + scroll(bigCenter, -20); // up from the top - bubbles + assertBubbledEvents(1); + scroll(bigCenter, 20); // down from the top - scrolled, so no bubble + assertBubbledEvents(0); + scroll(bigCenter, -40); // back to the top, with extra dy discarded + assertBubbledEvents(0); + scroll(bigCenter, -20); // now it bubbles + assertBubbledEvents(1); + scroll(bigCenter, 200000); // all the way to the bottom + assertBubbledEvents(0); + scroll(bigCenter, 20); // now it bubbles from the bottom.. + assertBubbledEvents(1); + }); + + it('does not scroll any tables with staticPlot', function(done) { + var allTableControls = document.querySelectorAll('.' + cn.tableControlView); + var bigCenter = getCenter(allTableControls[1]); + + var mock = Lib.extendDeep({}, mockMulti); + + // make sure initially with staticPlot: false, scrolling works + // (copied from previous test) + scroll(bigCenter, 20); + assertBubbledEvents(0); + scroll(bigCenter, -40); + assertBubbledEvents(0); + + Plotly.react(gd, mock.data, mock.layout, {staticPlot: true}) + .then(function() { + // now the same scrolls bubble + scroll(bigCenter, 20); + assertBubbledEvents(1); + scroll(bigCenter, -40); + assertBubbledEvents(1); + + return Plotly.react(gd, mock.data, mock.layout, {staticPlot: false}); + }) + .then(function() { + // scroll works again! + scroll(bigCenter, 20); + assertBubbledEvents(0); + scroll(bigCenter, -40); + assertBubbledEvents(0); + }) + .catch(failTest) + .then(done); + }); + }); });