Add slider input for watermark opacity to theme editor

refs CNVS-22338, CNVS-23413, CNVS-23412

Test plan:
* In Theme Editor, upload a water mark image
* Set the opacity to something other than 50%
* Preview the change and verify the opacity set on the bg image
* Apply the change and verify the opacity on the bg image
* Make sure the watermark image looks ok on as many canvas pages as possible
* Reset to the canvas default theme and verify that the image is removed and the opacity is reset to 50%

Change-Id: Ib6256343d20f9550f3d46c0a55731b9448c8a49c
Reviewed-on: https://gerrit.instructure.com/64471
Reviewed-by: Chris Hart <chart@instructure.com>
Tested-by: Jenkins
Product-Review: Chris Hart <chart@instructure.com>
QA-Review: Nathan Rogowski <nathan@instructure.com>
QA-Review: Myller de Araujo <myller@instructure.com>
This commit is contained in:
Jennifer Stern 2015-10-01 17:52:50 -05:00
parent 16b49a9d43
commit 82399ff885
9 changed files with 300 additions and 7 deletions

View File

@ -13,5 +13,5 @@ These modules are potentially shared but only used in one feature. If
you can use one of them in your feature, please move it into this
directory and update the require paths in the other feature.
- path/to/file.js
- app/jsx/theme_editor/RangeInput.jsx

View File

