diff --git a/src/components/modebar/modebar.js b/src/components/modebar/modebar.js index 35eec9fc6a6..b66dd2d30fa 100644 --- a/src/components/modebar/modebar.js +++ b/src/components/modebar/modebar.js @@ -56,11 +56,8 @@ 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 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); @@ -129,6 +126,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; }; @@ -246,11 +247,27 @@ 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) ? @@ -258,6 +275,7 @@ proto.updateActiveButton = function(buttonClicked) { Lib.nestedProperty(fullLayout, dataAttr).get(); button3.classed('active', val === thisval); + updateButtonStyle(button, val === thisval); } }); }; @@ -317,7 +335,6 @@ proto.removeAllButtons = function() { proto.destroy = function() { Lib.removeElement(this.container.querySelector('.modebar')); - Lib.deleteRelatedStyleRule(this._uid); }; function createModeBar(gd, buttons) { diff --git a/src/fonts/ploticon.js b/src/fonts/ploticon.js index c19aa81a2f3..f9f496f7ed0 100644 --- a/src/fonts/ploticon.js +++ b/src/fonts/ploticon.js @@ -167,29 +167,19 @@ module.exports = { name: 'newplotlylogo', svg: [ '', - '', - ' ', - '', ' plotly-logomark', ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', ' ', '' ].join('') diff --git a/src/lib/dom.js b/src/lib/dom.js index 6ed44293ea9..b1b06c44314 100644 --- a/src/lib/dom.js +++ b/src/lib/dom.js @@ -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); @@ -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); @@ -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 @@ -162,6 +208,7 @@ module.exports = { addStyleRule: addStyleRule, addRelatedStyleRule: addRelatedStyleRule, deleteRelatedStyleRule: deleteRelatedStyleRule, + setStyleOnHover: setStyleOnHover, getFullTransformMatrix: getFullTransformMatrix, getElementTransformMatrix: getElementTransformMatrix, getElementAndAncestors: getElementAndAncestors, diff --git a/src/lib/index.js b/src/lib/index.js index 3f989fb550c..c2b9b3f695f 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -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; diff --git a/tasks/preprocess.js b/tasks/preprocess.js index 8fc6e9a75f2..39075c178d4 100644 --- a/tasks/preprocess.js +++ b/tasks/preprocess.js @@ -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'); @@ -13,7 +14,7 @@ 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, @@ -21,11 +22,25 @@ function makeBuildCSS() { }, 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 = {}; diff --git a/tasks/util/constants.js b/tasks/util/constants.js index cc29bd49276..caca805805a 100644 --- a/tasks/util/constants.js +++ b/tasks/util/constants.js @@ -221,6 +221,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'),