Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7109_fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Remove inline styles that break plots in strict CSP setups [[#7109](https://github.com/plotly/plotly.js/pull/7109)]
37 changes: 29 additions & 8 deletions src/components/modebar/modebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,14 @@ proto.update = function(graphInfo, buttons) {
}

var style = fullLayout.modebar;
var bgSelector = context.displayModeBar === 'hover' ? '.js-plotly-plot .plotly:hover ' : '';

Lib.deleteRelatedStyleRule(modeBarId);
Lib.addRelatedStyleRule(modeBarId, bgSelector + '#' + modeBarId + ' .modebar-group', 'background-color: ' + style.bgcolor);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn .icon path', 'fill: ' + style.color);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn:hover .icon path', 'fill: ' + style.activecolor);
Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn.active .icon path', 'fill: ' + style.activecolor);
// set style for modebar-group directly instead of inline CSS that's not allowed by strict CSP's
var groupSelector = '#' + modeBarId + ' .modebar-group';
document.querySelectorAll(groupSelector).forEach(function(group) {
group.style.backgroundColor = style.bgcolor;
});
// set styles on hover using event listeners instead of inline CSS that's not allowed by strict CSP's
Lib.setStyleOnHover('#' + modeBarId + ' .modebar-btn', '.active', '.icon path', 'fill: ' + style.activecolor, 'fill: ' + style.color);

// if buttons or logo have changed, redraw modebar interior
var needsNewButtons = !this.hasButtons(buttons);
Expand Down Expand Up @@ -129,6 +130,10 @@ proto.updateButtons = function(buttons) {
proto.createGroup = function() {
var group = document.createElement('div');
group.className = 'modebar-group';

var style = this.graphInfo._fullLayout.modebar;
group.style.backgroundColor = style.bgcolor;

return group;
};

Expand Down Expand Up @@ -246,18 +251,35 @@ proto.updateActiveButton = function(buttonClicked) {
var isToggleButton = (button.getAttribute('data-toggle') === 'true');
var button3 = d3.select(button);

// set style on button based on its state at the moment this is called
// (e.g. during the handling when a modebar button is clicked)
var updateButtonStyle = function(button, isActive) {
var style = fullLayout.modebar;
var childEl = button.querySelector('.icon path');
if(childEl) {
if(isActive || button.matches(':hover')) {
childEl.style.fill = style.activecolor;
} else {
childEl.style.fill = style.color;
}
}
};

// Use 'data-toggle' and 'buttonClicked' to toggle buttons
// that have no one-to-one equivalent in fullLayout
if(isToggleButton) {
if(dataAttr === dataAttrClicked) {
button3.classed('active', !button3.classed('active'));
var isActive = !button3.classed('active');
button3.classed('active', isActive);
updateButtonStyle(button, isActive);
}
} else {
var val = (dataAttr === null) ?
dataAttr :
Lib.nestedProperty(fullLayout, dataAttr).get();

button3.classed('active', val === thisval);
updateButtonStyle(button, val === thisval);
}
});
};
Expand Down Expand Up @@ -317,7 +339,6 @@ proto.removeAllButtons = function() {

proto.destroy = function() {
Lib.removeElement(this.container.querySelector('.modebar'));
Lib.deleteRelatedStyleRule(this._uid);
};

function createModeBar(gd, buttons) {
Expand Down
32 changes: 11 additions & 21 deletions src/fonts/ploticon.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,29 +167,19 @@ module.exports = {
name: 'newplotlylogo',
svg: [
'<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 132 132\'>',
'<defs>',
' <style>',
' .cls-0{fill:#000;}',
' .cls-1{fill:#FFF;}',
' .cls-2{fill:#F26;}',
' .cls-3{fill:#D69;}',
' .cls-4{fill:#BAC;}',
' .cls-5{fill:#9EF;}',
' </style>',
'</defs>',
' <title>plotly-logomark</title>',
' <g id=\'symbol\'>',
' <rect class=\'cls-0\' x=\'0\' y=\'0\' width=\'132\' height=\'132\' rx=\'18\' ry=\'18\'/>',
' <circle class=\'cls-5\' cx=\'102\' cy=\'30\' r=\'6\'/>',
' <circle class=\'cls-4\' cx=\'78\' cy=\'30\' r=\'6\'/>',
' <circle class=\'cls-4\' cx=\'78\' cy=\'54\' r=\'6\'/>',
' <circle class=\'cls-3\' cx=\'54\' cy=\'30\' r=\'6\'/>',
' <circle class=\'cls-2\' cx=\'30\' cy=\'30\' r=\'6\'/>',
' <circle class=\'cls-2\' cx=\'30\' cy=\'54\' r=\'6\'/>',
' <path class=\'cls-1\' d=\'M30,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,30,72Z\'/>',
' <path class=\'cls-1\' d=\'M78,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,78,72Z\'/>',
' <path class=\'cls-1\' d=\'M54,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,54,48Z\'/>',
' <path class=\'cls-1\' d=\'M102,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,102,48Z\'/>',
' <rect fill=\'#000\' x=\'0\' y=\'0\' width=\'132\' height=\'132\' rx=\'18\' ry=\'18\'/>',
' <circle fill=\'#9EF\' cx=\'102\' cy=\'30\' r=\'6\'/>',
' <circle fill=\'#BAC\' cx=\'78\' cy=\'30\' r=\'6\'/>',
' <circle fill=\'#BAC\' cx=\'78\' cy=\'54\' r=\'6\'/>',
' <circle fill=\'#D69\' cx=\'54\' cy=\'30\' r=\'6\'/>',
' <circle fill=\'#F26\' cx=\'30\' cy=\'30\' r=\'6\'/>',
' <circle fill=\'#F26\' cx=\'30\' cy=\'54\' r=\'6\'/>',
' <path fill=\'#FFF\' d=\'M30,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,30,72Z\'/>',
' <path fill=\'#FFF\' d=\'M78,72a6,6,0,0,0-6,6v24a6,6,0,0,0,12,0V78A6,6,0,0,0,78,72Z\'/>',
' <path fill=\'#FFF\' d=\'M54,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,54,48Z\'/>',
' <path fill=\'#FFF\' d=\'M102,48a6,6,0,0,0-6,6v48a6,6,0,0,0,12,0V54A6,6,0,0,0,102,48Z\'/>',
' </g>',
'</svg>'
].join('')
Expand Down
49 changes: 48 additions & 1 deletion src/lib/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ function addStyleRule(selector, styleString) {
function addRelatedStyleRule(uid, selector, styleString) {
var id = 'plotly.js-style-' + uid;
var style = document.getElementById(id);
if(style && style.matches('.no-inline-styles')) {
// Do not proceed if user disable inline styles explicitly...
return;
}
if(!style) {
style = document.createElement('style');
style.setAttribute('id', id);
Expand All @@ -69,7 +73,9 @@ function addRelatedStyleRule(uid, selector, styleString) {
}
var styleSheet = style.sheet;

if(styleSheet.insertRule) {
if(!styleSheet) {
loggers.warn('Cannot addRelatedStyleRule, probably due to strict CSP...');
} else if(styleSheet.insertRule) {
styleSheet.insertRule(selector + '{' + styleString + '}', 0);
} else if(styleSheet.addRule) {
styleSheet.addRule(selector, styleString, 0);
Expand All @@ -85,6 +91,46 @@ function deleteRelatedStyleRule(uid) {
if(style) removeElement(style);
}

/**
* Setup event listeners on button elements to emulate the ':hover' state without using inline styles,
* which is not allowed with strict CSP. This supports modebar buttons set with the 'active' class,
* in which case, the active style remains even when it's no longer hovered.
* @param {string} selector selector for button elements to be styled when hovered
* @param {string} activeSelector selector used to determine if selected element is active
* @param {string} childSelector the child element on which the styling needs to be updated
* @param {string} activeStyle style that has to be applied when 'hovered' or 'active'
* @param {string} inactiveStyle style that has to be applied when not 'hovered' nor 'active'
*/
function setStyleOnHover(selector, activeSelector, childSelector, activeStyle, inactiveStyle) {
var activeStyleParts = activeStyle.split(':');
var inactiveStyleParts = inactiveStyle.split(':');
var eventAddedAttrName = 'data-btn-style-event-added';

document.querySelectorAll(selector).forEach(function(el) {
if(!el.getAttribute(eventAddedAttrName)) {
// Emulate ":hover" CSS style using JS event handlers to set the
// style in a strict CSP-compliant manner.
el.addEventListener('mouseenter', function() {
var childEl = this.querySelector(childSelector);
if(childEl) {
childEl.style[activeStyleParts[0]] = activeStyleParts[1];
}
});
el.addEventListener('mouseleave', function() {
var childEl = this.querySelector(childSelector);
if(childEl) {
if(activeSelector && this.matches(activeSelector)) {
childEl.style[activeStyleParts[0]] = activeStyleParts[1];
} else {
childEl.style[inactiveStyleParts[0]] = inactiveStyleParts[1];
}
}
});
el.setAttribute(eventAddedAttrName, true);
}
});
}

function getFullTransformMatrix(element) {
var allElements = getElementAndAncestors(element);
// the identity matrix
Expand Down Expand Up @@ -162,6 +208,7 @@ module.exports = {
addStyleRule: addStyleRule,
addRelatedStyleRule: addRelatedStyleRule,
deleteRelatedStyleRule: deleteRelatedStyleRule,
setStyleOnHover: setStyleOnHover,
getFullTransformMatrix: getFullTransformMatrix,
getElementTransformMatrix: getElementTransformMatrix,
getElementAndAncestors: getElementAndAncestors,
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ lib.removeElement = domModule.removeElement;
lib.addStyleRule = domModule.addStyleRule;
lib.addRelatedStyleRule = domModule.addRelatedStyleRule;
lib.deleteRelatedStyleRule = domModule.deleteRelatedStyleRule;
lib.setStyleOnHover = domModule.setStyleOnHover;
lib.getFullTransformMatrix = domModule.getFullTransformMatrix;
lib.getElementTransformMatrix = domModule.getElementTransformMatrix;
lib.getElementAndAncestors = domModule.getElementAndAncestors;
Expand Down
19 changes: 17 additions & 2 deletions tasks/preprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var path = require('path');
var sass = require('sass');

var constants = require('./util/constants');
var mapBoxGLStyleRules = require('./../src/plots/mapbox/constants').styleRules;
var common = require('./util/common');
var pullCSS = require('./util/pull_css');
var updateVersion = require('./util/update_version');
Expand All @@ -13,19 +14,33 @@ exposePartsInLib();
copyTopojsonFiles();
updateVersion(constants.pathToPlotlyVersion);

// convert scss to css to js
// convert scss to css to js and static css file
function makeBuildCSS() {
sass.render({
file: constants.pathToSCSS,
outputStyle: 'compressed'
}, function(err, result) {
if(err) throw err;

// css to js
// To support application with strict CSP where styles cannot be inlined,
// build a static CSS file that can be included into such applications.
var staticCSS = String(result.css);
for(var k in mapBoxGLStyleRules) {
staticCSS = addAdditionalCSSRules(staticCSS, '.js-plotly-plot .plotly .mapboxgl-' + k, mapBoxGLStyleRules[k]);
}
fs.writeFile(constants.pathToCSSDist, staticCSS, function(err) {
if(err) throw err;
});

// css to js to be inlined
pullCSS(String(result.css), constants.pathToCSSBuild);
});
}

function addAdditionalCSSRules(staticStyleString, selector, style) {
return staticStyleString + selector + '{' + style + '}';
}

function exposePartsInLib() {
var obj = {};

Expand Down
1 change: 1 addition & 0 deletions tasks/util/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ module.exports = {

pathToSCSS: path.join(pathToSrc, 'css/style.scss'),
pathToCSSBuild: path.join(pathToBuild, 'plotcss.js'),
pathToCSSDist: path.join(pathToDist, 'plotly.css'),

pathToTestDashboardBundle: path.join(pathToBuild, 'test_dashboard-bundle.js'),
pathToReglCodegenBundle: path.join(pathToBuild, 'regl_codegen-bundle.js'),
Expand Down
35 changes: 2 additions & 33 deletions test/jasmine/tests/modebar_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1544,33 +1544,6 @@ describe('ModeBar', function() {
expect(style.fill).toBe(color);
}

function getStyleRule() {
var uid = gd._fullLayout._uid;
var ownerNode = document.getElementById('plotly.js-style-modebar-' + uid);
var styleSheets = document.styleSheets;
for(var i = 0; i < styleSheets.length; i++) {
var ss = styleSheets[i];
if(ss.ownerNode === ownerNode) return ss;
}
}

it('create an associated style element and destroy it on purge', function(done) {
var styleSelector, style;
Plotly.newPlot(gd, [], {})
.then(function() {
styleSelector = 'style[id*="modebar-' + gd._fullLayout._uid + '"]';

style = document.querySelector(styleSelector);
expect(style).toBeTruthy();
})
.then(function() {
Plotly.purge(gd);
style = document.querySelector(styleSelector);
expect(style).toBeNull();
})
.then(done, done.fail);
});

it('changes icon colors', function(done) {
Plotly.newPlot(gd, [], {modebar: { color: colors[0]}})
.then(function() {
Expand Down Expand Up @@ -1602,14 +1575,12 @@ describe('ModeBar', function() {
Plotly.newPlot(gd, [], {modebar: { bgcolor: colors[0]}})
.then(function() {
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[0]);
expect(style.backgroundColor).toBe(colors[0]);
})
.then(function() { return Plotly.relayout(gd, 'modebar.bgcolor', colors[1]); })
.then(function() {
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)');
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[1]);
expect(style.backgroundColor).toBe(colors[1]);
})
.then(done, done.fail);
});
Expand All @@ -1619,13 +1590,11 @@ describe('ModeBar', function() {
.then(function() {
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
expect(style.backgroundColor).toBe(colors[0]);
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[0]);
})
.then(function() { return Plotly.relayout(gd, 'modebar.bgcolor', colors[1]); })
.then(function() {
style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group'));
expect(style.backgroundColor).toBe(colors[1]);
expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[1]);
})
.then(done, done.fail);
});
Expand Down