bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/presentation/cursor/component.jsx

353 lines
9.5 KiB
JavaScript
Executable File

import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Cursor extends Component {
static scale(attribute, widthRatio, physicalWidthRatio) {
return (attribute * widthRatio) / physicalWidthRatio;
}
static invertScale(attribute, widthRatio, physicalWidthRatio) {
return (attribute * physicalWidthRatio) / widthRatio;
}
static getCursorCoordinates(cursorX, cursorY, slideWidth, slideHeight) {
// main cursor x and y coordinates
const x = (cursorX / 100) * slideWidth;
const y = (cursorY / 100) * slideHeight;
return {
x,
y,
};
}
static getFillAndLabel(presenter, isMultiUser) {
const obj = {
fill: 'green',
displayLabel: false,
};
if (presenter) {
obj.fill = 'red';
}
if (isMultiUser) {
obj.displayLabel = true;
}
return obj;
}
static getScaledSizes(props, state) {
// TODO: This might need to change for the use case of fit-to-width portrait
// slides in non-presenter view. Some elements are still shrinking.
const scaleFactor = props.widthRatio / props.physicalWidthRatio;
return {
// Adjust the radius of the cursor according to zoom
// and divide it by the physicalWidth ratio, so that svg scaling wasn't applied to the cursor
finalRadius: props.radius * scaleFactor,
// scaling the properties for cursorLabel and the border (rect) around it
cursorLabelText: {
textDY: props.cursorLabelText.textDY * scaleFactor,
textDX: props.cursorLabelText.textDX * scaleFactor,
fontSize: props.cursorLabelText.fontSize * scaleFactor,
},
cursorLabelBox: {
xOffset: props.cursorLabelBox.xOffset * scaleFactor,
yOffset: props.cursorLabelBox.yOffset * scaleFactor,
// making width and height a little bit larger than the size of the text
// received from BBox, so that the text didn't touch the border
width: (state.labelBoxWidth + 3) * scaleFactor,
height: (state.labelBoxHeight + 3) * scaleFactor,
strokeWidth: props.cursorLabelBox.labelBoxStrokeWidth * scaleFactor,
},
};
}
constructor(props) {
super(props);
this.state = {
scaledSizes: null,
labelBoxWidth: 0,
labelBoxHeight: 0,
};
this.setLabelBoxDimensions = this.setLabelBoxDimensions.bind(this);
}
componentDidMount() {
const {
cursorX,
cursorY,
slideWidth,
slideHeight,
presenter,
isMultiUser,
} = this.props;
this.setState({
scaledSizes: Cursor.getScaledSizes(this.props, this.state),
});
this.cursorCoordinate = Cursor.getCursorCoordinates(
cursorX,
cursorY,
slideWidth,
slideHeight,
);
const { fill, displayLabel } = Cursor.getFillAndLabel(presenter, isMultiUser);
this.fill = fill;
this.displayLabel = displayLabel;
// we need to find the BBox of the text, so that we could set a proper border box arount it
}
componentDidUpdate(prevProps, prevState) {
const {
scaledSizes,
} = this.state;
if (!prevState.scaledSizes && scaledSizes) {
this.calculateCursorLabelBoxDimensions();
}
const {
presenter,
isMultiUser,
widthRatio,
physicalWidthRatio,
cursorX,
cursorY,
slideWidth,
slideHeight,
} = this.props;
const {
labelBoxWidth,
labelBoxHeight,
} = this.state;
const {
labelBoxWidth: prevLabelBoxWidth,
labelBoxHeight: prevLabelBoxHeight,
} = prevState;
if (presenter !== prevProps.presenter || isMultiUser !== prevProps.isMultiUser) {
const { fill, displayLabel } = Cursor.getFillAndLabel(
presenter,
isMultiUser,
);
this.displayLabel = displayLabel;
this.fill = fill;
}
if ((widthRatio !== prevProps.widthRatio
|| physicalWidthRatio !== prevProps.physicalWidthRatio)
|| (labelBoxWidth !== prevLabelBoxWidth
|| labelBoxHeight !== prevLabelBoxHeight)) {
this.setState({
scaledSizes: Cursor.getScaledSizes(this.props, this.state),
});
}
if (cursorX !== prevProps.cursorX || cursorY !== prevProps.cursorY) {
const cursorCoordinate = Cursor.getCursorCoordinates(
cursorX,
cursorY,
slideWidth,
slideHeight,
);
this.cursorCoordinate = cursorCoordinate;
}
}
setLabelBoxDimensions(labelBoxWidth, labelBoxHeight) {
this.setState({
labelBoxWidth,
labelBoxHeight,
});
}
// this function retrieves the text node, measures its BBox and sets the size for the outer box
calculateCursorLabelBoxDimensions() {
let labelBoxWidth = 0;
let labelBoxHeight = 0;
if (this.cursorLabelRef) {
const { width, height } = this.cursorLabelRef.getBBox();
const { widthRatio, physicalWidthRatio, cursorLabelBox } = this.props;
labelBoxWidth = Cursor.invertScale(width, widthRatio, physicalWidthRatio);
labelBoxHeight = Cursor.invertScale(height, widthRatio, physicalWidthRatio);
// if the width of the text node is bigger than the maxSize - set the width to maxWidth
if (labelBoxWidth > cursorLabelBox.maxWidth) {
labelBoxWidth = cursorLabelBox.maxWidth;
}
}
// updating labelBoxWidth and labelBoxHeight in the container, which then passes it down here
this.setLabelBoxDimensions(labelBoxWidth, labelBoxHeight);
}
render() {
const {
scaledSizes,
} = this.state;
const {
cursorId,
userName,
isRTL,
} = this.props;
const {
cursorCoordinate,
fill,
} = this;
if (!scaledSizes) return null;
const {
cursorLabelBox,
cursorLabelText,
finalRadius,
} = scaledSizes;
const {
x,
y,
} = cursorCoordinate;
const boxX = x + cursorLabelBox.xOffset;
const boxY = y + cursorLabelBox.yOffset;
return (
<g
x={x}
y={y}
>
<circle
cx={x}
cy={y}
r={finalRadius}
fill={fill}
fillOpacity="0.6"
/>
{this.displayLabel
? (
<g>
<rect
fill="white"
fillOpacity="0.8"
x={boxX}
y={boxY}
width={cursorLabelBox.width}
height={cursorLabelBox.height}
strokeWidth={cursorLabelBox.strokeWidth}
stroke={fill}
strokeOpacity="0.8"
/>
<text
ref={(ref) => { this.cursorLabelRef = ref; }}
x={x}
y={y}
dy={cursorLabelText.textDY}
dx={cursorLabelText.textDX}
fontFamily="Arial"
fontWeight="600"
fill={fill}
fillOpacity="0.8"
fontSize={cursorLabelText.fontSize}
clipPath={`url(#${cursorId})`}
textAnchor={isRTL ? 'end' : 'start'}
>
{userName}
</text>
<clipPath id={cursorId}>
<rect
x={boxX}
y={boxY}
width={cursorLabelBox.width}
height={cursorLabelBox.height}
/>
</clipPath>
</g>
)
: null }
</g>
);
}
}
Cursor.propTypes = {
// ESLint can't detect where all these propTypes are used, and they are not planning to fix it
// so the next line disables eslint's no-unused-prop-types rule for this file.
/* eslint-disable react/no-unused-prop-types */
// Current presenter status
presenter: PropTypes.bool.isRequired,
// Current multi-user status
isMultiUser: PropTypes.bool.isRequired,
// Defines the id of the current cursor
cursorId: PropTypes.string.isRequired,
// Defines the user name for the cursor label
userName: PropTypes.string.isRequired,
// Defines the cursor x position
cursorX: PropTypes.number.isRequired,
// Defines the cursor y position
cursorY: PropTypes.number.isRequired,
// Slide to view box width ratio
widthRatio: PropTypes.number.isRequired,
// Slide physical size to original size ratio
physicalWidthRatio: PropTypes.number.isRequired,
// Slide width (svg)
slideWidth: PropTypes.number.isRequired,
// Slide height (svg)
slideHeight: PropTypes.number.isRequired,
/**
* Defines the cursor radius (not scaled)
* @defaultValue 5
*/
radius: PropTypes.number,
cursorLabelBox: PropTypes.shape({
labelBoxStrokeWidth: PropTypes.number.isRequired,
xOffset: PropTypes.number.isRequired,
yOffset: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
}),
cursorLabelText: PropTypes.shape({
textDY: PropTypes.number.isRequired,
textDX: PropTypes.number.isRequired,
fontSize: PropTypes.number.isRequired,
}),
// Defines the direction the client text should be displayed
isRTL: PropTypes.bool.isRequired,
};
Cursor.defaultProps = {
radius: 5,
cursorLabelText: {
// text Y shift (10 points down)
textDY: 10,
// text X shift (10 points to the right)
textDX: 10,
// Initial label's font-size
fontSize: 12,
},
cursorLabelBox: {
// The thickness of the label box's border
labelBoxStrokeWidth: 1,
// X offset of the label box (8 points to the right)
xOffset: 8,
// Y offset of the label box (-2 points up)
yOffset: -2,
// Maximum width of the box, for the case when the user's name is too long
maxWidth: 65,
},
};