From 6f5c47686dd30ae371f6f35985bc2937d36e0ba1 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Wed, 5 Sep 2018 17:19:40 -0400 Subject: [PATCH] Rtl text tool (#651) --- src/containers/text-mode.jsx | 36 ++++++++++++--------- src/helper/tools/text-tool.js | 59 +++++++++++++++++++++++++---------- src/playground/playground.jsx | 6 +++- 3 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index 1aadd6cc..d9297812 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -34,20 +34,25 @@ class TextMode extends React.Component { } } 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 (this.tool && nextProps.font !== this.props.font) { - this.tool.setFont(nextProps.font); + if (this.tool) { + if (nextProps.colorState !== this.props.colorState) { + this.tool.setColorState(nextProps.colorState); + } + if (nextProps.selectedItems !== this.props.selectedItems) { + this.tool.onSelectionChanged(nextProps.selectedItems); + } + if (!nextProps.textEditTarget && this.props.textEditTarget) { + this.tool.onTextEditCancelled(); + } + if (!nextProps.viewBounds.equals(this.props.viewBounds)) { + this.tool.onViewBoundsChanged(nextProps.viewBounds); + } + if (nextProps.font !== this.props.font) { + this.tool.setFont(nextProps.font); + } + if (nextProps.rtl !== this.props.rtl) { + this.tool.setRtl(nextProps.rtl); + } } if (nextProps.isTextModeActive && !this.props.isTextModeActive) { @@ -97,6 +102,7 @@ class TextMode extends React.Component { this.props.changeFont, nextProps.isBitmap ); + this.tool.setRtl(this.props.rtl); this.tool.setColorState(nextProps.colorState); this.tool.setFont(nextProps.font); this.tool.activate(); @@ -142,6 +148,7 @@ TextMode.propTypes = { onChangeFillColor: PropTypes.func.isRequired, onChangeStrokeColor: PropTypes.func.isRequired, onUpdateImage: PropTypes.func.isRequired, + rtl: PropTypes.bool, selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), setSelectedItems: PropTypes.func.isRequired, setTextEditTarget: PropTypes.func.isRequired, @@ -156,6 +163,7 @@ const mapStateToProps = (state, ownProps) => ({ isTextModeActive: ownProps.isBitmap ? state.scratchPaint.mode === Modes.BIT_TEXT : state.scratchPaint.mode === Modes.TEXT, + rtl: state.scratchPaint.layout.rtl, selectedItems: state.scratchPaint.selectedItems, textEditTarget: state.scratchPaint.textEditTarget, viewBounds: state.scratchPaint.viewBounds diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 9c87bf61..24b779d2 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -143,17 +143,37 @@ class TextTool extends paper.Tool { 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})`; + this.calculateMatrix(viewMtx); + } + calculateMatrix (viewMtx) { + const textBoxMtx = this.textBox.matrix; + const calculated = new paper.Matrix(); + + // In RTL, the element is moved relative to its parent's right edge instead of its left + // edge. We need to correct for this in order for the element to overlap the object in paper. + let tx = 0; + if (this.rtl && this.element.parentElement) { + tx = -this.element.parentElement.clientWidth; + } + // The transform origin in paper is x at justification side, y at the baseline of the text. + // The offset from (0, 0) to the upper left corner is recorded by internalBounds + // (so this.textBox.internalBounds.y is negative). + // Move the transform origin down to the text baseline to match paper + this.element.style.transformOrigin = `${-this.textBox.internalBounds.x}px ${-this.textBox.internalBounds.y}px`; + // Start by translating the element up so that its (0, 0) is now at the text baseline, like in paper + calculated.translate(tx, this.textBox.internalBounds.y); + calculated.append(viewMtx); + calculated.append(textBoxMtx); + this.element.style.transform = `matrix(${calculated.a}, ${calculated.b}, ${calculated.c}, ${calculated.d}, + ${calculated.tx}, ${calculated.ty})`; } setColorState (colorState) { this.colorState = colorState; } + /** @param {boolean} isRtl True if paint editor is in right-to-left layout (e.g. Hebrew language) */ + setRtl (isRtl) { + this.rtl = isRtl; + } handleMouseMove (event) { const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions()); if (hitResults.length) { @@ -283,6 +303,12 @@ class TextTool extends paper.Tool { // Prevent line from wrapping this.element.style.width = `${this.textBox.internalBounds.width + 1}px`; this.element.style.height = `${this.textBox.internalBounds.height}px`; + // The transform origin needs to be updated in RTL because this.textBox.internalBounds.x + // changes as you type + if (this.rtl) { + this.element.style.transformOrigin = + `${-this.textBox.internalBounds.x}px ${-this.textBox.internalBounds.y}px`; + } } beginSelect () { if (this.textBox) { @@ -308,18 +334,17 @@ class TextTool extends paper.Tool { this.element.style.fontSize = `${this.textBox.fontSize}px`; this.element.style.lineHeight = this.textBox.leading / this.textBox.fontSize; - const viewMtx = paper.view.matrix; - this.element.style.display = 'initial'; this.element.value = textBox.content ? textBox.content : ''; - 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(${textBox.matrix.a}, ${textBox.matrix.b}, ${textBox.matrix.c}, ${textBox.matrix.d}, - ${textBox.matrix.tx}, ${textBox.matrix.ty})`; + this.calculateMatrix(paper.view.matrix); + + if (this.rtl) { + // make both the textbox and the textarea element grow to the left + this.textBox.justification = 'right'; + } else { + this.textBox.justification = 'left'; + } + this.element.focus({preventScroll: true}); this.eventListener = this.handleTextInput.bind(this); this.element.addEventListener('input', this.eventListener); diff --git a/src/playground/playground.jsx b/src/playground/playground.jsx index 9ca41a51..2b73c550 100644 --- a/src/playground/playground.jsx +++ b/src/playground/playground.jsx @@ -30,12 +30,16 @@ class Playground extends React.Component { 'handleUpdateName', 'handleUpdateImage' ]); + // Append ?dir=rtl to URL to get RTL layout + const match = location.search.match(/dir=([^&]+)/); + const rtl = match && match[1] == 'rtl'; this.state = { name: 'meow', rotationCenterX: 20, rotationCenterY: 400, imageFormat: 'svg', // 'svg', 'png', or 'jpg' - image: svgString // svg string or data URI + image: svgString, // svg string or data URI + rtl: rtl }; } handleUpdateName (name) {