diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 61a29b9fda8..3bd4875456b 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -85,6 +85,8 @@ function drawOne(gd, index) { function drawRaw(gd, options, index, subplotId, xa, ya) { var fullLayout = gd._fullLayout; var gs = gd._fullLayout._size; + var edits = gd._context.edits; + var className; var annbase; @@ -128,8 +130,11 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { var annTextGroup = annGroup.append('g') .classed('annotation-text-g', true); + var editTextPosition = edits[options.showarrow ? 'annotationTail' : 'annotationPosition']; + var textEvents = options.captureevents || edits.annotationText || editTextPosition; + var annTextGroupInner = annTextGroup.append('g') - .style('pointer-events', options.captureevents ? 'all' : null) + .style('pointer-events', textEvents ? 'all' : null) .call(setCursor, 'default') .on('click', function() { gd._dragging = false; @@ -519,7 +524,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { // the arrow dragger is a small square right at the head, then a line to the tail, // all expanded by a stroke width of 6px plus the arrow line width - if(gd._context.editable && arrow.node().parentNode && !subplotId) { + if(edits.annotationPosition && arrow.node().parentNode && !subplotId) { var arrowDragHeadX = headX; var arrowDragHeadY = headY; if(options.standoff) { @@ -601,7 +606,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { if(options.showarrow) drawArrow(0, 0); // user dragging the annotation (text, not arrow) - if(gd._context.editable) { + if(editTextPosition) { var update, baseTextTransform; @@ -679,7 +684,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { } } - if(gd._context.editable) { + if(edits.annotationText) { annText.call(svgTextUtils.makeEditable, {delegate: annTextGroupInner, gd: gd}) .call(textLayout) .on('edit', function(_text) { diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index b31de8e1b58..d817dd84635 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -550,7 +550,7 @@ module.exports = function draw(gd, id) { if(cbDone && cbDone.then) (gd._promises || []).push(cbDone); // dragging... - if(gd._context.editable) { + if(gd._context.edits.colorbarPosition) { var t0, xf, yf; diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 788b664ff09..807f0c97b41 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -312,7 +312,7 @@ module.exports = function draw(gd) { }); } - if(gd._context.editable) { + if(gd._context.edits.legendPosition) { var xf, yf, x0, y0; legend.classed('cursor-move', true); @@ -385,7 +385,7 @@ function drawTexts(g, gd) { }); } - if(gd._context.editable && !isPie) { + if(gd._context.edits.legendText && !isPie) { text.call(svgTextUtils.makeEditable, {gd: gd}) .call(textLayout) .on('edit', function(text) { @@ -597,10 +597,15 @@ function computeTextDimensions(g, gd) { } function computeLegendDimensions(gd, groups, traces) { - var fullLayout = gd._fullLayout, - opts = fullLayout.legend, - borderwidth = opts.borderwidth, - isGrouped = helpers.isGrouped(opts); + var fullLayout = gd._fullLayout; + var opts = fullLayout.legend; + var borderwidth = opts.borderwidth; + var isGrouped = helpers.isGrouped(opts); + + var extraWidth = 0; + + opts.width = 0; + opts.height = 0; if(helpers.isVertical(opts)) { if(isGrouped) { @@ -609,9 +614,6 @@ function computeLegendDimensions(gd, groups, traces) { }); } - opts.width = 0; - opts.height = 0; - traces.each(function(d) { var legendItem = d[0], textHeight = legendItem.height, @@ -632,26 +634,9 @@ function computeLegendDimensions(gd, groups, traces) { opts.height += (opts._lgroupsLength - 1) * opts.tracegroupgap; } - // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); - - traces.each(function(d) { - var legendItem = d[0], - bg = d3.select(this).select('.legendtoggle'); - - bg.call(Drawing.setRect, - 0, - -legendItem.height / 2, - (gd._context.editable ? 0 : opts.width) + 40, - legendItem.height - ); - }); + extraWidth = 40; } else if(isGrouped) { - opts.width = 0; - opts.height = 0; - var groupXOffsets = [opts.width], groupData = groups.data(); @@ -692,26 +677,8 @@ function computeLegendDimensions(gd, groups, traces) { opts.height += 10 + borderwidth * 2; opts.width += borderwidth * 2; - - // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); - - traces.each(function(d) { - var legendItem = d[0], - bg = d3.select(this).select('.legendtoggle'); - - bg.call(Drawing.setRect, - 0, - -legendItem.height / 2, - (gd._context.editable ? 0 : opts.width), - legendItem.height - ); - }); } else { - opts.width = 0; - opts.height = 0; var rowHeight = 0, maxTraceHeight = 0, maxTraceWidth = 0, @@ -750,22 +717,23 @@ function computeLegendDimensions(gd, groups, traces) { opts.width += borderwidth * 2; opts.height += 10 + borderwidth * 2; - // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); + } - traces.each(function(d) { - var legendItem = d[0], - bg = d3.select(this).select('.legendtoggle'); + // make sure we're only getting full pixels + opts.width = Math.ceil(opts.width); + opts.height = Math.ceil(opts.height); - bg.call(Drawing.setRect, - 0, - -legendItem.height / 2, - (gd._context.editable ? 0 : opts.width), - legendItem.height - ); - }); - } + traces.each(function(d) { + var legendItem = d[0], + bg = d3.select(this).select('.legendtoggle'); + + bg.call(Drawing.setRect, + 0, + -legendItem.height / 2, + (gd._context.edits.legendText ? 0 : opts.width) + extraWidth, + legendItem.height + ); + }); } function expandMargin(gd) { diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index bea5bf1108c..468ab55894a 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -114,7 +114,7 @@ function drawOne(gd, index) { null ); - if(gd._context.editable) setupDragElement(gd, path, options, index); + if(gd._context.edits.shapePosition) setupDragElement(gd, path, options, index); } } diff --git a/src/components/titles/index.js b/src/components/titles/index.js index 72278a72d64..a2819784c7b 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -70,7 +70,14 @@ Titles.draw = function(gd, titleClass, options) { var opacity = 1; var isplaceholder = false; var txt = cont.title.trim(); - var editable = gd._context.editable; + + // only make this title editable if we positively identify its property + // as one that has editing enabled. + var editAttr; + if(prop === 'title') editAttr = 'titleText'; + else if(prop.indexOf('axis') !== -1) editAttr = 'axisTitleText'; + else if(prop.indexOf('colorbar' !== -1)) editAttr = 'colorbarTitleText'; + var editable = gd._context.edits[editAttr]; if(txt === '') opacity = 0; if(txt.match(PLACEHOLDER_RE)) { diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index a8ec401eea8..f7fb3e83880 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -397,28 +397,57 @@ function opaqueSetBackground(gd, bgColor) { } function setPlotContext(gd, config) { - if(!gd._context) gd._context = Lib.extendFlat({}, Plotly.defaultConfig); + if(!gd._context) gd._context = Lib.extendDeep({}, Plotly.defaultConfig); var context = gd._context; + var i, keys, key; + if(config) { - Object.keys(config).forEach(function(key) { + keys = Object.keys(config); + for(i = 0; i < keys.length; i++) { + key = keys[i]; + if(key === 'editable' || key === 'edits') continue; if(key in context) { if(key === 'setBackground' && config[key] === 'opaque') { context[key] = opaqueSetBackground; } else context[key] = config[key]; } - }); + } // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility if(config.plot3dPixelRatio && !context.plotGlPixelRatio) { context.plotGlPixelRatio = context.plot3dPixelRatio; } + + // now deal with editable and edits - first editable overrides + // everything, then edits refines + var editable = config.editable; + if(editable !== undefined) { + // we're not going to *use* context.editable, we're only going to + // use context.edits... but keep it for the record + context.editable = editable; + + keys = Object.keys(context.edits); + for(i = 0; i < keys.length; i++) { + context.edits[keys[i]] = editable; + } + } + if(config.edits) { + keys = Object.keys(config.edits); + for(i = 0; i < keys.length; i++) { + key = keys[i]; + if(key in context.edits) { + context.edits[key] = config.edits[key]; + } + } + } } // staticPlot forces a bunch of others: if(context.staticPlot) { context.editable = false; + context.edits = {}; context.autosizable = false; context.scrollZoom = false; context.doubleClick = false; @@ -487,7 +516,7 @@ function plotPolar(gd, data, layout) { var title = polarPlotSVG.select('.title-group text') .call(titleLayout); - if(gd._context.editable) { + if(gd._context.edits.titleText) { if(!txt || txt === placeholderText) { opacity = 0.2; // placeholder is not going through convertToTspans diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 71871dad6be..c19d0d3ebc8 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -23,8 +23,27 @@ module.exports = { // no interactivity, for export or image generation staticPlot: false, - // we can edit titles, move annotations, etc + // we can edit titles, move annotations, etc - sets all pieces of `edits` + // unless a separate `edits` config item overrides individual parts editable: false, + edits: { + // annotationPosition: the main anchor of the annotation, which is the + // text (if no arrow) or the arrow (which drags the whole thing leaving + // the arrow length & direction unchanged) + annotationPosition: false, + // just for annotations with arrows, change the length and direction of the arrow + annotationTail: false, + annotationText: false, + axisTitleText: false, + colorbarPosition: false, + colorbarTitleText: false, + legendPosition: false, + // edit the trace name fields from the legend + legendText: false, + shapePosition: false, + // the global `layout.title` + titleText: false + }, // DO autosize once regardless of layout.autosize // (use default width or height values otherwise) diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 5ff20cfda94..951d1b4dba2 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -181,10 +181,15 @@ describe('config argument', function() { var gd; - beforeEach(function(done) { + beforeEach(function() { gd = createGraphDiv(); + }); + + function initPlot(editFlag) { + var edits = {}; + edits[editFlag] = true; - Plotly.plot(gd, [ + return Plotly.plot(gd, [ { x: [1, 2, 3], y: [1, 2, 3] }, { x: [1, 2, 3], y: [3, 2, 1] } ], { @@ -193,77 +198,157 @@ describe('config argument', function() { annotations: [ { text: 'testing', x: 1, y: 1, showarrow: true } ] - }, { editable: true }) - .then(done); - }); + }, { editable: false, edits: edits }); + } afterEach(destroyGraphDiv); function checkIfEditable(elClass, text) { - var label = document.getElementsByClassName(elClass)[0]; + return function() { + var label = document.getElementsByClassName(elClass)[0]; - expect(label.textContent).toBe(text); + expect(label.textContent).toBe(text); - var labelBox = label.getBoundingClientRect(), - labelX = labelBox.left + labelBox.width / 2, - labelY = labelBox.top + labelBox.height / 2; + var labelBox = label.getBoundingClientRect(), + labelX = labelBox.left + labelBox.width / 2, + labelY = labelBox.top + labelBox.height / 2; - mouseEvent('click', labelX, labelY); + mouseEvent('click', labelX, labelY); - var editBox = document.getElementsByClassName('plugin-editable editable')[0]; - expect(editBox).toBeDefined(); - expect(editBox.textContent).toBe(text); - expect(editBox.getAttribute('contenteditable')).toBe('true'); + var editBox = document.getElementsByClassName('plugin-editable editable')[0]; + expect(editBox).toBeDefined(); + expect(editBox.textContent).toBe(text); + expect(editBox.getAttribute('contenteditable')).toBe('true'); + }; } function checkIfDraggable(elClass) { - var el = document.getElementsByClassName(elClass)[0]; + return function() { + var el = document.getElementsByClassName(elClass)[0]; - var elBox = el.getBoundingClientRect(), - elX = elBox.left + elBox.width / 2, - elY = elBox.top + elBox.height / 2; + var elBox = el.getBoundingClientRect(), + elX = elBox.left + elBox.width / 2, + elY = elBox.top + elBox.height / 2; - mouseEvent('mousedown', elX, elY); - mouseEvent('mousemove', elX - 20, elY + 20); + mouseEvent('mousedown', elX, elY); + mouseEvent('mousemove', elX - 20, elY + 20); - var movedBox = el.getBoundingClientRect(); + var movedBox = el.getBoundingClientRect(); - expect(movedBox.left).toBe(elBox.left - 20); - expect(movedBox.top).toBe(elBox.top + 20); + expect(movedBox.left).toBe(elBox.left - 20); + expect(movedBox.top).toBe(elBox.top + 20); - mouseEvent('mouseup', elX - 20, elY + 20); + mouseEvent('mouseup', elX - 20, elY + 20); + }; } - it('should make titles editable', function() { - checkIfEditable('gtitle', 'Click to enter Plot title'); + it('should let edits override editable', function(done) { + var data = [{y: [1, 2, 3]}]; + var layout = {width: 600, height: 400}; + Plotly.newPlot(gd, data, layout, {editable: true}) + .then(function() { + expect(gd._context.edits).toEqual({ + annotationPosition: true, + annotationTail: true, + annotationText: true, + axisTitleText: true, + colorbarPosition: true, + colorbarTitleText: true, + legendPosition: true, + legendText: true, + shapePosition: true, + titleText: true + }); + }) + .then(function() { + return Plotly.newPlot(gd, data, layout, { + edits: {annotationPosition: false, annotationTail: false}, + editable: true + }); + }) + .then(function() { + expect(gd._context.edits).toEqual({ + annotationPosition: false, + annotationTail: false, + annotationText: true, + axisTitleText: true, + colorbarPosition: true, + colorbarTitleText: true, + legendPosition: true, + legendText: true, + shapePosition: true, + titleText: true + }); + }) + .then(function() { + return Plotly.newPlot(gd, data, layout, { + edits: {annotationText: true, axisTitleText: true}, + editable: false + }); + }) + .then(function() { + expect(gd._context.edits).toEqual({ + annotationPosition: false, + annotationTail: false, + annotationText: true, + axisTitleText: true, + colorbarPosition: false, + colorbarTitleText: false, + legendPosition: false, + legendText: false, + shapePosition: false, + titleText: false + }); + }) + .then(done); + }); + + it('should make titles editable', function(done) { + initPlot('titleText') + .then(checkIfEditable('gtitle', 'Click to enter Plot title')) + .then(done); }); - it('should make x axes labels editable', function() { - checkIfEditable('g-xtitle', 'Click to enter X axis title'); + it('should make x axes labels editable', function(done) { + initPlot('axisTitleText') + .then(checkIfEditable('g-xtitle', 'Click to enter X axis title')) + .then(done); }); - it('should make y axes labels editable', function() { - checkIfEditable('g-ytitle', 'Click to enter Y axis title'); + it('should make y axes labels editable', function(done) { + initPlot('axisTitleText') + .then(checkIfEditable('g-ytitle', 'Click to enter Y axis title')) + .then(done); }); - it('should make legend labels editable', function() { - checkIfEditable('legendtext', 'trace 0'); + it('should make legend labels editable', function(done) { + initPlot('legendText') + .then(checkIfEditable('legendtext', 'trace 0')) + .then(done); }); - it('should make annotation labels editable', function() { - checkIfEditable('annotation-text-g', 'testing'); + it('should make annotation labels editable', function(done) { + initPlot('annotationText') + .then(checkIfEditable('annotation-text-g', 'testing')) + .then(done); }); - it('should make annotation labels draggable', function() { - checkIfDraggable('annotation-text-g'); + it('should make annotation labels draggable', function(done) { + initPlot('annotationTail') + .then(checkIfDraggable('annotation-text-g')) + .then(done); }); - it('should make annotation arrows draggable', function() { - checkIfDraggable('annotation-arrow-g'); + it('should make annotation arrows draggable', function(done) { + initPlot('annotationPosition') + .then(checkIfDraggable('annotation-arrow-g')) + .then(done); }); - it('should make legends draggable', function() { - checkIfDraggable('legend'); + it('should make legends draggable', function(done) { + initPlot('legendPosition') + .then(checkIfDraggable('legend')) + .then(done); }); });