mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
commit
fb11d5dd5e
15 changed files with 551 additions and 35 deletions
|
@ -56,6 +56,7 @@
|
|||
"minilog": "3.1.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"paper": "0.11.4",
|
||||
"parse-color": "1.0.0",
|
||||
"postcss-import": "^10.0.0",
|
||||
"postcss-loader": "^2.0.5",
|
||||
"postcss-simple-vars": "^4.0.0",
|
||||
|
@ -65,6 +66,7 @@
|
|||
"react-dom": "16.0.0",
|
||||
"react-intl": "2.4.0",
|
||||
"react-intl-redux": "0.6.0",
|
||||
"react-popover": "0.5.4",
|
||||
"react-redux": "5.0.5",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"redux": "3.7.0",
|
||||
|
|
32
src/components/color-button.css
Normal file
32
src/components/color-button.css
Normal file
|
@ -0,0 +1,32 @@
|
|||
.color-button {
|
||||
height: 2rem;
|
||||
width: 3rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.color-button-swatch {
|
||||
display: flex;
|
||||
flex-basis: 2rem;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.color-button-arrow {
|
||||
display: flex;
|
||||
flex-basis: 1rem;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.25);
|
||||
border-left: none;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #575e75;
|
||||
font-size: 0.75rem;
|
||||
}
|
26
src/components/color-button.jsx
Normal file
26
src/components/color-button.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styles from './color-button.css';
|
||||
|
||||
const ColorButtonComponent = props => (
|
||||
<div
|
||||
className={styles.colorButton}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div
|
||||
className={styles.colorButtonSwatch}
|
||||
style={{
|
||||
background: props.color
|
||||
}}
|
||||
/>
|
||||
<div className={styles.colorButtonArrow}>▾</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
ColorButtonComponent.propTypes = {
|
||||
color: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ColorButtonComponent;
|
50
src/components/color-picker.css
Normal file
50
src/components/color-picker.css
Normal file
|
@ -0,0 +1,50 @@
|
|||
/* Popover styles */
|
||||
:global(.Popover-body) {
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, .3);
|
||||
}
|
||||
|
||||
:global(.Popover-tipShape) {
|
||||
fill: white;
|
||||
stroke: #ddd;
|
||||
}
|
||||
|
||||
.row-header {
|
||||
font-family: "Helvetica Neue", Helvetica, sans-serif;
|
||||
font-size: 0.65rem;
|
||||
color: #575E75;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.label-readout {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.label-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.swatches {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.active-swatch {
|
||||
border: 1px solid #4C97FF;
|
||||
box-shadow: 0px 0px 0px 3px hsla(215, 100%, 65%, 0.2);
|
||||
}
|
196
src/components/color-picker.jsx
Normal file
196
src/components/color-picker.jsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import parseColor from 'parse-color';
|
||||
import bindAll from 'lodash.bindall';
|
||||
|
||||
import {MIXED} from '../helper/style-path';
|
||||
|
||||
import Slider from './forms/slider.jsx';
|
||||
import styles from './color-picker.css';
|
||||
|
||||
const colorStringToHsv = hexString => {
|
||||
const hsv = parseColor(hexString).hsv;
|
||||
// Hue comes out in [0, 360], limit to [0, 100]
|
||||
hsv[0] = hsv[0] / 3.6;
|
||||
// Black is parsed as {0, 0, 0}, but turn saturation up to 100
|
||||
// to make it easier to see slider values.
|
||||
if (hsv[1] === 0 && hsv[2] === 0) {
|
||||
hsv[1] = 100;
|
||||
}
|
||||
return hsv;
|
||||
};
|
||||
|
||||
const hsvToHex = (h, s, v) =>
|
||||
// Scale hue back up to [0, 360] from [0, 100]
|
||||
parseColor(`hsv(${3.6 * h}, ${s}, ${v})`).hex
|
||||
;
|
||||
|
||||
// Important! This component ignores new color props and cannot be updated
|
||||
// This is to make the HSV <=> RGB conversion stable. Because of this, the
|
||||
// component MUST be unmounted in order to change the props externally.
|
||||
class ColorPickerComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleHueChange',
|
||||
'handleSaturationChange',
|
||||
'handleBrightnessChange',
|
||||
'handleTransparent'
|
||||
]);
|
||||
const isTransparent = this.props.color === null;
|
||||
const isMixed = this.props.color === MIXED;
|
||||
const hsv = isTransparent || isMixed ?
|
||||
[50, 100, 100] : colorStringToHsv(props.color);
|
||||
|
||||
this.state = {
|
||||
hue: hsv[0],
|
||||
saturation: hsv[1],
|
||||
brightness: hsv[2]
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps () {
|
||||
// Just a reminder, new props do not update the hsv state
|
||||
}
|
||||
|
||||
handleHueChange (hue) {
|
||||
this.setState({hue: hue});
|
||||
this.handleColorChange();
|
||||
}
|
||||
|
||||
handleSaturationChange (saturation) {
|
||||
this.setState({saturation: saturation});
|
||||
this.handleColorChange();
|
||||
}
|
||||
|
||||
handleBrightnessChange (brightness) {
|
||||
this.setState({brightness: brightness});
|
||||
this.handleColorChange();
|
||||
}
|
||||
|
||||
handleColorChange () {
|
||||
this.props.onChangeColor(hsvToHex(
|
||||
this.state.hue,
|
||||
this.state.saturation,
|
||||
this.state.brightness
|
||||
));
|
||||
}
|
||||
|
||||
handleTransparent () {
|
||||
this.props.onChangeColor(null);
|
||||
}
|
||||
|
||||
_makeBackground (channel) {
|
||||
const stops = [];
|
||||
// Generate the color slider background CSS gradients by adding
|
||||
// color stops depending on the slider.
|
||||
for (let n = 100; n >= 0; n -= 10) {
|
||||
switch (channel) {
|
||||
case 'hue':
|
||||
stops.push(hsvToHex(n, this.state.saturation, this.state.brightness));
|
||||
break;
|
||||
case 'saturation':
|
||||
stops.push(hsvToHex(this.state.hue, n, this.state.brightness));
|
||||
break;
|
||||
case 'brightness':
|
||||
stops.push(hsvToHex(this.state.hue, this.state.saturation, n));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown channel for color sliders: ${channel}`);
|
||||
}
|
||||
}
|
||||
return `linear-gradient(to left, ${stops.join(',')})`;
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.colorPickerContainer}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.rowHeader}>
|
||||
<span className={styles.labelName}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Hue"
|
||||
description="Label for the hue component in the color picker"
|
||||
id="paint.paintEditor.hue"
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.labelReadout}>
|
||||
{Math.round(this.state.hue)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
background={this._makeBackground('hue')}
|
||||
value={this.state.hue}
|
||||
onChange={this.handleHueChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.rowHeader}>
|
||||
<span className={styles.labelName}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Saturation"
|
||||
description="Label for the saturation component in the color picker"
|
||||
id="paint.paintEditor.saturation"
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.labelReadout}>
|
||||
{Math.round(this.state.saturation)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
background={this._makeBackground('saturation')}
|
||||
value={this.state.saturation}
|
||||
onChange={this.handleSaturationChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.rowHeader}>
|
||||
<span className={styles.labelName}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Brightness"
|
||||
description="Label for the brightness component in the color picker"
|
||||
id="paint.paintEditor.brightness"
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.labelReadout}>
|
||||
{Math.round(this.state.brightness)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.rowSlider}>
|
||||
<Slider
|
||||
background={this._makeBackground('brightness')}
|
||||
value={this.state.brightness}
|
||||
onChange={this.handleBrightnessChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.row}>
|
||||
<div className={styles.swatches}>
|
||||
<div
|
||||
className={classNames({
|
||||
[styles.swatch]: true,
|
||||
[styles.activeSwatch]: this.props.color === null
|
||||
})}
|
||||
onClick={this.handleTransparent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ColorPickerComponent.propTypes = {
|
||||
color: PropTypes.string,
|
||||
onChangeColor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ColorPickerComponent;
|
|
@ -1,16 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Popover from 'react-popover';
|
||||
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import BufferedInputHOC from './forms/buffered-input-hoc.jsx';
|
||||
import Label from './forms/label.jsx';
|
||||
import Input from './forms/input.jsx';
|
||||
|
||||
import {MIXED} from '../helper/style-path';
|
||||
import ColorPicker from './color-picker.jsx';
|
||||
import ColorButton from './color-button.jsx';
|
||||
|
||||
import styles from './paint-editor.css';
|
||||
|
||||
const BufferedInput = BufferedInputHOC(Input);
|
||||
const messages = defineMessages({
|
||||
fill: {
|
||||
id: 'paint.paintEditor.fill',
|
||||
|
@ -18,23 +16,37 @@ const messages = defineMessages({
|
|||
defaultMessage: 'Fill'
|
||||
}
|
||||
});
|
||||
|
||||
const FillColorIndicatorComponent = props => (
|
||||
<div className={styles.inputGroup}>
|
||||
<Popover
|
||||
body={
|
||||
<ColorPicker
|
||||
color={props.fillColor}
|
||||
onChangeColor={props.onChangeFillColor}
|
||||
/>
|
||||
}
|
||||
isOpen={props.fillColorModalVisible}
|
||||
preferPlace="below"
|
||||
onOuterAction={props.onCloseFillColor}
|
||||
>
|
||||
<Label text={props.intl.formatMessage(messages.fill)}>
|
||||
<BufferedInput
|
||||
type="text"
|
||||
value={props.fillColor === MIXED ? 'mixed' :
|
||||
props.fillColor === null ? 'transparent' : props.fillColor} // @todo Don't use text
|
||||
onSubmit={props.onChangeFillColor}
|
||||
<ColorButton
|
||||
color={props.fillColor}
|
||||
onClick={props.onOpenFillColor}
|
||||
/>
|
||||
</Label>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
FillColorIndicatorComponent.propTypes = {
|
||||
fillColor: PropTypes.string,
|
||||
fillColorModalVisible: PropTypes.bool.isRequired,
|
||||
intl: intlShape,
|
||||
onChangeFillColor: PropTypes.func.isRequired
|
||||
onChangeFillColor: PropTypes.func.isRequired,
|
||||
onCloseFillColor: PropTypes.func.isRequired,
|
||||
onOpenFillColor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(FillColorIndicatorComponent);
|
||||
|
|
20
src/components/forms/slider.css
Normal file
20
src/components/forms/slider.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.container {
|
||||
margin: 8px;
|
||||
height: 22px;
|
||||
width: 150px;
|
||||
position: relative;
|
||||
outline: none;
|
||||
border-radius: 11px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.handle {
|
||||
left: 100px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
79
src/components/forms/slider.jsx
Normal file
79
src/components/forms/slider.jsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
|
||||
import styles from './slider.css';
|
||||
|
||||
const CONTAINER_WIDTH = 150;
|
||||
const HANDLE_WIDTH = 26;
|
||||
|
||||
class SliderComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleMouseDown',
|
||||
'handleMouseUp',
|
||||
'handleMouseMove',
|
||||
'setBackground'
|
||||
]);
|
||||
}
|
||||
|
||||
handleMouseDown () {
|
||||
document.addEventListener('mouseup', this.handleMouseUp);
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
}
|
||||
|
||||
handleMouseUp () {
|
||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
}
|
||||
|
||||
handleMouseMove (event) {
|
||||
event.preventDefault();
|
||||
const backgroundBBox = this.background.getBoundingClientRect();
|
||||
const x = event.clientX - backgroundBBox.left;
|
||||
this.props.onChange(Math.max(1, Math.min(99, 100 * x / backgroundBBox.width)));
|
||||
}
|
||||
|
||||
setBackground (ref) {
|
||||
this.background = ref;
|
||||
}
|
||||
|
||||
render () {
|
||||
const halfHandleWidth = HANDLE_WIDTH / 2;
|
||||
const pixelMin = halfHandleWidth;
|
||||
const pixelMax = CONTAINER_WIDTH - halfHandleWidth;
|
||||
const handleOffset = pixelMin +
|
||||
((pixelMax - pixelMin) * (this.props.value / 100)) -
|
||||
halfHandleWidth;
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
ref={this.setBackground}
|
||||
style={{
|
||||
backgroundImage: this.props.background
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.handle}
|
||||
style={{
|
||||
left: `${handleOffset}px`
|
||||
}}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SliderComponent.propTypes = {
|
||||
background: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
SliderComponent.defaultProps = {
|
||||
background: 'yellow'
|
||||
};
|
||||
|
||||
export default SliderComponent;
|
|
@ -1,16 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Popover from 'react-popover';
|
||||
|
||||
import {defineMessages, injectIntl, intlShape} from 'react-intl';
|
||||
import BufferedInputHOC from './forms/buffered-input-hoc.jsx';
|
||||
import Label from './forms/label.jsx';
|
||||
import Input from './forms/input.jsx';
|
||||
|
||||
import {MIXED} from '../helper/style-path';
|
||||
import ColorPicker from './color-picker.jsx';
|
||||
import ColorButton from './color-button.jsx';
|
||||
|
||||
import styles from './paint-editor.css';
|
||||
|
||||
const BufferedInput = BufferedInputHOC(Input);
|
||||
const messages = defineMessages({
|
||||
stroke: {
|
||||
id: 'paint.paintEditor.stroke',
|
||||
|
@ -18,24 +16,37 @@ const messages = defineMessages({
|
|||
defaultMessage: 'Outline'
|
||||
}
|
||||
});
|
||||
|
||||
const StrokeColorIndicatorComponent = props => (
|
||||
<div className={styles.inputGroup}>
|
||||
<Popover
|
||||
body={
|
||||
<ColorPicker
|
||||
color={props.strokeColor}
|
||||
onChangeColor={props.onChangeStrokeColor}
|
||||
/>
|
||||
}
|
||||
isOpen={props.strokeColorModalVisible}
|
||||
preferPlace="below"
|
||||
onOuterAction={props.onCloseStrokeColor}
|
||||
>
|
||||
<Label text={props.intl.formatMessage(messages.stroke)}>
|
||||
<BufferedInput
|
||||
type="text"
|
||||
// @todo Don't use text
|
||||
value={props.strokeColor === MIXED ? 'mixed' :
|
||||
props.strokeColor === null ? 'transparent' : props.strokeColor}
|
||||
onSubmit={props.onChangeStrokeColor}
|
||||
<ColorButton
|
||||
color={props.strokeColor}
|
||||
onClick={props.onOpenStrokeColor}
|
||||
/>
|
||||
</Label>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
||||
StrokeColorIndicatorComponent.propTypes = {
|
||||
intl: intlShape,
|
||||
onChangeStrokeColor: PropTypes.func.isRequired,
|
||||
strokeColor: PropTypes.string
|
||||
onCloseStrokeColor: PropTypes.func.isRequired,
|
||||
onOpenStrokeColor: PropTypes.func.isRequired,
|
||||
strokeColor: PropTypes.string,
|
||||
strokeColorModalVisible: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(StrokeColorIndicatorComponent);
|
||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import {changeFillColor} from '../reducers/fill-color';
|
||||
import {openFillColor, closeFillColor} from '../reducers/modals';
|
||||
|
||||
import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx';
|
||||
import {applyFillColorToSelection} from '../helper/style-path';
|
||||
|
||||
|
@ -20,7 +22,7 @@ class FillColorIndicator extends React.Component {
|
|||
render () {
|
||||
return (
|
||||
<FillColorIndicatorComponent
|
||||
fillColor={this.props.fillColor}
|
||||
{...this.props}
|
||||
onChangeFillColor={this.handleChangeFillColor}
|
||||
/>
|
||||
);
|
||||
|
@ -28,11 +30,19 @@ class FillColorIndicator extends React.Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
fillColor: state.scratchPaint.color.fillColor
|
||||
fillColor: state.scratchPaint.color.fillColor,
|
||||
fillColorModalVisible: state.scratchPaint.modals.fillColor
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChangeFillColor: fillColor => {
|
||||
dispatch(changeFillColor(fillColor));
|
||||
},
|
||||
onOpenFillColor: () => {
|
||||
dispatch(openFillColor());
|
||||
},
|
||||
onCloseFillColor: () => {
|
||||
dispatch(closeFillColor());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
||||
import {openStrokeColor, closeStrokeColor} from '../reducers/modals';
|
||||
|
||||
import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx';
|
||||
import {applyStrokeColorToSelection} from '../helper/style-path';
|
||||
|
||||
|
@ -20,7 +22,7 @@ class StrokeColorIndicator extends React.Component {
|
|||
render () {
|
||||
return (
|
||||
<StrokeColorIndicatorComponent
|
||||
strokeColor={this.props.strokeColor}
|
||||
{...this.props}
|
||||
onChangeStrokeColor={this.handleChangeStrokeColor}
|
||||
/>
|
||||
);
|
||||
|
@ -28,11 +30,19 @@ class StrokeColorIndicator extends React.Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
strokeColor: state.scratchPaint.color.strokeColor
|
||||
strokeColor: state.scratchPaint.color.strokeColor,
|
||||
strokeColorModalVisible: state.scratchPaint.modals.strokeColor
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChangeStrokeColor: strokeColor => {
|
||||
dispatch(changeStrokeColor(strokeColor));
|
||||
},
|
||||
onOpenStrokeColor: () => {
|
||||
dispatch(openStrokeColor());
|
||||
},
|
||||
onCloseStrokeColor: () => {
|
||||
dispatch(closeStrokeColor());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const reducer = function (state, action) {
|
|||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case CHANGE_FILL_COLOR:
|
||||
if (!regExp.test(action.fillColor)) {
|
||||
if (!regExp.test(action.fillColor) && action.fillColor !== null) {
|
||||
log.warn(`Invalid hex color code: ${action.fillColor}`);
|
||||
return state;
|
||||
}
|
||||
|
|
66
src/reducers/modals.js
Normal file
66
src/reducers/modals.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
const OPEN_MODAL = 'scratch-paint/modals/OPEN_MODAL';
|
||||
const CLOSE_MODAL = 'scratch-paint/modals/CLOSE_MODAL';
|
||||
|
||||
const MODAL_FILL_COLOR = 'fillColor';
|
||||
const MODAL_STROKE_COLOR = 'strokeColor';
|
||||
|
||||
const initialState = {
|
||||
[MODAL_FILL_COLOR]: false,
|
||||
[MODAL_STROKE_COLOR]: false
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case OPEN_MODAL:
|
||||
return Object.assign({}, initialState, {
|
||||
[action.modal]: true
|
||||
});
|
||||
case CLOSE_MODAL:
|
||||
return Object.assign({}, initialState, {
|
||||
[action.modal]: false
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = function (modal) {
|
||||
return {
|
||||
type: OPEN_MODAL,
|
||||
modal: modal
|
||||
};
|
||||
};
|
||||
|
||||
const closeModal = function (modal) {
|
||||
return {
|
||||
type: CLOSE_MODAL,
|
||||
modal: modal
|
||||
};
|
||||
};
|
||||
|
||||
// Action creators ==================================
|
||||
|
||||
const openFillColor = function () {
|
||||
return openModal(MODAL_FILL_COLOR);
|
||||
};
|
||||
|
||||
const openStrokeColor = function () {
|
||||
return openModal(MODAL_STROKE_COLOR);
|
||||
};
|
||||
|
||||
const closeFillColor = function () {
|
||||
return closeModal(MODAL_FILL_COLOR);
|
||||
};
|
||||
|
||||
const closeStrokeColor = function () {
|
||||
return closeModal(MODAL_STROKE_COLOR);
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
openFillColor,
|
||||
openStrokeColor,
|
||||
closeFillColor,
|
||||
closeStrokeColor
|
||||
};
|
|
@ -4,6 +4,7 @@ import brushModeReducer from './brush-mode';
|
|||
import eraserModeReducer from './eraser-mode';
|
||||
import colorReducer from './color';
|
||||
import hoverReducer from './hover';
|
||||
import modalsReducer from './modals';
|
||||
import selectedItemReducer from './selected-items';
|
||||
import undoReducer from './undo';
|
||||
|
||||
|
@ -13,6 +14,7 @@ export default combineReducers({
|
|||
eraserMode: eraserModeReducer,
|
||||
color: colorReducer,
|
||||
hoveredItemId: hoverReducer,
|
||||
modals: modalsReducer,
|
||||
selectedItems: selectedItemReducer,
|
||||
undo: undoReducer
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ const reducer = function (state, action) {
|
|||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case CHANGE_STROKE_COLOR:
|
||||
if (!regExp.test(action.strokeColor)) {
|
||||
if (!regExp.test(action.strokeColor) && action.strokeColor !== null) {
|
||||
log.warn(`Invalid hex color code: ${action.fillColor}`);
|
||||
return state;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue