diff --git a/src/Context.ts b/src/Context.ts index ab4d14a27..40e21bce9 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -77,11 +77,13 @@ var CONTEXT_PROPERTIES = [ 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY', + 'letterSpacing', 'lineCap', 'lineDashOffset', 'lineJoin', 'lineWidth', 'miterLimit', + 'direction', 'font', 'textAlign', 'textBaseline', @@ -91,6 +93,11 @@ var CONTEXT_PROPERTIES = [ ] as const; const traceArrMax = 100; + +interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D { + letterSpacing: string; +} + /** * Konva wrapper around native 2d canvas context. It has almost the same API of 2d context with some additional functions. * With core Konva shapes you don't need to use this object. But you will use it if you want to create @@ -763,7 +770,7 @@ export class Context { // supported context properties type CanvasContextProps = Pick< - CanvasRenderingContext2D, + ExtendedCanvasRenderingContext2D, (typeof CONTEXT_PROPERTIES)[number] >; diff --git a/src/shapes/Text.ts b/src/shapes/Text.ts index a7820c421..3c034de27 100644 --- a/src/shapes/Text.ts +++ b/src/shapes/Text.ts @@ -22,6 +22,7 @@ export function stringToArray(string: string) { } export interface TextConfig extends ShapeConfig { + direction?: string; text?: string; fontFamily?: string; fontSize?: number; @@ -41,11 +42,13 @@ export interface TextConfig extends ShapeConfig { var AUTO = 'auto', //CANVAS = 'canvas', CENTER = 'center', + INHERIT = 'inherit', JUSTIFY = 'justify', CHANGE_KONVA = 'Change.konva', CONTEXT_2D = '2d', DASH = '-', LEFT = 'left', + LTR = 'ltr', TEXT = 'text', TEXT_UPPER = 'Text', TOP = 'top', @@ -55,11 +58,13 @@ var AUTO = 'auto', PX_SPACE = 'px ', SPACE = ' ', RIGHT = 'right', + RTL = 'rtl', WORD = 'word', CHAR = 'char', NONE = 'none', ELLIPSIS = '…', ATTR_CHANGE_LIST = [ + 'direction', 'fontFamily', 'fontSize', 'fontStyle', @@ -132,6 +137,7 @@ function checkDefaultFill(config?: TextConfig) { * @memberof Konva * @augments Konva.Shape * @param {Object} config + * @param {String} [config.direction] default is inherit * @param {String} [config.fontFamily] default is Arial * @param {Number} [config.fontSize] in pixels. Default is 12 * @param {String} [config.fontStyle] can be 'normal', 'italic', or 'bold', '500' or even 'italic bold'. 'normal' is the default. @@ -185,6 +191,7 @@ export class Text extends Shape { fontSize = this.fontSize(), lineHeightPx = this.lineHeight() * fontSize, verticalAlign = this.verticalAlign(), + direction = this.direction(), alignY = 0, align = this.align(), totalWidth = this.getWidth(), @@ -194,6 +201,8 @@ export class Text extends Shape { shouldUnderline = textDecoration.indexOf('underline') !== -1, shouldLineThrough = textDecoration.indexOf('line-through') !== -1, n; + + direction = direction === INHERIT ? context.direction : direction; var translateY = 0; var translateY = lineHeightPx / 2; @@ -201,12 +210,17 @@ export class Text extends Shape { var lineTranslateX = 0; var lineTranslateY = 0; + if (direction === RTL) { + context.setAttr('direction', direction); + } + context.setAttr('font', this._getContextFont()); context.setAttr('textBaseline', MIDDLE); context.setAttr('textAlign', LEFT); + // handle vertical alignment if (verticalAlign === MIDDLE) { alignY = (this.getHeight() - textArrLen * lineHeightPx - padding * 2) / 2; @@ -282,7 +296,10 @@ export class Text extends Shape { context.stroke(); context.restore(); } - if (letterSpacing !== 0 || align === JUSTIFY) { + // As `letterSpacing` isn't supported on Safari, we use this polyfill. + // The exception is for RTL text, which we rely on native as it cannot + // be supported otherwise. + if (direction !== RTL && (letterSpacing !== 0 || align === JUSTIFY)) { // var words = text.split(' '); spacesNumber = text.split(' ').length - 1; var array = stringToArray(text); @@ -303,6 +320,9 @@ export class Text extends Shape { lineTranslateX += this.measureSize(letter).width + letterSpacing; } } else { + if (letterSpacing !== 0) { + context.setAttr('letterSpacing', `${letterSpacing}px`); + } this._partialTextX = lineTranslateX; this._partialTextY = translateY + lineTranslateY; this._partialText = text; @@ -615,6 +635,7 @@ export class Text extends Shape { return super._useBufferCanvas(); } + direction: GetSet; fontFamily: GetSet; fontSize: GetSet; fontStyle: GetSet; @@ -682,6 +703,22 @@ Factory.overWriteSetter(Text, 'width', getNumberOrAutoValidator()); Factory.overWriteSetter(Text, 'height', getNumberOrAutoValidator()); + +/** + * get/set direction + * @name Konva.Text#direction + * @method + * @param {String} direction + * @returns {String} + * @example + * // get direction + * var direction = text.direction(); + * + * // set direction + * text.direction('rtl'); + */ +Factory.addGetterSetter(Text, 'direction', INHERIT); + /** * get/set font family * @name Konva.Text#fontFamily diff --git a/test/unit/Text-test.ts b/test/unit/Text-test.ts index 838b670de..f8b127aa9 100644 --- a/test/unit/Text-test.ts +++ b/test/unit/Text-test.ts @@ -1654,4 +1654,63 @@ describe('Text', function () { assert.equal(layer.getContext().getTrace(), trace); }); + + it('sets ltr text direction', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + + stage.add(layer); + var text = new Konva.Text({ + text: 'ltr text', + direction: 'ltr', + }); + + layer.add(text); + layer.draw(); + + var trace = + 'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);font=normal normal 12px Arial;textBaseline=middle;textAlign=left;translate(0,0);save();fillStyle=black;fillText(ltr text,0,6);restore();restore();'; + + assert.equal(layer.getContext().getTrace(), trace); + }); + + + it('sets rtl text direction', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + + stage.add(layer); + var text = new Konva.Text({ + text: 'rtl text', + direction: 'rtl', + }); + + layer.add(text); + layer.draw(); + + var trace = + 'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);direction=rtl;font=normal normal 12px Arial;textBaseline=middle;textAlign=left;translate(0,0);save();fillStyle=black;fillText(rtl text,0,6);restore();restore();'; + + assert.equal(layer.getContext().getTrace(), trace); + }); + + it('sets rtl text direction with letterSpacing', function () { + var stage = addStage(); + var layer = new Konva.Layer(); + + stage.add(layer); + var text = new Konva.Text({ + text: 'rtl text', + direction: 'rtl', + letterSpacing: 2, + }); + + layer.add(text); + layer.draw(); + + var trace = + 'clearRect(0,0,578,200);clearRect(0,0,578,200);save();transform(1,0,0,1,0,0);direction=rtl;font=normal normal 12px Arial;textBaseline=middle;textAlign=left;translate(0,0);save();letterSpacing=2px;fillStyle=black;fillText(rtl text,0,6);restore();restore();'; + + assert.equal(layer.getContext().getTrace(), trace); + }); });