mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 14:42:13 -05:00
Merge branch 'develop' into issue/219
This commit is contained in:
commit
d9587ba8d7
18 changed files with 759 additions and 125 deletions
|
@ -203,3 +203,20 @@ $border-radius: 0.25rem;
|
|||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.text-area {
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: none;
|
||||
font-family: Times;
|
||||
font-size: 30px;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
padding: 0px;
|
||||
position: absolute;
|
||||
resize: none;
|
||||
white-space: nowrap;
|
||||
margin: 0px;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import ReshapeMode from '../../containers/reshape-mode.jsx';
|
|||
import SelectMode from '../../containers/select-mode.jsx';
|
||||
import StrokeColorIndicatorComponent from '../../containers/stroke-color-indicator.jsx';
|
||||
import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicator.jsx';
|
||||
import TextModeComponent from '../text-mode/text-mode.jsx';
|
||||
import TextMode from '../../containers/text-mode.jsx';
|
||||
|
||||
import layout from '../../lib/layout-constants';
|
||||
import styles from './paint-editor.css';
|
||||
|
@ -350,11 +350,14 @@ const PaintEditorComponent = props => {
|
|||
<EraserMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* Text mode will go here */}
|
||||
<LineMode
|
||||
<FillMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<FillMode
|
||||
<TextMode
|
||||
textArea={props.textArea}
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<LineMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<OvalMode
|
||||
|
@ -363,37 +366,42 @@ const PaintEditorComponent = props => {
|
|||
<RectMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* text tool, coming soon */}
|
||||
<TextModeComponent />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Canvas */}
|
||||
<div
|
||||
className={classNames(
|
||||
styles.canvasContainer,
|
||||
{[styles.withEyeDropper]: props.isEyeDropping}
|
||||
)}
|
||||
>
|
||||
<PaperCanvas
|
||||
canvasRef={props.setCanvas}
|
||||
rotationCenterX={props.rotationCenterX}
|
||||
rotationCenterY={props.rotationCenterY}
|
||||
svg={props.svg}
|
||||
svgId={props.svgId}
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{props.isEyeDropping &&
|
||||
props.colorInfo !== null &&
|
||||
!props.colorInfo.hideLoupe ? (
|
||||
<Box className={styles.colorPickerWrapper}>
|
||||
<Loupe
|
||||
colorInfo={props.colorInfo}
|
||||
pixelRatio={paper.project.view.pixelRatio}
|
||||
/>
|
||||
</Box>
|
||||
) : null
|
||||
}
|
||||
|
||||
<div>
|
||||
{/* Canvas */}
|
||||
<div
|
||||
className={classNames(
|
||||
styles.canvasContainer,
|
||||
{[styles.withEyeDropper]: props.isEyeDropping}
|
||||
)}
|
||||
>
|
||||
<PaperCanvas
|
||||
canvasRef={props.setCanvas}
|
||||
rotationCenterX={props.rotationCenterX}
|
||||
rotationCenterY={props.rotationCenterY}
|
||||
svg={props.svg}
|
||||
svgId={props.svgId}
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<textarea
|
||||
className={styles.textArea}
|
||||
ref={props.setTextArea}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{props.isEyeDropping &&
|
||||
props.colorInfo !== null &&
|
||||
!props.colorInfo.hideLoupe ? (
|
||||
<Box className={styles.colorPickerWrapper}>
|
||||
<Loupe
|
||||
colorInfo={props.colorInfo}
|
||||
pixelRatio={paper.project.view.pixelRatio}
|
||||
/>
|
||||
</Box>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
<div className={styles.canvasControls}>
|
||||
<ComingSoonTooltip
|
||||
className={styles.bitmapTooltip}
|
||||
|
@ -459,7 +467,7 @@ const PaintEditorComponent = props => {
|
|||
PaintEditorComponent.propTypes = {
|
||||
canRedo: PropTypes.func.isRequired,
|
||||
canUndo: PropTypes.func.isRequired,
|
||||
canvas: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
canvas: PropTypes.instanceOf(Element),
|
||||
colorInfo: Loupe.propTypes.colorInfo,
|
||||
intl: intlShape,
|
||||
isEyeDropping: PropTypes.bool,
|
||||
|
@ -480,8 +488,10 @@ PaintEditorComponent.propTypes = {
|
|||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
setCanvas: PropTypes.func.isRequired,
|
||||
setTextArea: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
svgId: PropTypes.string
|
||||
svgId: PropTypes.string,
|
||||
textArea: PropTypes.instanceOf(Element)
|
||||
};
|
||||
|
||||
export default injectIntl(PaintEditorComponent);
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
||||
|
||||
import textIcon from './text.svg';
|
||||
|
||||
const TextModeComponent = () => (
|
||||
<ComingSoonTooltip
|
||||
place="right"
|
||||
tooltipId="text-mode-select"
|
||||
>
|
||||
<ToolSelectComponent
|
||||
disabled
|
||||
imgDescriptor={{
|
||||
defaultMessage: 'Text',
|
||||
description: 'Label for the text tool',
|
||||
id: 'paint.textMode.text'
|
||||
}}
|
||||
imgSrc={textIcon}
|
||||
isSelected={false}
|
||||
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
|
||||
/>
|
||||
</ComingSoonTooltip>
|
||||
const TextModeComponent = props => (
|
||||
<ToolSelectComponent
|
||||
imgDescriptor={{
|
||||
defaultMessage: 'Text',
|
||||
description: 'Label for the text tool',
|
||||
id: 'paint.textMode.text'
|
||||
}}
|
||||
imgSrc={textIcon}
|
||||
isSelected={props.isSelected}
|
||||
onMouseDown={props.onMouseDown}
|
||||
/>
|
||||
);
|
||||
|
||||
TextModeComponent.propTypes = {
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onMouseDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TextModeComponent;
|
||||
|
|
|
@ -30,7 +30,7 @@ class FillColorIndicator extends React.Component {
|
|||
}
|
||||
handleChangeFillColor (newColor) {
|
||||
// Apply color and update redux, but do not update svg until picker closes.
|
||||
const isDifferent = applyFillColorToSelection(newColor);
|
||||
const isDifferent = applyFillColorToSelection(newColor, this.props.textEditTarget);
|
||||
this._hasChanged = this._hasChanged || isDifferent;
|
||||
this.props.onChangeFillColor(newColor);
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ const mapStateToProps = state => ({
|
|||
disabled: state.scratchPaint.mode === Modes.LINE,
|
||||
fillColor: state.scratchPaint.color.fillColor,
|
||||
fillColorModalVisible: state.scratchPaint.modals.fillColor,
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||
textEditTarget: state.scratchPaint.textEditTarget
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -76,7 +77,8 @@ FillColorIndicator.propTypes = {
|
|||
isEyeDropping: PropTypes.bool.isRequired,
|
||||
onChangeFillColor: PropTypes.func.isRequired,
|
||||
onCloseFillColor: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
textEditTarget: PropTypes.number
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -7,6 +7,8 @@ import {changeMode} from '../reducers/modes';
|
|||
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
import {deactivateEyeDropper} from '../reducers/eye-dropper';
|
||||
import {setTextEditTarget} from '../reducers/text-edit-target';
|
||||
import {updateViewBounds} from '../reducers/view-bounds';
|
||||
|
||||
import {hideGuideLayers, showGuideLayers} from '../helper/layer';
|
||||
import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||
|
@ -43,6 +45,7 @@ class PaintEditor extends React.Component {
|
|||
'canUndo',
|
||||
'onMouseDown',
|
||||
'setCanvas',
|
||||
'setTextArea',
|
||||
'startEyeDroppingLoop',
|
||||
'stopEyeDroppingLoop'
|
||||
]);
|
||||
|
@ -52,7 +55,12 @@ class PaintEditor extends React.Component {
|
|||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
document.addEventListener('keydown', this.props.onKeyPress);
|
||||
document.addEventListener('keydown', event => {
|
||||
// Don't activate keyboard shortcuts during text editing
|
||||
if (!this.props.textEditing) {
|
||||
this.props.onKeyPress(event);
|
||||
}
|
||||
});
|
||||
// document listeners used to detect if a mouse is down outside of the
|
||||
// canvas, and should therefore stop the eye dropper
|
||||
document.addEventListener('mousedown', this.onMouseDown);
|
||||
|
@ -132,32 +140,37 @@ class PaintEditor extends React.Component {
|
|||
}
|
||||
handleZoomIn () {
|
||||
zoomOnSelection(PaintEditor.ZOOM_INCREMENT);
|
||||
if (this.props.setSelectedItems) {
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
handleZoomOut () {
|
||||
zoomOnSelection(-PaintEditor.ZOOM_INCREMENT);
|
||||
if (this.props.setSelectedItems) {
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
handleZoomReset () {
|
||||
resetZoom();
|
||||
if (this.props.setSelectedItems) {
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
}
|
||||
setCanvas (canvas) {
|
||||
this.setState({canvas: canvas});
|
||||
this.canvas = canvas;
|
||||
}
|
||||
setTextArea (element) {
|
||||
this.setState({textArea: element});
|
||||
}
|
||||
onMouseDown (event) {
|
||||
if (event.target === paper.view.element &&
|
||||
document.activeElement instanceof HTMLInputElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
if (event.target !== paper.view.element && event.target !== this.state.textArea) {
|
||||
// Exit text edit mode if you click anywhere outside of canvas
|
||||
this.props.removeTextEditTarget();
|
||||
}
|
||||
|
||||
if (this.props.isEyeDropping) {
|
||||
const colorString = this.eyeDropper.colorString;
|
||||
const callback = this.props.changeColorToEyeDropper;
|
||||
|
@ -221,8 +234,10 @@ class PaintEditor extends React.Component {
|
|||
rotationCenterX={this.props.rotationCenterX}
|
||||
rotationCenterY={this.props.rotationCenterY}
|
||||
setCanvas={this.setCanvas}
|
||||
setTextArea={this.setTextArea}
|
||||
svg={this.props.svg}
|
||||
svgId={this.props.svgId}
|
||||
textArea={this.state.textArea}
|
||||
onGroup={this.handleGroup}
|
||||
onRedo={this.handleRedo}
|
||||
onSendBackward={this.handleSendBackward}
|
||||
|
@ -256,16 +271,19 @@ PaintEditor.propTypes = {
|
|||
activate: PropTypes.func.isRequired,
|
||||
remove: PropTypes.func.isRequired
|
||||
}),
|
||||
removeTextEditTarget: PropTypes.func.isRequired,
|
||||
rotationCenterX: PropTypes.number,
|
||||
rotationCenterY: PropTypes.number,
|
||||
setSelectedItems: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
svgId: PropTypes.string,
|
||||
textEditing: PropTypes.bool.isRequired,
|
||||
undoSnapshot: PropTypes.func.isRequired,
|
||||
undoState: PropTypes.shape({
|
||||
stack: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
pointer: PropTypes.number.isRequired
|
||||
})
|
||||
}),
|
||||
updateViewBounds: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
@ -275,6 +293,7 @@ const mapStateToProps = state => ({
|
|||
pasteOffset: state.scratchPaint.clipboard.pasteOffset,
|
||||
previousTool: state.scratchPaint.color.eyeDropper.previousTool,
|
||||
selectedItems: state.scratchPaint.selectedItems,
|
||||
textEditing: state.scratchPaint.textEditTarget !== null,
|
||||
undoState: state.scratchPaint.undo
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -287,14 +306,31 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(changeMode(Modes.LINE));
|
||||
} else if (event.key === 's') {
|
||||
dispatch(changeMode(Modes.SELECT));
|
||||
} else if (event.key === 'w') {
|
||||
dispatch(changeMode(Modes.RESHAPE));
|
||||
} else if (event.key === 'f') {
|
||||
dispatch(changeMode(Modes.FILL));
|
||||
} else if (event.key === 't') {
|
||||
dispatch(changeMode(Modes.TEXT));
|
||||
} else if (event.key === 'c') {
|
||||
dispatch(changeMode(Modes.OVAL));
|
||||
} else if (event.key === 'r') {
|
||||
dispatch(changeMode(Modes.RECT));
|
||||
}
|
||||
},
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
removeTextEditTarget: () => {
|
||||
dispatch(setTextEditTarget());
|
||||
},
|
||||
setSelectedItems: () => {
|
||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||
},
|
||||
onDeactivateEyeDropper: () => {
|
||||
// set redux values to default for eye dropper reducer
|
||||
dispatch(deactivateEyeDropper());
|
||||
},
|
||||
onUndo: () => {
|
||||
dispatch(undo());
|
||||
},
|
||||
|
@ -304,9 +340,8 @@ const mapDispatchToProps = dispatch => ({
|
|||
undoSnapshot: snapshot => {
|
||||
dispatch(undoSnapshot(snapshot));
|
||||
},
|
||||
onDeactivateEyeDropper: () => {
|
||||
// set redux values to default for eye dropper reducer
|
||||
dispatch(deactivateEyeDropper());
|
||||
updateViewBounds: matrix => {
|
||||
dispatch(updateViewBounds(matrix));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
width: 480px;
|
||||
height: 360px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
/* Turn off anti-aliasing for the drawing canvas. Each time it's updated it switches
|
||||
back and forth from aliased to unaliased and that looks bad */
|
||||
|
|
|
@ -16,7 +16,7 @@ import {pan, resetZoom, zoomOnFixedPoint} from '../helper/view';
|
|||
import {ensureClockwise} from '../helper/math';
|
||||
import {clearHoveredItem} from '../reducers/hover';
|
||||
import {clearPasteOffset} from '../reducers/clipboard';
|
||||
|
||||
import {updateViewBounds} from '../reducers/view-bounds';
|
||||
|
||||
import styles from './paper-canvas.css';
|
||||
|
||||
|
@ -59,6 +59,7 @@ class PaperCanvas extends React.Component {
|
|||
const oldZoom = paper.project.view.zoom;
|
||||
const oldCenter = paper.project.view.center.clone();
|
||||
resetZoom();
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.importSvg(newProps.svg, newProps.rotationCenterX, newProps.rotationCenterY);
|
||||
paper.project.view.zoom = oldZoom;
|
||||
paper.project.view.center = oldCenter;
|
||||
|
@ -179,6 +180,7 @@ class PaperCanvas extends React.Component {
|
|||
new paper.Point(offsetX, offsetY)
|
||||
);
|
||||
zoomOnFixedPoint(-event.deltaY / 100, fixedPoint);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
this.props.setSelectedItems();
|
||||
} else if (event.shiftKey && event.deltaX === 0) {
|
||||
// Scroll horizontally (based on vertical scroll delta)
|
||||
|
@ -186,10 +188,12 @@ class PaperCanvas extends React.Component {
|
|||
// See #156.
|
||||
const dx = event.deltaY / paper.project.view.zoom;
|
||||
pan(dx, 0);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
} else {
|
||||
const dx = event.deltaX / paper.project.view.zoom;
|
||||
const dy = event.deltaY / paper.project.view.zoom;
|
||||
pan(dx, dy);
|
||||
this.props.updateViewBounds(paper.view.matrix);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
@ -219,7 +223,8 @@ PaperCanvas.propTypes = {
|
|||
setSelectedItems: PropTypes.func.isRequired,
|
||||
svg: PropTypes.string,
|
||||
svgId: PropTypes.string,
|
||||
undoSnapshot: PropTypes.func.isRequired
|
||||
undoSnapshot: PropTypes.func.isRequired,
|
||||
updateViewBounds: PropTypes.func.isRequired
|
||||
};
|
||||
const mapStateToProps = state => ({
|
||||
mode: state.scratchPaint.mode
|
||||
|
@ -242,6 +247,9 @@ const mapDispatchToProps = dispatch => ({
|
|||
},
|
||||
clearPasteOffset: () => {
|
||||
dispatch(clearPasteOffset());
|
||||
},
|
||||
updateViewBounds: matrix => {
|
||||
dispatch(updateViewBounds(matrix));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ class StrokeColorIndicator extends React.Component {
|
|||
}
|
||||
handleChangeStrokeColor (newColor) {
|
||||
// Apply color and update redux, but do not update svg until picker closes.
|
||||
const isDifferent = applyStrokeColorToSelection(newColor);
|
||||
const isDifferent = applyStrokeColorToSelection(newColor, this.props.textEditTarget);
|
||||
this._hasChanged = this._hasChanged || isDifferent;
|
||||
this.props.onChangeStrokeColor(newColor);
|
||||
}
|
||||
|
@ -51,10 +51,12 @@ class StrokeColorIndicator extends React.Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH,
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH ||
|
||||
state.scratchPaint.mode === Modes.TEXT,
|
||||
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
|
||||
strokeColor: state.scratchPaint.color.strokeColor,
|
||||
strokeColorModalVisible: state.scratchPaint.modals.strokeColor
|
||||
strokeColorModalVisible: state.scratchPaint.modals.strokeColor,
|
||||
textEditTarget: state.scratchPaint.textEditTarget
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -76,7 +78,8 @@ StrokeColorIndicator.propTypes = {
|
|||
onCloseStrokeColor: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
strokeColor: PropTypes.string,
|
||||
strokeColorModalVisible: PropTypes.bool.isRequired
|
||||
strokeColorModalVisible: PropTypes.bool.isRequired,
|
||||
textEditTarget: PropTypes.number
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -15,7 +15,9 @@ class StrokeWidthIndicator extends React.Component {
|
|||
]);
|
||||
}
|
||||
handleChangeStrokeWidth (newWidth) {
|
||||
applyStrokeWidthToSelection(newWidth, this.props.onUpdateSvg);
|
||||
if (applyStrokeWidthToSelection(newWidth, this.props.textEditTarget)) {
|
||||
this.props.onUpdateSvg();
|
||||
}
|
||||
this.props.onChangeStrokeWidth(newWidth);
|
||||
}
|
||||
render () {
|
||||
|
@ -30,8 +32,10 @@ class StrokeWidthIndicator extends React.Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH,
|
||||
strokeWidth: state.scratchPaint.color.strokeWidth
|
||||
disabled: state.scratchPaint.mode === Modes.BRUSH ||
|
||||
state.scratchPaint.mode === Modes.TEXT,
|
||||
strokeWidth: state.scratchPaint.color.strokeWidth,
|
||||
textEditTarget: state.scratchPaint.textEditTarget
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChangeStrokeWidth: strokeWidth => {
|
||||
|
@ -43,7 +47,8 @@ StrokeWidthIndicator.propTypes = {
|
|||
disabled: PropTypes.bool.isRequired,
|
||||
onChangeStrokeWidth: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
strokeWidth: PropTypes.number
|
||||
strokeWidth: PropTypes.number,
|
||||
textEditTarget: PropTypes.number
|
||||
};
|
||||
|
||||
export default connect(
|
||||
|
|
148
src/containers/text-mode.jsx
Normal file
148
src/containers/text-mode.jsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
import paper from '@scratch/paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import Modes from '../lib/modes';
|
||||
import {MIXED} from '../helper/style-path';
|
||||
|
||||
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
|
||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
||||
import {changeMode} from '../reducers/modes';
|
||||
import {setTextEditTarget} from '../reducers/text-edit-target';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
|
||||
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
|
||||
import TextTool from '../helper/tools/text-tool';
|
||||
import TextModeComponent from '../components/text-mode/text-mode.jsx';
|
||||
|
||||
class TextMode extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'activateTool',
|
||||
'deactivateTool'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.isTextModeActive) {
|
||||
this.activateTool(this.props);
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.tool && nextProps.colorState !== this.props.colorState) {
|
||||
this.tool.setColorState(nextProps.colorState);
|
||||
}
|
||||
if (this.tool && nextProps.selectedItems !== this.props.selectedItems) {
|
||||
this.tool.onSelectionChanged(nextProps.selectedItems);
|
||||
}
|
||||
if (this.tool && !nextProps.textEditTarget && this.props.textEditTarget) {
|
||||
this.tool.onTextEditCancelled();
|
||||
}
|
||||
if (this.tool && !nextProps.viewBounds.equals(this.props.viewBounds)) {
|
||||
this.tool.onViewBoundsChanged(nextProps.viewBounds);
|
||||
}
|
||||
|
||||
if (nextProps.isTextModeActive && !this.props.isTextModeActive) {
|
||||
this.activateTool();
|
||||
} else if (!nextProps.isTextModeActive && this.props.isTextModeActive) {
|
||||
this.deactivateTool();
|
||||
}
|
||||
}
|
||||
shouldComponentUpdate (nextProps) {
|
||||
return nextProps.isTextModeActive !== this.props.isTextModeActive;
|
||||
}
|
||||
activateTool () {
|
||||
clearSelection(this.props.clearSelectedItems);
|
||||
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
||||
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
||||
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
||||
const {fillColor, strokeColor, strokeWidth} = this.props.colorState;
|
||||
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
||||
const strokeColorPresent =
|
||||
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
||||
if (!fillColorPresent && !strokeColorPresent) {
|
||||
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||
this.props.onChangeStrokeColor(null);
|
||||
} else if (!fillColorPresent && strokeColorPresent) {
|
||||
this.props.onChangeFillColor(null);
|
||||
} else if (fillColorPresent && !strokeColorPresent) {
|
||||
this.props.onChangeStrokeColor(null);
|
||||
}
|
||||
this.tool = new TextTool(
|
||||
this.props.textArea,
|
||||
this.props.setSelectedItems,
|
||||
this.props.clearSelectedItems,
|
||||
this.props.onUpdateSvg,
|
||||
this.props.setTextEditTarget,
|
||||
);
|
||||
this.tool.setColorState(this.props.colorState);
|
||||
this.tool.activate();
|
||||
}
|
||||
deactivateTool () {
|
||||
this.tool.deactivateTool();
|
||||
this.tool.remove();
|
||||
this.tool = null;
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<TextModeComponent
|
||||
isSelected={this.props.isTextModeActive}
|
||||
onMouseDown={this.props.handleMouseDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextMode.propTypes = {
|
||||
clearSelectedItems: PropTypes.func.isRequired,
|
||||
colorState: PropTypes.shape({
|
||||
fillColor: PropTypes.string,
|
||||
strokeColor: PropTypes.string,
|
||||
strokeWidth: PropTypes.number
|
||||
}).isRequired,
|
||||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isTextModeActive: PropTypes.bool.isRequired,
|
||||
onChangeFillColor: PropTypes.func.isRequired,
|
||||
onChangeStrokeColor: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||
setSelectedItems: PropTypes.func.isRequired,
|
||||
setTextEditTarget: PropTypes.func.isRequired,
|
||||
textArea: PropTypes.instanceOf(Element),
|
||||
textEditTarget: PropTypes.number,
|
||||
viewBounds: PropTypes.instanceOf(paper.Matrix).isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
colorState: state.scratchPaint.color,
|
||||
isTextModeActive: state.scratchPaint.mode === Modes.TEXT,
|
||||
selectedItems: state.scratchPaint.selectedItems,
|
||||
textEditTarget: state.scratchPaint.textEditTarget,
|
||||
viewBounds: state.scratchPaint.viewBounds
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
setSelectedItems: () => {
|
||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||
},
|
||||
setTextEditTarget: targetId => {
|
||||
dispatch(setTextEditTarget(targetId));
|
||||
},
|
||||
handleMouseDown: () => {
|
||||
dispatch(changeMode(Modes.TEXT));
|
||||
},
|
||||
onChangeFillColor: fillColor => {
|
||||
dispatch(changeFillColor(fillColor));
|
||||
},
|
||||
onChangeStrokeColor: strokeColor => {
|
||||
dispatch(changeStrokeColor(strokeColor));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TextMode);
|
|
@ -30,14 +30,19 @@ const hoverItem = function (item) {
|
|||
return clone;
|
||||
};
|
||||
|
||||
const hoverBounds = function (item) {
|
||||
const rect = new paper.Path.Rectangle(item.internalBounds);
|
||||
const hoverBounds = function (item, expandBy) {
|
||||
let bounds = item.internalBounds;
|
||||
if (expandBy) {
|
||||
bounds = bounds.expand(expandBy);
|
||||
}
|
||||
const rect = new paper.Path.Rectangle(bounds);
|
||||
rect.matrix = item.matrix;
|
||||
setDefaultGuideStyle(rect);
|
||||
rect.parent = getGuideLayer();
|
||||
rect.strokeColor = GUIDE_BLUE;
|
||||
rect.fillColor = null;
|
||||
rect.data.isHelperItem = true;
|
||||
rect.data.origItem = item;
|
||||
rect.bringToFront();
|
||||
|
||||
return rect;
|
||||
|
|
|
@ -2,6 +2,7 @@ import paper from '@scratch/paper';
|
|||
import {getSelectedLeafItems} from './selection';
|
||||
import {isPGTextItem, isPointTextItem} from './item';
|
||||
import {isGroup} from './group';
|
||||
import {getItems} from './selection';
|
||||
|
||||
const MIXED = 'scratch-paint/style-path/mixed';
|
||||
|
||||
|
@ -15,44 +16,38 @@ const _colorMatch = function (itemColor, incomingColor) {
|
|||
(itemColor && incomingColor && itemColor.toCSS() === new paper.Color(incomingColor).toCSS());
|
||||
};
|
||||
|
||||
// Selected items and currently active text edit items respond to color changes.
|
||||
const _getColorStateListeners = function (textEditTargetId) {
|
||||
const items = getSelectedLeafItems();
|
||||
if (textEditTargetId) {
|
||||
const matches = getItems({
|
||||
match: item => item.id === textEditTargetId
|
||||
});
|
||||
if (matches.length) {
|
||||
items.push(matches[0]);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when setting fill color
|
||||
* @param {string} colorString New color, css format
|
||||
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
|
||||
* @return {boolean} Whether the color application actually changed visibly.
|
||||
*/
|
||||
const applyFillColorToSelection = function (colorString) {
|
||||
const items = getSelectedLeafItems();
|
||||
const applyFillColorToSelection = function (colorString, textEditTargetId) {
|
||||
const items = _getColorStateListeners(textEditTargetId);
|
||||
let changed = false;
|
||||
for (let item of items) {
|
||||
if (item.parent instanceof paper.CompoundPath) {
|
||||
if (isPointTextItem(item) && !colorString) {
|
||||
colorString = 'rgba(0,0,0,0)';
|
||||
} else if (item.parent instanceof paper.CompoundPath) {
|
||||
item = item.parent;
|
||||
}
|
||||
if (isPGTextItem(item)) {
|
||||
for (const child of item.children) {
|
||||
if (child.children) {
|
||||
for (const path of child.children) {
|
||||
if (!path.data.isPGGlyphRect) {
|
||||
if (!_colorMatch(path.fillColor, colorString)) {
|
||||
changed = true;
|
||||
path.fillColor = colorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!child.data.isPGGlyphRect) {
|
||||
if (!_colorMatch(child.fillColor, colorString)) {
|
||||
changed = true;
|
||||
child.fillColor = colorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isPointTextItem(item) && !colorString) {
|
||||
colorString = 'rgba(0,0,0,0)';
|
||||
}
|
||||
if (!_colorMatch(item.fillColor, colorString)) {
|
||||
changed = true;
|
||||
item.fillColor = colorString;
|
||||
}
|
||||
if (!_colorMatch(item.fillColor, colorString)) {
|
||||
changed = true;
|
||||
item.fillColor = colorString;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
|
@ -61,10 +56,11 @@ const applyFillColorToSelection = function (colorString) {
|
|||
/**
|
||||
* Called when setting stroke color
|
||||
* @param {string} colorString New color, css format
|
||||
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
|
||||
* @return {boolean} Whether the color application actually changed visibly.
|
||||
*/
|
||||
const applyStrokeColorToSelection = function (colorString) {
|
||||
const items = getSelectedLeafItems();
|
||||
const applyStrokeColorToSelection = function (colorString, textEditTargetId) {
|
||||
const items = _getColorStateListeners(textEditTargetId);
|
||||
let changed = false;
|
||||
for (let item of items) {
|
||||
if (item.parent instanceof paper.CompoundPath) {
|
||||
|
@ -106,11 +102,12 @@ const applyStrokeColorToSelection = function (colorString) {
|
|||
/**
|
||||
* Called when setting stroke width
|
||||
* @param {number} value New stroke width
|
||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||
* @param {?string} textEditTargetId paper.Item.id of text editing target, if any
|
||||
* @return {boolean} Whether the color application actually changed visibly.
|
||||
*/
|
||||
const applyStrokeWidthToSelection = function (value, onUpdateSvg) {
|
||||
const applyStrokeWidthToSelection = function (value, textEditTargetId) {
|
||||
let changed = false;
|
||||
const items = getSelectedLeafItems();
|
||||
const items = _getColorStateListeners(textEditTargetId);
|
||||
for (let item of items) {
|
||||
if (item.parent instanceof paper.CompoundPath) {
|
||||
item = item.parent;
|
||||
|
@ -122,9 +119,7 @@ const applyStrokeWidthToSelection = function (value, onUpdateSvg) {
|
|||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
onUpdateSvg();
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -38,14 +38,14 @@ class FillTool extends paper.Tool {
|
|||
item.lastSegment.point.getDistance(item.firstSegment.point) < 8;
|
||||
};
|
||||
return {
|
||||
class: paper.Path,
|
||||
segments: true,
|
||||
stroke: true,
|
||||
curves: true,
|
||||
fill: true,
|
||||
guide: false,
|
||||
match: function (hitResult) {
|
||||
return (hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item));
|
||||
return (hitResult.item instanceof paper.Path || hitResult.item instanceof paper.PointText) &&
|
||||
(hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item));
|
||||
},
|
||||
hitUnfilledPaths: true,
|
||||
tolerance: FillTool.TOLERANCE / paper.view.zoom
|
||||
|
|
326
src/helper/tools/text-tool.js
Normal file
326
src/helper/tools/text-tool.js
Normal file
|
@ -0,0 +1,326 @@
|
|||
import paper from '@scratch/paper';
|
||||
import Modes from '../../lib/modes';
|
||||
import {clearSelection} from '../selection';
|
||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||
import NudgeTool from '../selection-tools/nudge-tool';
|
||||
import {hoverBounds} from '../guides';
|
||||
|
||||
/**
|
||||
* Tool for adding text. Text elements have limited editability; they can't be reshaped,
|
||||
* drawn on or erased. This way they can preserve their ability to have the text edited.
|
||||
*/
|
||||
class TextTool extends paper.Tool {
|
||||
static get TOLERANCE () {
|
||||
return 6;
|
||||
}
|
||||
static get TEXT_EDIT_MODE () {
|
||||
return 'TEXT_EDIT_MODE';
|
||||
}
|
||||
static get SELECT_MODE () {
|
||||
return 'SELECT_MODE';
|
||||
}
|
||||
/** Clicks registered within this amount of time are registered as double clicks */
|
||||
static get DOUBLE_CLICK_MILLIS () {
|
||||
return 250;
|
||||
}
|
||||
/** Typing with no pauses longer than this amount of type will count as 1 action */
|
||||
static get TYPING_TIMEOUT_MILLIS () {
|
||||
return 1000;
|
||||
}
|
||||
static get TEXT_PADDING () {
|
||||
return 8;
|
||||
}
|
||||
/**
|
||||
* @param {HTMLTextAreaElement} textAreaElement dom element for the editable text field
|
||||
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||
* @param {!function} setTextEditTarget Call to set text editing target whenever text editing is active
|
||||
*/
|
||||
constructor (textAreaElement, setSelectedItems, clearSelectedItems, onUpdateSvg, setTextEditTarget) {
|
||||
super();
|
||||
this.element = textAreaElement;
|
||||
this.setSelectedItems = setSelectedItems;
|
||||
this.clearSelectedItems = clearSelectedItems;
|
||||
this.onUpdateSvg = onUpdateSvg;
|
||||
this.setTextEditTarget = setTextEditTarget;
|
||||
this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
||||
this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg);
|
||||
this.lastEvent = null;
|
||||
|
||||
// We have to set these functions instead of just declaring them because
|
||||
// paper.js tools hook up the listeners in the setter functions.
|
||||
this.onMouseDown = this.handleMouseDown;
|
||||
this.onMouseDrag = this.handleMouseDrag;
|
||||
this.onMouseUp = this.handleMouseUp;
|
||||
this.onMouseMove = this.handleMouseMove;
|
||||
this.onKeyUp = this.handleKeyUp;
|
||||
this.onKeyDown = this.handleKeyDown;
|
||||
|
||||
this.textBox = null;
|
||||
this.guide = null;
|
||||
this.colorState = null;
|
||||
this.mode = null;
|
||||
this.active = false;
|
||||
this.lastTypeEvent = null;
|
||||
|
||||
// If text selected and then activate this tool, switch to text edit mode for that text
|
||||
// If double click on text while in select mode, does mode change to text mode? Text fully selected by default
|
||||
}
|
||||
getBoundingBoxHitOptions () {
|
||||
return {
|
||||
segments: true,
|
||||
stroke: true,
|
||||
curves: true,
|
||||
fill: true,
|
||||
guide: false,
|
||||
match: hitResult =>
|
||||
(hitResult.item.data && hitResult.item.data.isHelperItem) ||
|
||||
hitResult.item.selected, // Allow hits on bounding box and selected only
|
||||
tolerance: TextTool.TOLERANCE / paper.view.zoom
|
||||
};
|
||||
}
|
||||
getTextEditHitOptions () {
|
||||
return {
|
||||
class: paper.PointText,
|
||||
segments: true,
|
||||
stroke: true,
|
||||
curves: true,
|
||||
fill: true,
|
||||
guide: false,
|
||||
match: hitResult => hitResult.item && !hitResult.item.selected, // Unselected only
|
||||
tolerance: TextTool.TOLERANCE / paper.view.zoom
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Called when the selection changes to update the bounds of the bounding box.
|
||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
||||
*/
|
||||
onSelectionChanged (selectedItems) {
|
||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
||||
}
|
||||
// Allow other tools to cancel text edit mode
|
||||
onTextEditCancelled () {
|
||||
this.endTextEdit();
|
||||
if (this.textBox) {
|
||||
this.mode = TextTool.SELECT_MODE;
|
||||
this.textBox.selected = true;
|
||||
this.setSelectedItems();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when the view matrix changes
|
||||
* @param {paper.Matrix} viewMtx applied to paper.view
|
||||
*/
|
||||
onViewBoundsChanged (viewMtx) {
|
||||
if (this.mode !== TextTool.TEXT_EDIT_MODE) {
|
||||
return;
|
||||
}
|
||||
const matrix = this.textBox.matrix;
|
||||
this.element.style.transform =
|
||||
`translate(0px, ${this.textBox.internalBounds.y}px)
|
||||
matrix(${viewMtx.a}, ${viewMtx.b}, ${viewMtx.c}, ${viewMtx.d},
|
||||
${viewMtx.tx}, ${viewMtx.ty})
|
||||
matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d},
|
||||
${matrix.tx}, ${matrix.ty})`;
|
||||
}
|
||||
setColorState (colorState) {
|
||||
this.colorState = colorState;
|
||||
}
|
||||
handleMouseMove (event) {
|
||||
const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions());
|
||||
if (hitResults.length) {
|
||||
document.body.style.cursor = 'text';
|
||||
} else {
|
||||
document.body.style.cursor = 'auto';
|
||||
}
|
||||
}
|
||||
handleMouseDown (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
this.active = true;
|
||||
|
||||
const lastMode = this.mode;
|
||||
|
||||
// Check if double clicked
|
||||
let doubleClicked = false;
|
||||
if (this.lastEvent) {
|
||||
if ((event.event.timeStamp - this.lastEvent.event.timeStamp) < TextTool.DOUBLE_CLICK_MILLIS) {
|
||||
doubleClicked = true;
|
||||
} else {
|
||||
doubleClicked = false;
|
||||
}
|
||||
}
|
||||
this.lastEvent = event;
|
||||
|
||||
const doubleClickHitTest = paper.project.hitTest(event.point, this.getBoundingBoxHitOptions());
|
||||
if (doubleClicked &&
|
||||
this.mode === TextTool.SELECT_MODE &&
|
||||
doubleClickHitTest) {
|
||||
// Double click in select mode moves you to text edit mode
|
||||
clearSelection(this.clearSelectedItems);
|
||||
this.textBox = doubleClickHitTest.item;
|
||||
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
|
||||
} else if (
|
||||
this.boundingBoxTool.onMouseDown(
|
||||
event, false /* clone */, false /* multiselect */, this.getBoundingBoxHitOptions())) {
|
||||
// In select mode staying in select mode
|
||||
return;
|
||||
}
|
||||
|
||||
// We clicked away from the item, so end the current mode
|
||||
if (lastMode === TextTool.SELECT_MODE) {
|
||||
clearSelection(this.clearSelectedItems);
|
||||
this.mode = null;
|
||||
} else if (lastMode === TextTool.TEXT_EDIT_MODE) {
|
||||
this.endTextEdit();
|
||||
}
|
||||
|
||||
const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions());
|
||||
if (hitResults.length) {
|
||||
// Clicking a different text item to begin text edit mode on that item
|
||||
clearSelection(this.clearSelectedItems);
|
||||
this.textBox = hitResults[0].item;
|
||||
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
|
||||
} else if (lastMode === TextTool.TEXT_EDIT_MODE) {
|
||||
// In text mode clicking away to begin select mode
|
||||
if (this.textBox) {
|
||||
this.mode = TextTool.SELECT_MODE;
|
||||
this.textBox.selected = true;
|
||||
this.setSelectedItems();
|
||||
}
|
||||
} else {
|
||||
// In no mode or select mode clicking away to begin text edit mode
|
||||
this.textBox = new paper.PointText({
|
||||
point: event.point,
|
||||
content: '',
|
||||
font: 'Times',
|
||||
fontSize: 30,
|
||||
fillColor: this.colorState.fillColor,
|
||||
// Default leading for both the HTML text area and paper.PointText
|
||||
// is 120%, but for some reason they are slightly off from each other.
|
||||
// This value was obtained experimentally.
|
||||
// (Don't round to 34.6, the text area will start to scroll.)
|
||||
leading: 34.61
|
||||
});
|
||||
this.beginTextEdit(this.textBox.content, this.textBox.matrix);
|
||||
}
|
||||
}
|
||||
handleMouseDrag (event) {
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.mode === TextTool.SELECT_MODE) {
|
||||
this.boundingBoxTool.onMouseDrag(event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleMouseUp (event) {
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.mode === TextTool.SELECT_MODE) {
|
||||
this.boundingBoxTool.onMouseUp(event);
|
||||
this.isBoundingBoxMode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.active = false;
|
||||
}
|
||||
handleKeyUp (event) {
|
||||
if (this.mode === TextTool.SELECT_MODE) {
|
||||
this.nudgeTool.onKeyUp(event);
|
||||
}
|
||||
}
|
||||
handleKeyDown (event) {
|
||||
if (event.event.target instanceof HTMLInputElement) {
|
||||
// Ignore nudge if a text input field is focused
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mode === TextTool.SELECT_MODE) {
|
||||
this.nudgeTool.onKeyUp(event);
|
||||
}
|
||||
}
|
||||
handleTextInput (event) {
|
||||
// Save undo state if you paused typing for long enough.
|
||||
if (this.lastTypeEvent && event.timeStamp - this.lastTypeEvent.timeStamp > TextTool.TYPING_TIMEOUT_MILLIS) {
|
||||
this.onUpdateSvg();
|
||||
}
|
||||
this.lastTypeEvent = event;
|
||||
if (this.mode === TextTool.TEXT_EDIT_MODE) {
|
||||
this.textBox.content = this.element.value;
|
||||
}
|
||||
this.resizeGuide();
|
||||
}
|
||||
resizeGuide () {
|
||||
if (this.guide) this.guide.remove();
|
||||
this.guide = hoverBounds(this.textBox, TextTool.TEXT_PADDING);
|
||||
this.guide.dashArray = [4, 4];
|
||||
this.element.style.width = `${this.textBox.internalBounds.width}px`;
|
||||
this.element.style.height = `${this.textBox.internalBounds.height}px`;
|
||||
}
|
||||
/**
|
||||
* @param {string} initialText Text to initialize the text area with
|
||||
* @param {paper.Matrix} matrix Transform matrix for the element. Defaults
|
||||
* to the identity matrix.
|
||||
*/
|
||||
beginTextEdit (initialText, matrix) {
|
||||
this.mode = TextTool.TEXT_EDIT_MODE;
|
||||
this.setTextEditTarget(this.textBox.id);
|
||||
|
||||
const viewMtx = paper.view.matrix;
|
||||
|
||||
this.element.style.display = 'initial';
|
||||
this.element.value = initialText ? initialText : '';
|
||||
this.element.style.transformOrigin =
|
||||
`${-this.textBox.internalBounds.x}px ${-this.textBox.internalBounds.y}px`;
|
||||
this.element.style.transform =
|
||||
`translate(0px, ${this.textBox.internalBounds.y}px)
|
||||
matrix(${viewMtx.a}, ${viewMtx.b}, ${viewMtx.c}, ${viewMtx.d},
|
||||
${viewMtx.tx}, ${viewMtx.ty})
|
||||
matrix(${matrix.a}, ${matrix.b}, ${matrix.c}, ${matrix.d},
|
||||
${matrix.tx}, ${matrix.ty})`;
|
||||
this.element.focus({preventScroll: true});
|
||||
this.eventListener = this.handleTextInput.bind(this);
|
||||
this.element.addEventListener('input', this.eventListener);
|
||||
this.resizeGuide();
|
||||
}
|
||||
endTextEdit () {
|
||||
if (this.mode !== TextTool.TEXT_EDIT_MODE) {
|
||||
return;
|
||||
}
|
||||
this.mode = null;
|
||||
|
||||
// Remove invisible textboxes
|
||||
if (this.textBox && this.textBox.content.trim() === '') {
|
||||
this.textBox.remove();
|
||||
this.textBox = null;
|
||||
}
|
||||
|
||||
// Remove guide
|
||||
if (this.guide) {
|
||||
this.guide.remove();
|
||||
this.guide = null;
|
||||
this.setTextEditTarget();
|
||||
}
|
||||
this.element.style.display = 'none';
|
||||
if (this.eventListener) {
|
||||
this.element.removeEventListener('input', this.eventListener);
|
||||
this.eventListener = null;
|
||||
}
|
||||
this.lastTypeEvent = null;
|
||||
|
||||
// If you finished editing a textbox, save undo state
|
||||
if (this.textBox && this.textBox.content.trim().length) {
|
||||
this.onUpdateSvg();
|
||||
}
|
||||
}
|
||||
deactivateTool () {
|
||||
if (this.textBox && this.textBox.content.trim() === '') {
|
||||
this.textBox.remove();
|
||||
this.textBox = null;
|
||||
}
|
||||
this.endTextEdit();
|
||||
this.boundingBoxTool.removeBoundsPath();
|
||||
}
|
||||
}
|
||||
|
||||
export default TextTool;
|
|
@ -9,7 +9,8 @@ const Modes = keyMirror({
|
|||
RESHAPE: null,
|
||||
OVAL: null,
|
||||
RECT: null,
|
||||
ROUNDED_RECT: null
|
||||
ROUNDED_RECT: null,
|
||||
TEXT: null
|
||||
});
|
||||
|
||||
export default Modes;
|
||||
|
|
|
@ -7,6 +7,8 @@ import clipboardReducer from './clipboard';
|
|||
import hoverReducer from './hover';
|
||||
import modalsReducer from './modals';
|
||||
import selectedItemReducer from './selected-items';
|
||||
import textEditTargetReducer from './text-edit-target';
|
||||
import viewBoundsReducer from './view-bounds';
|
||||
import undoReducer from './undo';
|
||||
|
||||
export default combineReducers({
|
||||
|
@ -18,5 +20,7 @@ export default combineReducers({
|
|||
hoveredItemId: hoverReducer,
|
||||
modals: modalsReducer,
|
||||
selectedItems: selectedItemReducer,
|
||||
undo: undoReducer
|
||||
textEditTarget: textEditTargetReducer,
|
||||
undo: undoReducer,
|
||||
viewBounds: viewBoundsReducer
|
||||
});
|
||||
|
|
40
src/reducers/text-edit-target.js
Normal file
40
src/reducers/text-edit-target.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import log from '../log/log';
|
||||
|
||||
const CHANGE_TEXT_EDIT_TARGET = 'scratch-paint/text-tool/CHANGE_TEXT_EDIT_TARGET';
|
||||
const initialState = null;
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case CHANGE_TEXT_EDIT_TARGET:
|
||||
if (typeof action.textEditTargetId === 'undefined') {
|
||||
log.warn(`Text edit target should not be set to undefined. Use null.`);
|
||||
return state;
|
||||
} else if (typeof action.textEditTargetId === 'undefined' || isNaN(action.textEditTargetId)) {
|
||||
log.warn(`Text edit target should be an item ID number. Got: ${action.textEditTargetId}`);
|
||||
return state;
|
||||
}
|
||||
return action.textEditTargetId;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Action creators ==================================
|
||||
/**
|
||||
* Set the currently-being-edited text field to the given item ID
|
||||
* @param {?number} textEditTargetId The paper.Item ID of the active text field.
|
||||
* Leave empty if there is no text editing target.
|
||||
* @return {object} Redux action to change the text edit target.
|
||||
*/
|
||||
const setTextEditTarget = function (textEditTargetId) {
|
||||
return {
|
||||
type: CHANGE_TEXT_EDIT_TARGET,
|
||||
textEditTargetId: textEditTargetId ? textEditTargetId : null
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
setTextEditTarget
|
||||
};
|
37
src/reducers/view-bounds.js
Normal file
37
src/reducers/view-bounds.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import paper from '@scratch/paper';
|
||||
import log from '../log/log';
|
||||
|
||||
const UPDATE_VIEW_BOUNDS = 'scratch-paint/view/UPDATE_VIEW_BOUNDS';
|
||||
const initialState = new paper.Matrix(); // Identity
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case UPDATE_VIEW_BOUNDS:
|
||||
if (!(action.viewBounds instanceof paper.Matrix)) {
|
||||
log.warn(`View bounds should be a paper.Matrix.`);
|
||||
return state;
|
||||
}
|
||||
return action.viewBounds;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Action creators ==================================
|
||||
/**
|
||||
* Set the view bounds, which defines the zoom and scroll of the paper canvas.
|
||||
* @param {paper.Matrix} matrix The matrix applied to the view
|
||||
* @return {object} Redux action to set the view bounds
|
||||
*/
|
||||
const updateViewBounds = function (matrix) {
|
||||
return {
|
||||
type: UPDATE_VIEW_BOUNDS,
|
||||
viewBounds: matrix.clone()
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
updateViewBounds
|
||||
};
|
Loading…
Reference in a new issue