bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/whiteboard/annotations/poll/component.jsx

663 lines
21 KiB
React
Raw Normal View History

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PollService from '/imports/ui/components/poll/service';
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
import { injectIntl, defineMessages } from 'react-intl';
2020-05-11 20:56:53 +08:00
import styles from './styles';
2021-04-13 19:43:08 +08:00
import {
getSwapLayout,
shouldEnableSwapLayout,
} from '/imports/ui/components/media/service';
const intlMessages = defineMessages({
pollResultAria: {
id: 'app.whiteboard.annotations.pollResult',
description: 'aria label used in poll result string',
},
});
2021-05-07 02:38:01 +08:00
const MAX_DISPLAYED_CHARS = 15;
2019-05-08 21:36:40 +08:00
class PollDrawComponent extends Component {
constructor(props) {
super(props);
2016-06-22 07:19:39 +08:00
this.state = {
// We did it because it was calculated in the componentWillMount
calculated: false,
2017-07-27 20:35:55 +08:00
// flag indicating whether we need to continue calculating the sizes or display the annotation
2016-06-22 07:19:39 +08:00
prepareToDisplay: true,
// outer (white) rectangle's coordinates and sizes (calculated in componentWillMount)
2016-06-22 07:19:39 +08:00
outerRect: {
x: 0,
y: 0,
width: 0,
height: 0,
},
2017-06-03 03:25:02 +08:00
// inner rectangle's coordinates and sizes
2016-06-22 07:19:39 +08:00
innerRect: {
x: 0,
y: 0,
width: 0,
height: 0,
},
thickness: 0,
backgroundColor: '#ffffff',
2017-06-03 03:25:02 +08:00
// max line sizes
2016-06-22 07:19:39 +08:00
maxLineWidth: 0,
maxLineHeight: 0,
2017-06-03 03:25:02 +08:00
// max widths of the keys (left) and percent (right) strings
2016-06-22 07:19:39 +08:00
maxLeftWidth: 0,
maxRightWidth: 0,
2017-06-03 03:25:02 +08:00
// these parameters are used in calculations before and while displaying the final result
2016-06-22 07:19:39 +08:00
maxNumVotes: 0,
textArray: [],
maxDigitWidth: 0,
maxDigitHeight: 0,
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// start value used for font-size calculations
2016-06-22 07:19:39 +08:00
calcFontSize: 50,
currentLine: 0,
lineToMeasure: [],
fontSizeDirection: 1,
reducedResult: [],
2016-06-22 07:19:39 +08:00
};
this.pollInitialCalculation = this.pollInitialCalculation.bind(this);
2016-06-22 07:19:39 +08:00
}
componentDidMount() {
const isLayoutSwapped = getSwapLayout() && shouldEnableSwapLayout();
if (isLayoutSwapped) return;
this.pollInitialCalculation();
2016-06-22 07:19:39 +08:00
this.checkSizes();
}
// this might have to be changed if we want to reuse it for a presenter's poll popup
shouldComponentUpdate() {
const { prepareToDisplay } = this.state;
return prepareToDisplay === true;
}
2016-06-22 07:19:39 +08:00
componentDidUpdate() {
const { prepareToDisplay } = this.state;
if (prepareToDisplay) {
2016-06-22 07:19:39 +08:00
this.checkSizes();
}
}
checkSizes() {
let { maxLineHeight } = this.state;
const {
currentLine,
maxLineWidth,
fontSizeDirection,
calcFontSize,
textArray,
calculated,
} = this.state;
const { annotation } = this.props;
if (!calculated) return null;
// increment the font size by 2 to prevent Maximum update depth exceeded
const fontSizeIncrement = 2;
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// calculating the font size in this if / else block
if (fontSizeDirection !== 0) {
const key = `${annotation.id}_key_${currentLine}`;
const votes = `${annotation.id}_votes_${currentLine}`;
const percent = `${annotation.id}_percent_${currentLine}`;
const keySizes = this[key].getBBox();
const voteSizes = this[votes].getBBox();
const percSizes = this[percent].getBBox();
2017-06-03 03:25:02 +08:00
// first check if we can still increase the font-size
if (fontSizeDirection === 1) {
if ((keySizes.width < maxLineWidth && keySizes.height < maxLineHeight
&& voteSizes.width < maxLineWidth && voteSizes.height < maxLineHeight
&& percSizes.width < maxLineWidth && percSizes.height < maxLineHeight)
&& calcFontSize < 100) {
2016-06-22 07:19:39 +08:00
return this.setState({
calcFontSize: calcFontSize + fontSizeIncrement,
2016-06-22 07:19:39 +08:00
});
2017-07-07 20:18:54 +08:00
// we can't increase font-size anymore, start decreasing
2016-06-22 07:19:39 +08:00
}
2017-06-03 03:25:02 +08:00
return this.setState({
fontSizeDirection: -1,
calcFontSize: calcFontSize - fontSizeIncrement,
2017-06-03 03:25:02 +08:00
});
} if (fontSizeDirection === -1) {
2017-06-03 03:25:02 +08:00
// check if the font-size is still bigger than allowed
if ((keySizes.width > maxLineWidth || keySizes.height > maxLineHeight
|| voteSizes.width > maxLineWidth || voteSizes.height > maxLineHeight
|| percSizes.width > maxLineWidth || percSizes.height > maxLineHeight)
&& calcFontSize > 0) {
2016-06-22 07:19:39 +08:00
return this.setState({
calcFontSize: calcFontSize - fontSizeIncrement,
2016-06-22 07:19:39 +08:00
});
2017-07-07 20:18:54 +08:00
// font size is fine for the current line, switch to the next line
// or finish with the font-size calculations if this we are at the end of the array
2016-06-22 07:19:39 +08:00
}
if (currentLine < textArray.length - 1) {
2017-06-03 03:25:02 +08:00
return this.setState({
currentLine: currentLine + 1,
lineToMeasure: textArray[currentLine + 1],
2017-06-03 03:25:02 +08:00
});
2016-06-22 07:19:39 +08:00
}
2017-06-03 03:25:02 +08:00
return this.setState({
fontSizeDirection: 0,
currentLine: 0,
lineToMeasure: textArray[0],
2017-06-03 03:25:02 +08:00
});
2016-06-22 07:19:39 +08:00
}
}
2017-06-03 03:25:02 +08:00
// next block is executed when we finally found a proper font size
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// finding the biggest width and height of the left and right strings,
// max real line height and max width value for 1 digit
2016-06-25 05:30:37 +08:00
let maxLeftWidth = 0;
let maxRightWidth = 0;
2016-06-22 07:19:39 +08:00
maxLineHeight = 0;
for (let i = 0; i < textArray.length; i += 1) {
const key = `${annotation.id}_key_${i}`;
const percent = `${annotation.id}_percent_${i}`;
const keySizes = this[key].getBBox();
const percSizes = this[percent].getBBox();
2016-06-22 07:19:39 +08:00
if (keySizes.width > maxLeftWidth) {
maxLeftWidth = keySizes.width;
}
if (percSizes.width > maxRightWidth) {
maxRightWidth = percSizes.width;
}
if (keySizes.height > maxLineHeight) {
maxLineHeight = keySizes.height;
}
if (percSizes.height > maxLineHeight) {
maxLineHeight = percSizes.height;
}
}
const digitRef = `${annotation.id}_digit`;
const maxDigitWidth = this[digitRef].getBBox().width;
const maxDigitHeight = this[digitRef].getBBox().height;
2016-06-22 07:19:39 +08:00
return this.setState({
2017-06-03 03:25:02 +08:00
maxLeftWidth,
maxRightWidth,
maxLineHeight,
maxDigitWidth,
maxDigitHeight,
2016-06-22 07:19:39 +08:00
prepareToDisplay: false,
});
}
pollInitialCalculation() {
// in this part we retrieve the props and perform initial calculations for the state
// calculating only the parts which have to be done just once and don't require
// rendering / rerendering the text objects
// if (!state.initialState) return;
const { annotation } = this.props;
const { points, result, pollType } = annotation;
const { slideWidth, slideHeight, intl } = this.props;
2021-04-23 22:24:01 +08:00
// group duplicated responses and keep track of the number of removed items
const reducedResult = result.reduce(caseInsensitiveReducer, []);
const reducedResultRatio = reducedResult.length * 100 / result.length;
// x1 and y1 - coordinates of the top left corner of the annotation
// initial width and height are the width and height of the annotation
// all the points are given as percentages of the slide
const initialWidth = points[2];
2021-04-23 22:24:01 +08:00
const initialHeight = points[3] / 100 * reducedResultRatio; // calculate new height after grouping
const x1 = points[0];
const y1 = points[1] + (points[3] - initialHeight); // add the difference between original and reduced values
// calculating the data for the outer rectangle
// 0.001 is needed to accomodate bottom and right borders of the annotation
const x = (x1 / 100) * slideWidth;
const y = (y1 / 100) * slideHeight;
const width = ((initialWidth - 0.001) / 100) * slideWidth;
const height = ((initialHeight - 0.001) / 100) * slideHeight;
let votesTotal = 0;
let maxNumVotes = 0;
const textArray = [];
// counting the total number of votes, finding the biggest number of votes
reducedResult.reduce((previousValue, currentValue) => {
votesTotal = previousValue + currentValue.numVotes;
if (maxNumVotes < currentValue.numVotes) {
maxNumVotes = currentValue.numVotes;
}
return votesTotal;
}, 0);
// filling the textArray with data to display
// adding value of the iterator to each line needed to create unique
// keys while rendering at the end
const arrayLength = reducedResult.length;
const { pollAnswerIds } = PollService;
const isDefaultPoll = PollService.isDefaultPoll(pollType);
for (let i = 0; i < arrayLength; i += 1) {
const _tempArray = [];
const _result = reducedResult[i];
2021-02-11 04:35:55 +08:00
if (isDefaultPoll && pollAnswerIds[_result.key.toLowerCase()]) {
_result.key = intl.formatMessage(pollAnswerIds[_result.key.toLowerCase()]);
2021-02-11 04:35:55 +08:00
}
2021-05-07 02:38:01 +08:00
if (_result.key.length > MAX_DISPLAYED_CHARS) {
// find closest end of word
const before = _result.key.lastIndexOf(' ', MAX_DISPLAYED_CHARS);
const after = _result.key.indexOf(' ', MAX_DISPLAYED_CHARS + 1);
const breakpoint = (MAX_DISPLAYED_CHARS - before < after - MAX_DISPLAYED_CHARS) ? before : after;
if (breakpoint === -1) {
_result.key = `${_result.key.substr(0, MAX_DISPLAYED_CHARS)}...`;
} else {
_result.key = `${_result.key.substr(0, breakpoint)}...`;
}
}
_tempArray.push(_result.key, `${_result.numVotes}`);
if (votesTotal === 0) {
_tempArray.push('0%');
_tempArray.push(i);
} else {
const percResult = (_result.numVotes / votesTotal) * 100;
_tempArray.push(`${Math.round(percResult)}%`);
_tempArray.push(i);
}
textArray.push(_tempArray);
}
// calculating the data for the inner rectangle
const innerWidth = width * 0.95;
const innerHeight = height - (width * 0.05);
const innerX = x + (width * 0.025);
const innerY = y + (width * 0.025);
const thickness = (width - innerWidth) / 10;
// calculating the maximum possible width and height of the each line
// 25% of the height goes to the padding
2021-05-07 02:38:01 +08:00
const maxLineWidth = innerWidth / 2;
const maxLineHeight = (innerHeight * 0.75) / textArray.length;
const lineToMeasure = textArray[0];
const messageIndex = lineToMeasure[0].toLowerCase();
if (isDefaultPoll && pollAnswerIds[messageIndex]) {
lineToMeasure[0] = intl.formatMessage(pollAnswerIds[messageIndex]);
}
this.setState({
outerRect: {
x,
y,
width,
height,
},
innerRect: {
x: innerX,
y: innerY,
width: innerWidth,
height: innerHeight,
},
thickness,
maxNumVotes,
textArray,
maxLineWidth,
maxLineHeight,
lineToMeasure,
calculated: true,
reducedResult,
});
}
2016-06-22 07:19:39 +08:00
renderPoll() {
const {
backgroundColor,
calcFontSize,
innerRect,
maxDigitHeight,
maxDigitWidth,
maxLeftWidth,
maxLineHeight,
maxNumVotes,
maxRightWidth,
outerRect,
textArray,
thickness,
calculated,
reducedResult,
} = this.state;
if (!calculated) return null;
2019-05-23 02:00:44 +08:00
const { annotation, intl } = this.props;
const { pollAnswerIds } = PollService;
2019-06-26 20:03:34 +08:00
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
2017-06-03 03:25:02 +08:00
//* ********************************************************************************************
//* *****************************************MAGIC NUMBER***************************************
// There is no automatic vertical centering in SVG.
// To center the text element we have to move it down by the half of its height.
// But every text element has its own padding by default. The height we receive
// by calling getBBox() includes padding, but the anchor point doesn't consider it.
// This way the text element is moved down a little bit and we have to move it up a bit.
// 1/6 of the maximum height of the digit seems to work fine.
2017-06-03 03:25:02 +08:00
// Oleksandr Zhurbenko. June 22, 2016
const magicNumber = maxDigitHeight / 6;
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// maximum height and width of the line bar
const maxBarWidth = ((innerRect.width * 0.9)
- maxLeftWidth) - maxRightWidth;
const barHeight = (innerRect.height * 0.75) / textArray.length;
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// Horizontal padding
const horizontalPadding = (innerRect.width * 0.1) / 4;
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// Vertical padding
const verticalPadding = (innerRect.height * 0.25) / (textArray.length + 1);
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// Initial coordinates of the key column
let yLeft = ((innerRect.y + verticalPadding) + (barHeight / 2)) - magicNumber;
const xLeft = (innerRect.x + horizontalPadding) + 1;
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// Initial coordinates of the line bar column
const xBar = (innerRect.x + maxLeftWidth) + (horizontalPadding * 2);
let yBar = innerRect.y + verticalPadding;
2016-06-22 07:19:39 +08:00
2017-06-03 03:25:02 +08:00
// Initial coordinates of the percentage column
let yRight = ((innerRect.y + verticalPadding) + (barHeight / 2)) - magicNumber;
const xRight = ((((innerRect.x + (horizontalPadding * 3))
+ maxLeftWidth) + maxRightWidth) + maxBarWidth + 1);
let yNumVotes = (innerRect.y + verticalPadding) - magicNumber;
2017-06-03 03:25:02 +08:00
const extendedTextArray = [];
for (let i = 0; i < textArray.length; i += 1) {
let barWidth;
if (maxNumVotes === 0 || reducedResult[i].numVotes === 0) {
2016-06-22 07:19:39 +08:00
barWidth = 1;
} else {
barWidth = (reducedResult[i].numVotes / maxNumVotes) * maxBarWidth;
2016-06-22 07:19:39 +08:00
}
2019-05-23 02:00:44 +08:00
let label = textArray[i][0];
const formattedMessageIndex = label.toLowerCase();
const isDefaultPoll = PollService.isDefaultPoll(annotation.pollType);
if (isDefaultPoll && pollAnswerIds[formattedMessageIndex]) {
2019-05-23 02:00:44 +08:00
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
}
2017-06-03 03:25:02 +08:00
// coordinates and color of the text inside the line bar
// xNumVotesDefault and xNumVotesMovedRight are 2 different x coordinates for the text
// since if the line bar is too small then we place the number to the right of the bar
const xNumVotesDefault = (innerRect.x + maxLeftWidth) + (horizontalPadding * 2);
const xNumVotesMovedRight = (xNumVotesDefault + (barWidth / 2)) + (maxDigitWidth / 2);
2016-06-22 07:19:39 +08:00
2016-06-25 05:30:37 +08:00
let xNumVotes;
let color;
if (barWidth < maxDigitWidth + 8) {
2016-06-22 07:19:39 +08:00
xNumVotes = xNumVotesMovedRight;
color = '#333333';
} else {
xNumVotes = xNumVotesDefault;
color = 'white';
}
extendedTextArray[i] = {
key: `${annotation.id}_${textArray[i][3]}`,
2017-07-27 20:35:55 +08:00
keyColumn: {
2019-05-23 02:00:44 +08:00
keyString: label,
2017-07-27 20:35:55 +08:00
xLeft,
yLeft,
},
barColumn: {
votesString: textArray[i][1],
2017-07-27 20:35:55 +08:00
xBar,
yBar,
barWidth,
barHeight,
yNumVotes,
xNumVotes,
color,
numVotes: reducedResult[i].numVotes,
2017-07-27 20:35:55 +08:00
},
percentColumn: {
xRight,
yRight,
percentString: textArray[i][2],
2017-07-27 20:35:55 +08:00
},
};
2017-06-03 03:25:02 +08:00
// changing the Y coordinate for all the objects
2016-06-22 07:19:39 +08:00
yBar = yBar + barHeight + verticalPadding;
yLeft = yLeft + barHeight + verticalPadding;
yRight = yRight + barHeight + verticalPadding;
yNumVotes = yNumVotes + barHeight + verticalPadding;
}
return (
<g aria-hidden>
2016-06-22 07:19:39 +08:00
<rect
x={outerRect.x}
y={outerRect.y}
width={outerRect.width}
height={outerRect.height}
2016-06-22 07:19:39 +08:00
strokeWidth="0"
fill={backgroundColor}
2016-06-22 07:19:39 +08:00
/>
<rect
x={innerRect.x}
y={innerRect.y}
width={innerRect.width}
height={innerRect.height}
2016-06-22 07:19:39 +08:00
stroke="#333333"
fill={backgroundColor}
strokeWidth={thickness}
2016-06-22 07:19:39 +08:00
/>
2021-01-09 16:37:49 +08:00
{extendedTextArray.map(line => (
<text
x={line.keyColumn.xLeft}
y={line.keyColumn.yLeft}
dy={maxLineHeight / 2}
key={`${line.key}_key`}
fill="#333333"
fontFamily="Arial"
fontSize={calcFontSize}
textAnchor={isRTL ? 'end' : 'start'}
className={styles.outline}
>
{line.keyColumn.keyString}
</text>
))}
{extendedTextArray.map(line => (
<rect
2017-06-03 03:25:02 +08:00
key={`${line.key}_bar`}
x={line.barColumn.xBar}
y={line.barColumn.yBar}
width={line.barColumn.barWidth}
height={line.barColumn.barHeight}
stroke="#333333"
fill="#333333"
strokeWidth={thickness - 1}
/>
))}
2016-06-22 07:19:39 +08:00
<text
x={innerRect.x}
y={innerRect.y}
2016-06-22 07:19:39 +08:00
fill="#333333"
fontFamily="Arial"
fontSize={calcFontSize}
2019-06-26 20:03:34 +08:00
textAnchor={isRTL ? 'start' : 'end'}
2016-06-22 07:19:39 +08:00
>
{extendedTextArray.map(line => (
<tspan
2016-06-22 07:19:39 +08:00
x={line.percentColumn.xRight}
y={line.percentColumn.yRight}
dy={maxLineHeight / 2}
2016-06-25 05:30:37 +08:00
key={`${line.key}_percent`}
2020-05-11 20:56:53 +08:00
className={styles.outline}
2016-06-22 07:19:39 +08:00
>
{line.percentColumn.percentString}
</tspan>
))}
2016-06-22 07:19:39 +08:00
</text>
<text
x={innerRect.x}
y={innerRect.y}
2016-06-22 07:19:39 +08:00
fill="#333333"
fontFamily="Arial"
fontSize={calcFontSize}
2019-06-26 20:03:34 +08:00
textAnchor={isRTL ? 'end' : 'start'}
2016-06-22 07:19:39 +08:00
>
{extendedTextArray.map(line => (
<tspan
x={line.barColumn.xNumVotes + (line.barColumn.barWidth / 2)}
y={line.barColumn.yNumVotes + (line.barColumn.barHeight / 2)}
dy={maxLineHeight / 2}
2017-07-07 20:18:37 +08:00
key={`${line.key}_numVotes`}
2016-06-22 07:19:39 +08:00
fill={line.barColumn.color}
2020-05-11 20:56:53 +08:00
className={styles.outline}
2016-06-22 07:19:39 +08:00
>
{line.barColumn.numVotes}
</tspan>
))}
2016-06-22 07:19:39 +08:00
</text>
</g>
);
}
renderLine(line) {
2017-06-03 03:25:02 +08:00
// this func just renders the strings for one line
const { calcFontSize } = this.state;
const { annotation } = this.props;
2016-06-22 07:19:39 +08:00
return (
<g key={`${annotation.id}_line_${line[3]}`}>
2016-06-22 07:19:39 +08:00
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_key_${line[3]}`] = ref; }}
2016-06-22 07:19:39 +08:00
>
<tspan>
{line[0]}
</tspan>
</text>
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_votes_${line[3]}`] = ref; }}
2016-06-22 07:19:39 +08:00
>
<tspan>
{line[1]}
</tspan>
</text>
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_percent_${line[3]}`] = ref; }}
2016-06-22 07:19:39 +08:00
>
<tspan>
{line[2]}
</tspan>
</text>
</g>
);
}
renderTestStrings() {
const { annotation } = this.props;
const {
calcFontSize,
fontSizeDirection,
lineToMeasure,
textArray,
} = this.state;
// check whether we need to render just one line, which means that
// we are still calculating the font-size
2017-06-03 03:25:02 +08:00
// or if we finished with the font-size and we need to render all the strings in order to
// determine the maxHeight, maxWidth and maxDigitWidth
if (fontSizeDirection !== 0) {
return this.renderLine(lineToMeasure);
2017-06-03 03:25:02 +08:00
}
return (
<g aria-hidden>
{textArray.map(line => this.renderLine(line))}
2017-06-03 03:25:02 +08:00
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_digit`] = ref; }}
2017-06-03 03:25:02 +08:00
>
<tspan>
2017-07-07 20:18:54 +08:00
0
</tspan>
2017-06-03 03:25:02 +08:00
</text>
</g>
);
}
render() {
const { intl } = this.props;
const { prepareToDisplay, textArray } = this.state;
let ariaResultLabel = `${intl.formatMessage(intlMessages.pollResultAria)}: `;
textArray.map((t, idx) => {
const pollLine = t.slice(0, -1);
ariaResultLabel += `${idx > 0 ? ' |' : ''} ${pollLine.join(' | ')}`;
});
return (
<g aria-label={ariaResultLabel} data-test="pollResultAria">
{prepareToDisplay
? this.renderTestStrings()
: this.renderPoll()
}
2016-06-22 07:19:39 +08:00
</g>
);
}
}
2019-05-08 21:36:40 +08:00
export default injectIntl(PollDrawComponent);
PollDrawComponent.propTypes = {
intl: PropTypes.object.isRequired,
// Defines an annotation object, which contains all the basic info we need to draw a line
annotation: PropTypes.shape({
id: PropTypes.string.isRequired,
points: PropTypes.arrayOf(PropTypes.number).isRequired,
result: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
key: PropTypes.string.isRequired,
numVotes: PropTypes.number.isRequired,
}),
).isRequired,
}).isRequired,
// Defines the width of the slide (svg coordinate system), which needed in calculations
slideWidth: PropTypes.number.isRequired,
// Defines the height of the slide (svg coordinate system), which needed in calculations
slideHeight: PropTypes.number.isRequired,
};