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

627 lines
19 KiB
JavaScript

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PollService from '/imports/ui/components/poll/service';
import { injectIntl, intlShape } from 'react-intl';
import styles from './styles';
class PollDrawComponent extends Component {
constructor(props) {
super(props);
this.state = {
// flag indicating whether we need to continue calculating the sizes or display the annotation
prepareToDisplay: true,
// outer (white) rectangle's coordinates and sizes (calculated in componentWillMount)
outerRect: {
x: 0,
y: 0,
width: 0,
height: 0,
},
// inner rectangle's coordinates and sizes
innerRect: {
x: 0,
y: 0,
width: 0,
height: 0,
},
thickness: 0,
backgroundColor: '#ffffff',
// max line sizes
maxLineWidth: 0,
maxLineHeight: 0,
// max widths of the keys (left) and percent (right) strings
maxLeftWidth: 0,
maxRightWidth: 0,
// these parameters are used in calculations before and while displaying the final result
maxNumVotes: 0,
textArray: [],
maxDigitWidth: 0,
maxDigitHeight: 0,
// start value used for font-size calculations
calcFontSize: 50,
currentLine: 0,
lineToMeasure: [],
fontSizeDirection: 1,
};
}
componentWillMount() {
// 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
const { annotation } = this.props;
const { points, result } = annotation;
const { slideWidth, slideHeight, intl } = this.props;
// 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 x1 = points[0];
const y1 = points[1];
const initialWidth = points[2];
const initialHeight = points[3];
// 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
result.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 = result.length;
for (let i = 0; i < arrayLength; i += 1) {
const _tempArray = [];
const _result = result[i];
let isDefaultPoll;
switch (_result.key.toLowerCase()) {
case 'true':
case 'false':
case 'yes':
case 'no':
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
isDefaultPoll = true;
break;
default:
isDefaultPoll = false;
break;
}
if (isDefaultPoll) {
_result.key = intl.formatMessage({ id: `app.poll.answer.${_result.key.toLowerCase()}` });
}
_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
const maxLineWidth = innerWidth / 3;
const maxLineHeight = (innerHeight * 0.75) / textArray.length;
const lineToMeasure = textArray[0];
const { pollAnswerIds } = PollService;
const messageIndex = lineToMeasure[0].toLowerCase();
if (pollAnswerIds[messageIndex]) {
lineToMeasure[0] = intl.formatMessage(pollAnswerIds[messageIndex]);
}
// saving all the initial calculations in the state
this.setState({
outerRect: {
x,
y,
width,
height,
},
innerRect: {
x: innerX,
y: innerY,
width: innerWidth,
height: innerHeight,
},
thickness,
maxNumVotes,
textArray,
maxLineWidth,
maxLineHeight,
lineToMeasure,
});
}
componentDidMount() {
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;
}
componentDidUpdate() {
const { prepareToDisplay } = this.state;
if (prepareToDisplay) {
this.checkSizes();
}
}
checkSizes() {
let { maxLineHeight } = this.state;
const {
currentLine,
maxLineWidth,
fontSizeDirection,
calcFontSize,
textArray,
} = this.state;
const { annotation } = this.props;
// increment the font size by 2 to prevent Maximum update depth exceeded
const fontSizeIncrement = 2;
// 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();
// 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) {
return this.setState({
calcFontSize: calcFontSize + fontSizeIncrement,
});
// we can't increase font-size anymore, start decreasing
}
return this.setState({
fontSizeDirection: -1,
calcFontSize: calcFontSize - fontSizeIncrement,
});
} if (fontSizeDirection === -1) {
// 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) {
return this.setState({
calcFontSize: calcFontSize - fontSizeIncrement,
});
// 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
}
if (currentLine < textArray.length - 1) {
return this.setState({
currentLine: currentLine + 1,
lineToMeasure: textArray[currentLine + 1],
});
}
return this.setState({
fontSizeDirection: 0,
currentLine: 0,
lineToMeasure: textArray[0],
});
}
}
// next block is executed when we finally found a proper font size
// finding the biggest width and height of the left and right strings,
// max real line height and max width value for 1 digit
let maxLeftWidth = 0;
let maxRightWidth = 0;
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();
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;
return this.setState({
maxLeftWidth,
maxRightWidth,
maxLineHeight,
maxDigitWidth,
maxDigitHeight,
prepareToDisplay: false,
});
}
renderPoll() {
const {
backgroundColor,
calcFontSize,
innerRect,
maxDigitHeight,
maxDigitWidth,
maxLeftWidth,
maxLineHeight,
maxNumVotes,
maxRightWidth,
outerRect,
textArray,
thickness,
} = this.state;
const { annotation, intl } = this.props;
const { pollAnswerIds } = PollService;
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
//* ********************************************************************************************
//* *****************************************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.
// Oleksandr Zhurbenko. June 22, 2016
const magicNumber = maxDigitHeight / 6;
// maximum height and width of the line bar
const maxBarWidth = ((innerRect.width * 0.9)
- maxLeftWidth) - maxRightWidth;
const barHeight = (innerRect.height * 0.75) / textArray.length;
// Horizontal padding
const horizontalPadding = (innerRect.width * 0.1) / 4;
// Vertical padding
const verticalPadding = (innerRect.height * 0.25) / (textArray.length + 1);
// Initial coordinates of the key column
let yLeft = ((innerRect.y + verticalPadding) + (barHeight / 2)) - magicNumber;
const xLeft = (innerRect.x + horizontalPadding) + 1;
// Initial coordinates of the line bar column
const xBar = (innerRect.x + maxLeftWidth) + (horizontalPadding * 2);
let yBar = innerRect.y + verticalPadding;
// 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;
const extendedTextArray = [];
for (let i = 0; i < textArray.length; i += 1) {
let barWidth;
if (maxNumVotes === 0 || annotation.result[i].numVotes === 0) {
barWidth = 1;
} else {
barWidth = (annotation.result[i].numVotes / maxNumVotes) * maxBarWidth;
}
let label = textArray[i][0];
const formattedMessageIndex = label.toLowerCase();
if (pollAnswerIds[formattedMessageIndex]) {
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
}
// 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);
let xNumVotes;
let color;
if (barWidth < maxDigitWidth + 8) {
xNumVotes = xNumVotesMovedRight;
color = '#333333';
} else {
xNumVotes = xNumVotesDefault;
color = 'white';
}
extendedTextArray[i] = {
key: `${annotation.id}_${textArray[i][3]}`,
keyColumn: {
keyString: label,
xLeft,
yLeft,
},
barColumn: {
votesString: textArray[i][1],
xBar,
yBar,
barWidth,
barHeight,
yNumVotes,
xNumVotes,
color,
numVotes: annotation.result[i].numVotes,
},
percentColumn: {
xRight,
yRight,
percentString: textArray[i][2],
},
};
// changing the Y coordinate for all the objects
yBar = yBar + barHeight + verticalPadding;
yLeft = yLeft + barHeight + verticalPadding;
yRight = yRight + barHeight + verticalPadding;
yNumVotes = yNumVotes + barHeight + verticalPadding;
}
return (
<g>
<rect
x={outerRect.x}
y={outerRect.y}
width={outerRect.width}
height={outerRect.height}
strokeWidth="0"
fill={backgroundColor}
/>
<rect
x={innerRect.x}
y={innerRect.y}
width={innerRect.width}
height={innerRect.height}
stroke="#333333"
fill={backgroundColor}
strokeWidth={thickness}
/>
<text
x={innerRect.x}
y={innerRect.y}
fill="#333333"
fontFamily="Arial"
fontSize={calcFontSize}
textAnchor={isRTL ? 'end' : 'start'}
>
{extendedTextArray.map(line => (
<tspan
x={line.keyColumn.xLeft}
y={line.keyColumn.yLeft}
dy={maxLineHeight / 2}
key={`${line.key}_key`}
className={styles.outline}
>
{line.keyColumn.keyString}
</tspan>
))}
</text>
{extendedTextArray.map(line => (
<rect
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}
/>
))}
<text
x={innerRect.x}
y={innerRect.y}
fill="#333333"
fontFamily="Arial"
fontSize={calcFontSize}
textAnchor={isRTL ? 'start' : 'end'}
>
{extendedTextArray.map(line => (
<tspan
x={line.percentColumn.xRight}
y={line.percentColumn.yRight}
dy={maxLineHeight / 2}
key={`${line.key}_percent`}
className={styles.outline}
>
{line.percentColumn.percentString}
</tspan>
))}
</text>
<text
x={innerRect.x}
y={innerRect.y}
fill="#333333"
fontFamily="Arial"
fontSize={calcFontSize}
textAnchor={isRTL ? 'end' : 'start'}
>
{extendedTextArray.map(line => (
<tspan
x={line.barColumn.xNumVotes + (line.barColumn.barWidth / 2)}
y={line.barColumn.yNumVotes + (line.barColumn.barHeight / 2)}
dy={maxLineHeight / 2}
key={`${line.key}_numVotes`}
fill={line.barColumn.color}
className={styles.outline}
>
{line.barColumn.numVotes}
</tspan>
))}
</text>
</g>
);
}
renderLine(line) {
// this func just renders the strings for one line
const { calcFontSize } = this.state;
const { annotation } = this.props;
return (
<g key={`${annotation.id}_line_${line[3]}`}>
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_key_${line[3]}`] = ref; }}
>
<tspan>
{line[0]}
</tspan>
</text>
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_votes_${line[3]}`] = ref; }}
>
<tspan>
{line[1]}
</tspan>
</text>
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_percent_${line[3]}`] = ref; }}
>
<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
// 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);
}
return (
<g>
{textArray.map(line => this.renderLine(line))}
<text
fontFamily="Arial"
fontSize={calcFontSize}
ref={(ref) => { this[`${annotation.id}_digit`] = ref; }}
>
<tspan>
0
</tspan>
</text>
</g>
);
}
render() {
const { prepareToDisplay } = this.state;
return (
<g>
{prepareToDisplay
? this.renderTestStrings()
: this.renderPoll()
}
</g>
);
}
}
export default injectIntl(PollDrawComponent);
PollDrawComponent.propTypes = {
intl: intlShape.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,
};