@ -26,10 +26,15 @@ define([
types.image = React.PropTypes.shape(_.extend({
type: React.PropTypes.oneOf(['image']).isRequired,
accept: React.PropTypes.string.isRequired,
helper_text: React.PropTypes.string.isRequired
helper_text: React.PropTypes.string
}, baseVarDef))
types.varDef = React.PropTypes.oneOfType([types.image, types.color])
types.percentage = React.PropTypes.shape(_.extend({
type: React.PropTypes.oneOf(['percentage']).isRequired,
helper_text: React.PropTypes.string
}, baseVarDef))
types.varDef = React.PropTypes.oneOfType([types.image, types.color, types.percentage])
types.brandConfig = React.PropTypes.shape({
md5: types.md5,

View File

@ -0,0 +1,82 @@
/** @jsx React.DOM */
define([
'react',
'jquery'
], function (React, $) {
var RangeInput = React.createClass({
propTypes: {
min: React.PropTypes.number.isRequired,
max: React.PropTypes.number.isRequired,
defaultValue: React.PropTypes.number.isRequired,
labelText: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired,
step: React.PropTypes.number,
formatValue: React.PropTypes.func,
onChange: React.PropTypes.func
},
getDefaultProps: function() {
return {
step: 1,
onChange: function(){},
formatValue: val => val
};
},
getInitialState: function() {
return { value: this.props.defaultValue };
},
/* workaround for https://github.com/facebook/react/issues/554 */
componentDidMount: function() {
// https://connect.microsoft.com/IE/Feedback/Details/856998
$(this.refs.rangeInput.getDOMNode()).on('input change', this.handleChange);
},
componentWillUnmount: function() {
$(this.refs.rangeInput.getDOMNode()).off('input change', this.handleChange);
},
/* end workaround */
handleChange: function(event) {
this.setState({ value: event.target.value });
this.props.onChange(event.target.value);
},
render: function() {
var {
labelText,
formatValue,
onChange,
value,
...props
} = this.props;
return (
<label className="RangeInput">
<div className="RangeInput__label">
{labelText}
</div>
<div className="RangeInput__control">
<input className="RangeInput__input"
ref="rangeInput"
type="range"
role="slider"
aria-valuenow={this.props.defaultValue}
aria-valuemin={this.props.min}
aria-valuemax={this.props.max}
aria-valuetext={formatValue(this.state.value)}
onChange={function() {}}
{...props} />
<output htmlFor={this.props.name} className="RangeInput__value">
{ formatValue(this.state.value) }
</output>
</div>
</label>
);
}
});
return RangeInput;
});

View File

@ -2,12 +2,14 @@
define([
'react',
'i18n!theme_editor',
'./ThemeEditorColorRow',
'./ThemeEditorImageRow',
'./RangeInput',
'./PropTypes',
'jquery',
'jqueryui/accordion'
], (React, ThemeEditorColorRow, ThemeEditorImageRow, customTypes, $) => {
], (React, I18n, ThemeEditorColorRow, ThemeEditorImageRow, RangeInput, customTypes, $) => {
return React.createClass({
@ -44,8 +46,31 @@ define([
onChange: this.props.changeSomething.bind(null, varDef.variable_name),
placeholder: this.props.getDisplayValue(varDef.variable_name),
varDef: varDef
};
if (varDef.type === 'color') {
return (
<ThemeEditorColorRow {...props} />
);
} else if (varDef.type === 'image') {
return (
<ThemeEditorImageRow {...props} />
);
} else if (varDef.type === 'percentage') {
var defaultValue = props.currentValue || props.placeholder;
return (
<RangeInput labelText={varDef.human_name}
min={0}
max={1}
step={0.1}
defaultValue={defaultValue ? parseFloat(defaultValue) : 0.5}
name={'brand_config[variables][' + varDef.variable_name + ']'}
onChange={value => props.onChange(value)}
formatValue={value => I18n.toPercentage(value * 100, {precision: 0})} />
);
} else {
return null;
}
return varDef.type === 'color' ? ThemeEditorColorRow(props) : ThemeEditorImageRow(props)
},
render() {

View File

@ -119,6 +119,7 @@ define([
},
render() {
var colorInputValue = (this.props.placeholder !== "none") ? this.props.placeholder : null;
return (
<section className="Theme__editor-accordion_element Theme__editor-color ic-Form-control">
<div className="Theme__editor-form--color">
@ -139,7 +140,7 @@ define([
className="Theme__editor-color-block_input-sample Theme__editor-color-block_input"
type="color"
ref="colorpicker"
value={this.props.placeholder}
value={colorInputValue}
role="presentation-only"
onChange={event => this.inputChange(event.target.value) } />
</label>

View File

@ -45,7 +45,17 @@ $ic-left-side-width: $ic-sp*15;
// Theme Editor watermark gets attached to this element as a background image
body:not(.no-headers) & {
background: $ic-brand-watermark left bottom no-repeat;
z-index: 1;
&:before {
content: "";
bottom: 0; left: 0;
width: 100%; height: 100%;
position: absolute;
z-index: -1;
opacity: $ic-brand-watermark-opacity;
background: transparent $ic-brand-watermark left bottom no-repeat;
}
}
body.ic-no-flex-layout & {

View File

@ -1,3 +1,4 @@
[{
"group_name": "Global Branding",
"variables": [{
@ -101,6 +102,12 @@
"type": "image",
"accept": "image/png,image/svg,image/gif,image/jpeg",
"default": ""
},{
"human_name": "Watermark Opacity",
"helper_text": "Specify the transparency of the watermark background image.",
"variable_name": "ic-brand-watermark-opacity",
"type": "percentage",
"default": "1.0"
},{
"human_name": "Favicon",
"helper_text": "You can use a single 16x16, 32x32, 48x48 ico file.",
@ -155,6 +162,7 @@
"human_name": "Background Image",
"variable_name": "ic-brand-Login-body-bgd-image",
"type": "image",
"accept": "image/png,image/svg,image/gif,image/jpeg",
"default": ""
},
{

View File

@ -1,4 +1,5 @@
@import "base/environment";
@import "components/ic-range-input";
//// Vertical content alignment on elements in Theme editor

View File

@ -0,0 +1,161 @@
.RangeInput {
$range-input-handle-color: $ic-brand-primary;
$range-input-handle-color-hover: darken($ic-brand-primary, 10%);
$range-input-handle-border-color-focused: darken($ic-brand-primary, 50%);
$range-input-handle-size: 20px;
$range-input-handle-shadow: 0 3px 6px rgba(black, 0.3);
$range-input-slider-color: $ic-color-neutral;
$range-input-value-bg-color: $ic-color-dark;
$range-input-value-text-color: $ic-color-light;
$range-input-label-color: $ic-color-dark;
$range-input-label-font-size: $h2-font-size;
@mixin range-input-handle-selector() {
&::-webkit-slider-thumb {
@content;
}
&::-ms-thumb {
@content;
}
&::-moz-range-thumb {
@content;
}
}
@mixin range-input-track-selector() {
&::-webkit-slider-runnable-track {
@content;
}
&::-moz-range-track {
@content;
}
&::-ms-track {
@content;
}
}
* {
box-sizing: border-box;
}
width: 100%;
// Style range input
input[type=range] {
-webkit-appearance: none;
width: 100%; // for Firefox
outline: none;
margin: 0; padding: 0; // for IE
// Slider Handle/Thumb
@include range-input-handle-selector {
-webkit-appearance: none;
width: $range-input-handle-size;
height: $range-input-handle-size;
border-radius: 50%;
background: $range-input-handle-color;
box-shadow: $range-input-handle-shadow;
cursor: pointer;
transition: all .15s ease-in-out;
&:hover {
background: $range-input-handle-color-hover;
}
}
&::-webkit-slider-thumb {
margin-top: -$range-input-handle-size/4;
}
&:focus,
&:active {
outline: none;
@include range-input-handle-selector {
box-shadow: $range-input-handle-shadow,
0 0 0 $range-input-handle-size/2 rgba($range-input-handle-color, .15);
}
}
// remove outline in FF
&::-moz-focus-inner,
&::-moz-focus-outer {
border: 0;
outline: none;
}
@include range-input-track-selector {
height: $range-input-handle-size/2;
border-radius: 5px;
background: $range-input-slider-color;
border-color: transparent;
color: transparent;
cursor: pointer;
animate: 0.2s;
}
&::-ms-track {
background: transparent;
border-width: $range-input-handle-size 0;
}
&::-ms-fill-upper,
&::-ms-fill-lower {
background: $range-input-slider-color;
border-radius: $range-input-handle-size/2;
}
&::-ms-tooltip {
display: none;
}
}
// Label text
.RangeInput__label {
margin: 0 0 $ic-sp/2;
display: block;
line-height: 1.3;
font-size: $range-input-label-font-size;
color: $range-input-label-color;
}
// Slider + Tooltip
.RangeInput__control {
display: flex;
justify-content: center;
align-items: center;
}
// Slider
.RangeInput__input {
flex: auto;
}
// Tooltip/Selected Value
.RangeInput__value {
display: inline-block;
position: relative;
width: 70px;
color: $range-input-value-text-color;
font-size: 16px;
line-height: 20px;
text-align: center;
border-radius: 3px;
background: $range-input-value-bg-color;
padding: 5px 10px;
margin-left: 7px;
&:after {
content: "";
position: absolute;
top: 8px;
left: -6px;
width: 0;
height: 0;
border-top: 7px solid transparent;
border-right: 7px solid $range-input-value-bg-color;
border-bottom: 7px solid transparent;
}
}
}