]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Implemented RTL support for legends and tooltips (#6460)
authorDaniel Cohen Gindi <Danielgindi@gmail.com>
Wed, 11 Sep 2019 23:20:03 +0000 (02:20 +0300)
committerEvert Timberg <evert.timberg+github@gmail.com>
Wed, 11 Sep 2019 23:20:03 +0000 (19:20 -0400)
Implemented RTL support for legends and tooltips

docs/configuration/legend.md
docs/configuration/tooltip.md
src/core/core.tooltip.js
src/helpers/helpers.rtl.js [new file with mode: 0644]
src/helpers/index.js
src/plugins/plugin.legend.js

index 5caef61759d80628f9bab0a2dedea6ab92283198..e6ab285e7805a0f54dcf9d639005151301ee2151 100644 (file)
@@ -16,6 +16,8 @@ The legend configuration is passed into the `options.legend` namespace. The glob
 | `onLeave` | `function` | | A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item.
 | `reverse` | `boolean` | `false` | Legend will show datasets in reverse order.
 | `labels` | `object` | | See the [Legend Label Configuration](#legend-label-configuration) section below.
+| `rtl` | `boolean` | | `true` for rendering the legends from right to left.
+| `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'|'ltr` on the canvas for rendering the legend, regardless of the css specified on the canvas
 
 ## Position
 Position of the legend. Options are:
index 2ae32ac91fbc42d61375b31b280ba8b2f1a37afe..213f5c4b89123b5d29d0aab92076e08f71e5ae9c 100644 (file)
@@ -44,6 +44,8 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g
 | `displayColors` | `boolean` | `true` | If true, color boxes are shown in the tooltip.
 | `borderColor` | `Color` | `'rgba(0, 0, 0, 0)'` | Color of the border.
 | `borderWidth` | `number` | `0` | Size of the border.
+| `rtl` | `boolean` | | `true` for rendering the legends from right to left.
+| `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'|'ltr` on the canvas for rendering the tooltips, regardless of the css specified on the canvas
 
 ### Position Modes
 
index 00e97ca38109f3ea86dc9d7c1ad49998b8764a4c..97f7f16df7596eeafd01f4b19bb7081bb9d86238 100644 (file)
@@ -5,6 +5,7 @@ var Element = require('./core.element');
 var helpers = require('../helpers/index');
 
 var valueOrDefault = helpers.valueOrDefault;
+var getRtlHelper = helpers.rtl.getRtlAdapter;
 
 defaults._set('global', {
        tooltips: {
@@ -242,6 +243,10 @@ function getBaseModel(tooltipOpts) {
                xAlign: tooltipOpts.xAlign,
                yAlign: tooltipOpts.yAlign,
 
+               // Drawing direction and text direction
+               rtl: tooltipOpts.rtl,
+               textDirection: tooltipOpts.textDirection,
+
                // Body
                bodyFontColor: tooltipOpts.bodyFontColor,
                _bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily),
@@ -752,9 +757,11 @@ var exports = Element.extend({
                var titleFontSize, titleSpacing, i;
 
                if (length) {
+                       var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
+
                        pt.x = getAlignedX(vm, vm._titleAlign);
 
-                       ctx.textAlign = vm._titleAlign;
+                       ctx.textAlign = rtlHelper.textAlign(vm._titleAlign);
                        ctx.textBaseline = 'middle';
 
                        titleFontSize = vm.titleFontSize;
@@ -764,7 +771,7 @@ var exports = Element.extend({
                        ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
 
                        for (i = 0; i < length; ++i) {
-                               ctx.fillText(title[i], pt.x, pt.y + titleFontSize / 2);
+                               ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFontSize / 2);
                                pt.y += titleFontSize + titleSpacing; // Line Height and spacing
 
                                if (i + 1 === length) {
@@ -783,24 +790,27 @@ var exports = Element.extend({
                var xLinePadding = 0;
                var colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0;
 
+               var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
+
                var fillLineOfText = function(line) {
-                       ctx.fillText(line, pt.x + xLinePadding, pt.y + bodyFontSize / 2);
+                       ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyFontSize / 2);
                        pt.y += bodyFontSize + bodySpacing;
                };
 
                var bodyItem, textColor, labelColors, lines, i, j, ilen, jlen;
+               var bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign);
 
                ctx.textAlign = bodyAlign;
                ctx.textBaseline = 'middle';
                ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
 
-               pt.x = getAlignedX(vm, bodyAlign);
+               pt.x = getAlignedX(vm, bodyAlignForCalculation);
 
                // Before body lines
                ctx.fillStyle = vm.bodyFontColor;
                helpers.each(vm.beforeBody, fillLineOfText);
 
-               xLinePadding = drawColorBoxes && bodyAlign !== 'right'
+               xLinePadding = drawColorBoxes && bodyAlignForCalculation !== 'right'
                        ? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2)
                        : 0;
 
@@ -817,18 +827,20 @@ var exports = Element.extend({
                        for (j = 0, jlen = lines.length; j < jlen; ++j) {
                                // Draw Legend-like boxes if needed
                                if (drawColorBoxes) {
+                                       var rtlColorX = rtlHelper.x(colorX);
+
                                        // Fill a white rect so that colours merge nicely if the opacity is < 1
                                        ctx.fillStyle = vm.legendColorBackground;
-                                       ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize);
+                                       ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);
 
                                        // Border
                                        ctx.lineWidth = 1;
                                        ctx.strokeStyle = labelColors.borderColor;
-                                       ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize);
+                                       ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);
 
                                        // Inner square
                                        ctx.fillStyle = labelColors.backgroundColor;
-                                       ctx.fillRect(colorX + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
+                                       ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), bodyFontSize - 2), pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
                                        ctx.fillStyle = textColor;
                                }
 
@@ -852,10 +864,12 @@ var exports = Element.extend({
                var footerFontSize, i;
 
                if (length) {
+                       var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
+
                        pt.x = getAlignedX(vm, vm._footerAlign);
                        pt.y += vm.footerMarginTop;
 
-                       ctx.textAlign = vm._footerAlign;
+                       ctx.textAlign = rtlHelper.textAlign(vm._footerAlign);
                        ctx.textBaseline = 'middle';
 
                        footerFontSize = vm.footerFontSize;
@@ -864,7 +878,7 @@ var exports = Element.extend({
                        ctx.font = helpers.fontString(footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
 
                        for (i = 0; i < length; ++i) {
-                               ctx.fillText(footer[i], pt.x, pt.y + footerFontSize / 2);
+                               ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFontSize / 2);
                                pt.y += footerFontSize + vm.footerSpacing;
                        }
                }
@@ -946,6 +960,8 @@ var exports = Element.extend({
                        // Draw Title, Body, and Footer
                        pt.y += vm.yPadding;
 
+                       helpers.rtl.overrideTextDirection(ctx, vm.textDirection);
+
                        // Titles
                        this.drawTitle(pt, vm, ctx);
 
@@ -955,6 +971,8 @@ var exports = Element.extend({
                        // Footer
                        this.drawFooter(pt, vm, ctx);
 
+                       helpers.rtl.restoreTextDirection(ctx, vm.textDirection);
+
                        ctx.restore();
                }
        },
diff --git a/src/helpers/helpers.rtl.js b/src/helpers/helpers.rtl.js
new file mode 100644 (file)
index 0000000..392b053
--- /dev/null
@@ -0,0 +1,75 @@
+'use strict';
+
+var getRtlAdapter = function(rectX, width) {
+       return {
+               x: function(x) {
+                       return rectX + rectX + width - x;
+               },
+               setWidth: function(w) {
+                       width = w;
+               },
+               textAlign: function(align) {
+                       if (align === 'center') {
+                               return align;
+                       }
+                       return align === 'right' ? 'left' : 'right';
+               },
+               xPlus: function(x, value) {
+                       return x - value;
+               },
+               leftForLtr: function(x, itemWidth) {
+                       return x - itemWidth;
+               },
+       };
+};
+
+var getLtrAdapter = function() {
+       return {
+               x: function(x) {
+                       return x;
+               },
+               setWidth: function(w) { // eslint-disable-line no-unused-vars
+               },
+               textAlign: function(align) {
+                       return align;
+               },
+               xPlus: function(x, value) {
+                       return x + value;
+               },
+               leftForLtr: function(x, _itemWidth) { // eslint-disable-line no-unused-vars
+                       return x;
+               },
+       };
+};
+
+var getAdapter = function(rtl, rectX, width) {
+       return rtl ? getRtlAdapter(rectX, width) : getLtrAdapter();
+};
+
+var overrideTextDirection = function(ctx, direction) {
+       var style, original;
+       if (direction === 'ltr' || direction === 'rtl') {
+               style = ctx.canvas.style;
+               original = [
+                       style.getPropertyValue('direction'),
+                       style.getPropertyPriority('direction'),
+               ];
+
+               style.setProperty('direction', direction, 'important');
+               ctx.prevTextDirection = original;
+       }
+};
+
+var restoreTextDirection = function(ctx) {
+       var original = ctx.prevTextDirection;
+       if (original !== undefined) {
+               delete ctx.prevTextDirection;
+               ctx.canvas.style.setProperty('direction', original[0], original[1]);
+       }
+};
+
+module.exports = {
+       getRtlAdapter: getAdapter,
+       overrideTextDirection: overrideTextDirection,
+       restoreTextDirection: restoreTextDirection,
+};
index ef51007609007ad75a24e463f85ac4ca09d77940..167c550b6c2c85abe43c7eb508ffc0e2ed2f1007 100644 (file)
@@ -5,3 +5,4 @@ module.exports.easing = require('./helpers.easing');
 module.exports.canvas = require('./helpers.canvas');
 module.exports.options = require('./helpers.options');
 module.exports.math = require('./helpers.math');
+module.exports.rtl = require('./helpers.rtl');
index b98f2655dc1453d8d6e13bd5853e0a009686f192..78a5efe70769c1eafc46ed243abcf52a9943fb0d 100644 (file)
@@ -5,6 +5,7 @@ var Element = require('../core/core.element');
 var helpers = require('../helpers/index');
 var layouts = require('../core/core.layouts');
 
+var getRtlHelper = helpers.rtl.getRtlAdapter;
 var noop = helpers.noop;
 var valueOrDefault = helpers.valueOrDefault;
 
@@ -355,6 +356,7 @@ var Legend = Element.extend({
                        return;
                }
 
+               var rtlHelper = getRtlHelper(opts.rtl, me.left, me.minSize.width);
                var ctx = me.ctx;
                var fontColor = valueOrDefault(labelOpts.fontColor, globalDefaults.defaultFontColor);
                var labelFont = helpers.options._parseFont(labelOpts);
@@ -362,7 +364,7 @@ var Legend = Element.extend({
                var cursor;
 
                // Canvas setup
-               ctx.textAlign = 'left';
+               ctx.textAlign = rtlHelper.textAlign('left');
                ctx.textBaseline = 'middle';
                ctx.lineWidth = 0.5;
                ctx.strokeStyle = fontColor; // for strikethrough effect
@@ -398,24 +400,25 @@ var Legend = Element.extend({
                                // Recalculate x and y for drawPoint() because its expecting
                                // x and y to be center of figure (instead of top left)
                                var radius = boxWidth * Math.SQRT2 / 2;
-                               var centerX = x + boxWidth / 2;
+                               var centerX = rtlHelper.xPlus(x, boxWidth / 2);
                                var centerY = y + fontSize / 2;
 
                                // Draw pointStyle as legend symbol
                                helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY, legendItem.rotation);
                        } else {
                                // Draw box as legend symbol
-                               ctx.fillRect(x, y, boxWidth, fontSize);
+                               ctx.fillRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize);
                                if (lineWidth !== 0) {
-                                       ctx.strokeRect(x, y, boxWidth, fontSize);
+                                       ctx.strokeRect(rtlHelper.leftForLtr(x, boxWidth), y, boxWidth, fontSize);
                                }
                        }
 
                        ctx.restore();
                };
+
                var fillText = function(x, y, legendItem, textWidth) {
                        var halfFontSize = fontSize / 2;
-                       var xLeft = boxWidth + halfFontSize + x;
+                       var xLeft = rtlHelper.xPlus(x, boxWidth + halfFontSize);
                        var yMiddle = y + halfFontSize;
 
                        ctx.fillText(legendItem.text, xLeft, yMiddle);
@@ -425,7 +428,7 @@ var Legend = Element.extend({
                                ctx.beginPath();
                                ctx.lineWidth = 2;
                                ctx.moveTo(xLeft, yMiddle);
-                               ctx.lineTo(xLeft + textWidth, yMiddle);
+                               ctx.lineTo(rtlHelper.xPlus(xLeft, textWidth), yMiddle);
                                ctx.stroke();
                        }
                };
@@ -457,6 +460,8 @@ var Legend = Element.extend({
                        };
                }
 
+               helpers.rtl.overrideTextDirection(me.ctx, opts.textDirection);
+
                var itemHeight = fontSize + labelOpts.padding;
                helpers.each(me.legendItems, function(legendItem, i) {
                        var textWidth = ctx.measureText(legendItem.text).width;
@@ -464,6 +469,8 @@ var Legend = Element.extend({
                        var x = cursor.x;
                        var y = cursor.y;
 
+                       rtlHelper.setWidth(me.minSize.width);
+
                        // Use (me.left + me.minSize.width) and (me.top + me.minSize.height)
                        // instead of me.right and me.bottom because me.width and me.height
                        // may have been changed since me.minSize was calculated
@@ -479,13 +486,15 @@ var Legend = Element.extend({
                                y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]);
                        }
 
-                       drawLegendBox(x, y, legendItem);
+                       var realX = rtlHelper.x(x);
 
-                       hitboxes[i].left = x;
+                       drawLegendBox(realX, y, legendItem);
+
+                       hitboxes[i].left = rtlHelper.leftForLtr(realX, hitboxes[i].width);
                        hitboxes[i].top = y;
 
                        // Fill the actual label
-                       fillText(x, y, legendItem, textWidth);
+                       fillText(realX, y, legendItem, textWidth);
 
                        if (isHorizontal) {
                                cursor.x += width + labelOpts.padding;
@@ -493,6 +502,8 @@ var Legend = Element.extend({
                                cursor.y += itemHeight;
                        }
                });
+
+               helpers.rtl.restoreTextDirection(me.ctx, opts.textDirection);
        },
 
        /**