Merge pull request #4734 from OZhurbenko/presenter-functionality-2x

Whiteboard touch events support, bug fixes, improvements
This commit is contained in:
Anton Georgiev 2017-12-04 18:59:59 -02:00 committed by GitHub
commit e5060ea6b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 563 additions and 140 deletions

4
bigbluebutton-html5/client/main.html Executable file → Normal file
View File

@ -11,6 +11,8 @@
}
body {
position: absolute;
height: 100%;
font-family: 'Source Sans Pro', Arial, sans-serif;
font-size: 1rem; /* 16px */
background-color: #06172A;
@ -21,7 +23,7 @@
}
#app {
height: 100vh;
height: 100%;
width: 100vw;
overflow: hidden;
}

View File

@ -21,15 +21,19 @@ export default function clearAnnotations(meetingId, whiteboardId, userId) {
return Logger.error(`Removing Annotations from collection: ${err}`);
}
if (!meetingId) {
return Logger.info('Cleared Annotations (all)');
}
if (userId) {
return Logger.info(`Removed Annotations for userId=${userId} where whiteboard=${whiteboardId}`);
return Logger.info(`Cleared Annotations for userId=${userId} where whiteboard=${whiteboardId}`);
}
return Logger.info(`Removed Annotations where whiteboard=${whiteboardId}`);
if (whiteboardId) {
return Logger.info(`Cleared Annotations for whiteboard=${whiteboardId}`);
}
if (meetingId) {
return Logger.info(`Cleared Annotations (${meetingId})`);
}
return Logger.info('Cleared Annotations (all)');
};
return Annotations.remove(selector, cb);

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
const propTypes = {
@ -24,13 +24,47 @@ const propTypes = {
* Defines the button click handler
* @defaultValue undefined
*/
onClick: PropTypes.func.isRequired,
onClick: (props, propName, componentName) => {
if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
` 'onMouseUp' was not specified in '${componentName}'.`);
}
return null;
},
onMouseDown: (props, propName, componentName) => {
if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
` 'onMouseUp' was not specified in '${componentName}'.`);
}
return null;
},
onMouseUp: (props, propName, componentName) => {
if (!props.onClick && !props.onMouseDown && !props.onMouseUp) {
return new Error('One of props \'onClick\' or \'onMouseDown\' or' +
` 'onMouseUp' was not specified in '${componentName}'.`);
}
return null;
},
onKeyPress: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyUp: PropTypes.func,
setRef: PropTypes.func,
};
const defaultProps = {
disabled: false,
tagName: 'button',
role: 'button',
onClick: undefined,
onMouseDown: undefined,
onMouseUp: undefined,
onKeyPress: undefined,
onKeyDown: undefined,
onKeyUp: undefined,
setRef: undefined,
};
/**
@ -40,7 +74,7 @@ const defaultProps = {
* keyboard users to comply with ARIA standards.
*/
export default class ButtonBase extends Component {
export default class ButtonBase extends React.Component {
constructor(props) {
super(props);
@ -60,36 +94,38 @@ export default class ButtonBase extends Component {
if (!this.props.disabled && typeof eventHandler === 'function') {
return eventHandler(...args);
}
return null;
}
// Define Mouse Event Handlers
internalClickHandler(event) {
return this.validateDisabled(this.props.onClick, ...arguments);
internalClickHandler(...args) {
return this.validateDisabled(this.props.onClick, ...args);
}
internalDoubleClickHandler(event) {
return this.validateDisabled(this.props.onDoubleClick, ...arguments);
internalDoubleClickHandler(...args) {
return this.validateDisabled(this.props.onDoubleClick, ...args);
}
internalMouseDownHandler(event) {
return this.validateDisabled(this.props.onMouseDown, ...arguments);
internalMouseDownHandler(...args) {
return this.validateDisabled(this.props.onMouseDown, ...args);
}
internalMouseUpHandler() {
return this.validateDisabled(this.props.onMouseUp, ...arguments);
internalMouseUpHandler(...args) {
return this.validateDisabled(this.props.onMouseUp, ...args);
}
// Define Keyboard Event Handlers
internalKeyPressHandler() {
return this.validateDisabled(this.props.onKeyPress, ...arguments);
internalKeyPressHandler(...args) {
return this.validateDisabled(this.props.onKeyPress, ...args);
}
internalKeyDownHandler() {
return this.validateDisabled(this.props.onKeyDown, ...arguments);
internalKeyDownHandler(...args) {
return this.validateDisabled(this.props.onKeyDown, ...args);
}
internalKeyUpHandler() {
return this.validateDisabled(this.props.onKeyUp, ...arguments);
internalKeyUpHandler(...args) {
return this.validateDisabled(this.props.onKeyUp, ...args);
}
render() {
@ -111,8 +147,12 @@ export default class ButtonBase extends Component {
delete remainingProps.onKeyDown;
delete remainingProps.onKeyUp;
// Delete setRef callback if it exists
delete remainingProps.setRef;
return (
<Component
ref={this.props.setRef}
aria-label={this.props.label}
aria-disabled={this.props.disabled}

View File

@ -1,8 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import styles from '../styles';
const DropdownListSeparator = (props, { style, className }) => (
<li style={style} className={cx(styles.separator, className)} />);
const DropdownListSeparator = ({ style, className }) =>
(
<li style={style} className={cx(styles.separator, className)} />
);
DropdownListSeparator.propTypes = {
style: PropTypes.shape({}),
className: PropTypes.string,
};
DropdownListSeparator.defaultProps = {
style: null,
className: null,
};
export default DropdownListSeparator;

View File

@ -31,6 +31,7 @@ $item-border-focus: $color-blue-lighter;
display: flex;
flex: 1 1 100%;
height: 1px;
min-height: 1px;
background-color: $color-gray-lighter;
padding: 0;
margin-left: -($line-height-computed / 2);

View File

@ -18,6 +18,17 @@ export default class PresentationOverlay extends Component {
// id of the setInterval()
this.intervalId = 0;
// Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent
// onmousedown from triggering right after. Thus we have to track it manually.
// In case if it's fixed one day - there is another issue, React one.
// https://github.com/facebook/react/issues/9809
// Check it to figure if you can add onTouchStart in render(), or should use raw DOM api
this.touchStarted = false;
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.handleTouchCancel = this.handleTouchCancel.bind(this);
this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
this.checkCursor = this.checkCursor.bind(this);
this.mouseEnterHandler = this.mouseEnterHandler.bind(this);
@ -67,7 +78,77 @@ export default class PresentationOverlay extends Component {
return point;
}
handleTouchStart(event) {
// to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box
event.preventDefault();
window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
window.addEventListener('touchcancel', this.handleTouchCancel, true);
this.touchStarted = true;
const { clientX, clientY } = event.changedTouches[0];
this.currentClientX = clientX;
this.currentClientY = clientY;
const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL);
this.intervalId = intervalId;
}
handleTouchMove(event) {
const { clientX, clientY } = event.changedTouches[0];
this.currentClientX = clientX;
this.currentClientY = clientY;
}
handleTouchEnd(event) {
event.preventDefault();
// touch ended, removing the interval
clearInterval(this.intervalId);
this.intervalId = 0;
// resetting the touchStarted flag
this.touchStarted = false;
// setting the coords to negative values and send the last message (the cursor will disappear)
this.currentClientX = -1;
this.currentClientY = -1;
this.checkCursor();
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
}
handleTouchCancel(event) {
event.preventDefault();
// touch was cancelled, removing the interval
clearInterval(this.intervalId);
this.intervalId = 0;
// resetting the touchStarted flag
this.touchStarted = false;
// setting the coords to negative values and send the last message (the cursor will disappear)
this.currentClientX = -1;
this.currentClientY = -1;
this.checkCursor();
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
}
mouseMoveHandler(event) {
if (this.touchStarted) {
return;
}
// for the case where you change settings in one of the lists (which are displayed on the slide)
// the mouse starts pointing to the slide right away and mouseEnter doesn't fire
// so we call it manually here
@ -75,11 +156,15 @@ export default class PresentationOverlay extends Component {
this.mouseEnterHandler();
}
this.currentClientX = event.nativeEvent.clientX;
this.currentClientY = event.nativeEvent.clientY;
this.currentClientX = event.clientX;
this.currentClientY = event.clientY;
}
mouseEnterHandler() {
if (this.touchStarted) {
return;
}
const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL);
this.intervalId = intervalId;
}
@ -105,10 +190,11 @@ export default class PresentationOverlay extends Component {
height={this.props.slideHeight}
>
<div
onTouchStart={this.handleTouchStart}
onMouseOut={this.mouseOutHandler}
onMouseEnter={this.mouseEnterHandler}
onMouseMove={this.mouseMoveHandler}
style={{ width: '100%', height: '100%' }}
style={{ width: '100%', height: '100%', touchAction: 'none' }}
>
{this.props.children}
</div>

View File

@ -55,6 +55,19 @@ export default class TextDrawComponent extends Component {
}
componentDidMount() {
// iOS doesn't show the keyboard if the input field was focused by event NOT invoked by a user
// by it still technically moves the focus there
// that's why we have a separate case for iOS - we don't focus here automatically
// but we focus on the next "tap" invoked by a user
const iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;
// unsupported Firefox condition (not iOS though) can be removed when FF 59 is released
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1409113
const unsupportedFirefox = navigator.userAgent.indexOf('Firefox/57') !== -1
|| navigator.userAgent.indexOf('Firefox/58') !== -1;
if (iOS || unsupportedFirefox) { return; }
if (this.props.isActive && this.props.annotation.status !== DRAW_END) {
this.handleFocus();
}

View File

@ -6,7 +6,6 @@ import PencilDrawListener from './pencil-draw-listener/component';
import PanZoomDrawListener from './pan-zoom-draw-listener/component';
export default class WhiteboardOverlay extends Component {
// a function to transform a screen point to svg point
// accepts and returns a point of type SvgPoint and an svg object
static coordinateTransform(screenPoint, someSvgObject) {
@ -48,11 +47,11 @@ export default class WhiteboardOverlay extends Component {
// this function receives an event from the mouse event attached to the window
// it transforms the coordinate to the main svg coordinate system
getTransformedSvgPoint(event) {
getTransformedSvgPoint(clientX, clientY) {
const svgObject = this.props.getSvgRef();
const svgPoint = svgObject.createSVGPoint();
svgPoint.x = event.clientX;
svgPoint.y = event.clientY;
svgPoint.x = clientX;
svgPoint.y = clientY;
const transformedSvgPoint = WhiteboardOverlay.coordinateTransform(svgPoint, svgObject);
return transformedSvgPoint;
@ -84,10 +83,11 @@ export default class WhiteboardOverlay extends Component {
// this function receives a transformed svg coordinate and checks if it's not out of bounds
checkIfOutOfBounds(point) {
const { viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight } = this.props;
const {
viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
} = this.props;
let x = point.x;
let y = point.y;
let { x, y } = point;
// set this flag to true if either x or y are out of bounds
let shouldUnselect = false;
@ -120,7 +120,8 @@ export default class WhiteboardOverlay extends Component {
}
render() {
const { drawSettings,
const {
drawSettings,
userId,
whiteboardId,
sendAnnotation,

View File

@ -22,8 +22,13 @@ export default class PencilDrawListener extends Component {
this.mouseDownHandler = this.mouseDownHandler.bind(this);
this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
this.mouseUpHandler = this.mouseUpHandler.bind(this);
this.resetState = this.resetState.bind(this);
this.sendLastMessage = this.sendLastMessage.bind(this);
this.sendCoordinates = this.sendCoordinates.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.handleTouchCancel = this.handleTouchCancel.bind(this);
}
componentDidMount() {
@ -38,41 +43,31 @@ export default class PencilDrawListener extends Component {
this.sendLastMessage();
}
// main mouse down handler
mouseDownHandler(event) {
if (!this.isDrawing) {
window.addEventListener('mouseup', this.mouseUpHandler);
window.addEventListener('mousemove', this.mouseMoveHandler, true);
this.isDrawing = true;
commonDrawStartHandler(clientX, clientY) {
// changing isDrawing to true
this.isDrawing = true;
const {
getTransformedSvgPoint,
generateNewShapeId,
svgCoordinateToPercentages,
} = this.props.actions;
const {
getTransformedSvgPoint,
generateNewShapeId,
svgCoordinateToPercentages,
} = this.props.actions;
// sending the first message
let transformedSvgPoint = getTransformedSvgPoint(event);
// sending the first message
let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// transforming svg coordinate to percentages relative to the slide width/height
transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
// transforming svg coordinate to percentages relative to the slide width/height
transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
// sending the first message
const _points = [transformedSvgPoint.x, transformedSvgPoint.y];
this.handleDrawPencil(_points, DRAW_START, generateNewShapeId());
// sending the first message
const _points = [transformedSvgPoint.x, transformedSvgPoint.y];
this.handleDrawPencil(_points, DRAW_START, generateNewShapeId());
// All the DRAW_UPDATE messages will be send on timer by sendCoordinates func
this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY);
// if you switch to a different window using Alt+Tab while mouse is down and release it
// it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
} else {
this.sendLastMessage();
}
// All the DRAW_UPDATE messages will be send on timer by sendCoordinates func
this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY);
}
// main mouse move handler
mouseMoveHandler(event) {
commonDrawMoveHandler(clientX, clientY) {
if (this.isDrawing) {
const {
checkIfOutOfBounds,
@ -81,7 +76,7 @@ export default class PencilDrawListener extends Component {
} = this.props.actions;
// get the transformed svg coordinate
let transformedSvgPoint = getTransformedSvgPoint(event);
let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// check if it's out of bounds
transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@ -95,6 +90,58 @@ export default class PencilDrawListener extends Component {
}
}
handleTouchStart(event) {
event.preventDefault();
if (!this.isDrawing) {
window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
window.addEventListener('touchcancel', this.handleTouchCancel, true);
const { clientX, clientY } = event.changedTouches[0];
this.commonDrawStartHandler(clientX, clientY);
// if you switch to a different window using Alt+Tab while mouse is down and release it
// it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
} else {
this.sendLastMessage();
}
}
handleTouchMove(event) {
const { clientX, clientY } = event.changedTouches[0];
this.commonDrawMoveHandler(clientX, clientY);
}
handleTouchEnd() {
this.sendLastMessage();
}
handleTouchCancel() {
this.sendLastMessage();
}
// main mouse down handler
mouseDownHandler(event) {
if (!this.isDrawing) {
window.addEventListener('mouseup', this.mouseUpHandler);
window.addEventListener('mousemove', this.mouseMoveHandler, true);
const { clientX, clientY } = event;
this.commonDrawStartHandler(clientX, clientY);
// if you switch to a different window using Alt+Tab while mouse is down and release it
// it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
} else {
this.sendLastMessage();
}
}
// main mouse move handler
mouseMoveHandler(event) {
const { clientX, clientY } = event;
this.commonDrawMoveHandler(clientX, clientY);
}
// main mouse up handler
mouseUpHandler() {
this.sendLastMessage();
@ -153,21 +200,30 @@ export default class PencilDrawListener extends Component {
getCurrentShapeId(),
[Math.round(physicalSlideWidth), Math.round(physicalSlideHeight)],
);
// resetting the current info
this.points = [];
this.isDrawing = false;
window.removeEventListener('mouseup', this.mouseUpHandler);
window.removeEventListener('mousemove', this.mouseMoveHandler, true);
this.resetState();
}
}
resetState() {
// resetting the current info
this.points = [];
this.isDrawing = false;
// mouseup and mousemove are removed on desktop
window.removeEventListener('mouseup', this.mouseUpHandler);
window.removeEventListener('mousemove', this.mouseMoveHandler, true);
// touchend, touchmove and touchcancel are removed on devices
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
}
render() {
return (
<div
onTouchStart={this.handleTouchStart}
role="presentation"
className={styles.pencil}
style={{ width: '100%', height: '100%' }}
style={{ width: '100%', height: '100%', touchAction: 'none' }}
onMouseDown={this.mouseDownHandler}
/>
);
@ -175,7 +231,7 @@ export default class PencilDrawListener extends Component {
}
PencilDrawListener.propTypes = {
// Defines a whiteboard id, which needed to publish an annotation message
// Defines a whiteboard id, which needed to publish an annotation message
whiteboardId: PropTypes.string.isRequired,
// Defines a user id, which needed to publish an annotation message
userId: PropTypes.string.isRequired,

View File

@ -40,6 +40,10 @@ export default class ShapeDrawListener extends Component {
this.resetState = this.resetState.bind(this);
this.sendLastMessage = this.sendLastMessage.bind(this);
this.sendCoordinates = this.sendCoordinates.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.handleTouchCancel = this.handleTouchCancel.bind(this);
}
componentDidMount() {
@ -54,16 +58,7 @@ export default class ShapeDrawListener extends Component {
this.sendLastMessage();
}
// main mouse down handler
handleMouseDown(event) {
// Sometimes when you Alt+Tab while drawing it can happen that your mouse is up,
// but the browser didn't catch it. So check it here.
if (this.isDrawing) {
return this.sendLastMessage();
}
window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove, true);
commonDrawStartHandler(clientX, clientY) {
this.isDrawing = true;
const {
@ -73,7 +68,7 @@ export default class ShapeDrawListener extends Component {
} = this.props.actions;
// sending the first message
let transformedSvgPoint = getTransformedSvgPoint(event);
let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// transforming svg coordinate to percentages relative to the slide width/height
transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
@ -97,12 +92,9 @@ export default class ShapeDrawListener extends Component {
// All the messages will be send on timer by sendCoordinates func
this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY);
return true;
}
// main mouse move handler
handleMouseMove(event) {
commonDrawMoveHandler(clientX, clientY) {
if (!this.isDrawing) {
return;
}
@ -114,7 +106,7 @@ export default class ShapeDrawListener extends Component {
} = this.props.actions;
// get the transformed svg coordinate
let transformedSvgPoint = getTransformedSvgPoint(event);
let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// check if it's out of bounds
transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@ -126,6 +118,58 @@ export default class ShapeDrawListener extends Component {
this.currentCoordinate = transformedSvgPoint;
}
handleTouchStart(event) {
event.preventDefault();
if (!this.isDrawing) {
window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
window.addEventListener('touchcancel', this.handleTouchCancel, true);
const { clientX, clientY } = event.changedTouches[0];
this.commonDrawStartHandler(clientX, clientY);
// if you switch to a different window using Alt+Tab while mouse is down and release it
// it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
} else {
this.sendLastMessage();
}
}
handleTouchMove(event) {
const { clientX, clientY } = event.changedTouches[0];
this.commonDrawMoveHandler(clientX, clientY);
}
handleTouchEnd() {
this.sendLastMessage();
}
handleTouchCancel() {
this.sendLastMessage();
}
// main mouse down handler
handleMouseDown(event) {
// Sometimes when you Alt+Tab while drawing it can happen that your mouse is up,
// but the browser didn't catch it. So check it here.
if (this.isDrawing) {
return this.sendLastMessage();
}
window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove, true);
const { clientX, clientY } = event;
return this.commonDrawStartHandler(clientX, clientY);
}
// main mouse move handler
handleMouseMove(event) {
const { clientX, clientY } = event;
this.commonDrawMoveHandler(clientX, clientY);
}
// main mouse up handler
handleMouseUp() {
this.sendLastMessage();
@ -190,6 +234,10 @@ export default class ShapeDrawListener extends Component {
// resetting the current drawing state
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove, true);
// touchend, touchmove and touchcancel are removed on devices
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
this.isDrawing = false;
this.currentStatus = undefined;
this.initialCoordinate = {
@ -238,12 +286,13 @@ export default class ShapeDrawListener extends Component {
}
render() {
const tool = this.props.drawSettings.tool;
const { tool } = this.props.drawSettings;
return (
<div
onTouchStart={this.handleTouchStart}
role="presentation"
className={styles[tool]}
style={{ width: '100%', height: '100%' }}
style={{ width: '100%', height: '100%', touchAction: 'none' }}
onMouseDown={this.handleMouseDown}
/>
);

View File

@ -39,11 +39,23 @@ export default class TextDrawListener extends Component {
// current text shape status, it may change between DRAW_START, DRAW_UPDATE, DRAW_END
this.currentStatus = '';
// Mobile Firefox has a bug where e.preventDefault on touchstart doesn't prevent
// onmousedown from triggering right after. Thus we have to track it manually.
// In case if it's fixed one day - there is another issue, React one.
// https://github.com/facebook/react/issues/9809
// Check it to figure if you can add onTouchStart in render(), or should use raw DOM api
this.hasBeenTouchedRecently = false;
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.resetState = this.resetState.bind(this);
this.sendLastMessage = this.sendLastMessage.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.handleTouchCancel = this.handleTouchCancel.bind(this);
this.checkTextAreaFocus = this.checkTextAreaFocus.bind(this);
}
componentDidMount() {
@ -92,46 +104,85 @@ export default class TextDrawListener extends Component {
componentWillUnmount() {
window.removeEventListener('beforeunload', this.sendLastMessage);
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove, true);
// sending the last message on componentDidUnmount
// for example in case when you switched a tool while drawing text shape
this.sendLastMessage();
}
// checks if the input textarea is focused or not, and if not - moves focus there
// returns false if text area wasn't focused
// returns true if textarea was focused
// currently used only with iOS devices
checkTextAreaFocus() {
const { getCurrentShapeId } = this.props.actions;
const textarea = document.getElementById(getCurrentShapeId());
if (document.activeElement === textarea) {
return true;
}
textarea.focus();
return false;
}
handleTouchStart(event) {
this.hasBeenTouchedRecently = true;
setTimeout(() => { this.hasBeenTouchedRecently = false; }, 500);
// to prevent default behavior (scrolling) on devices (in Safari), when you draw a text box
event.preventDefault();
// if our current drawing state is not drawing the box and not writing the text
if (!this.state.isDrawing && !this.state.isWritingText) {
window.addEventListener('touchend', this.handleTouchEnd, { passive: false });
window.addEventListener('touchmove', this.handleTouchMove, { passive: false });
window.addEventListener('touchcancel', this.handleTouchCancel, true);
const { clientX, clientY } = event.changedTouches[0];
this.commonDrawStartHandler(clientX, clientY);
// this case is specifically for iOS, since text shape is working in 3 steps there:
// touch to draw a box -> tap to focus -> tap to publish
} else if (!this.state.isDrawing && this.state.isWritingText && !this.checkTextAreaFocus()) {
// if you switch to a different window using Alt+Tab while mouse is down and release it
// it wont catch mouseUp and will keep tracking the movements. Thus we need this check.
} else {
this.sendLastMessage();
}
}
handleTouchMove(event) {
const { clientX, clientY } = event.changedTouches[0];
this.commonDrawMoveHandler(clientX, clientY);
}
handleTouchEnd() {
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
this.commonDrawEndHandler();
}
handleTouchCancel() {
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
this.commonDrawEndHandler();
}
// main mouse down handler
handleMouseDown(event) {
this.mouseDownText(event);
}
if (this.hasBeenTouchedRecently) {
return;
}
// main mouse up handler
handleMouseUp(event) {
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove, true);
this.mouseUpText(event);
}
// main mouse move handler
handleMouseMove(event) {
this.mouseMoveText(event);
}
mouseDownText(event) {
// if our current drawing state is not drawing the box and not writing the text
if (!this.state.isDrawing && !this.state.isWritingText) {
window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove, true);
// saving initial X and Y coordinates for further displaying of the textarea
this.initialX = event.nativeEvent.offsetX;
this.initialY = event.nativeEvent.offsetY;
this.setState({
textBoxX: event.nativeEvent.offsetX,
textBoxY: event.nativeEvent.offsetY,
isDrawing: true,
});
const { clientX, clientY } = event;
this.commonDrawStartHandler(clientX, clientY);
// second case is when a user finished writing the text and publishes the final result
} else {
@ -140,6 +191,35 @@ export default class TextDrawListener extends Component {
}
}
// main mouse move handler
handleMouseMove(event) {
const { clientX, clientY } = event;
this.commonDrawMoveHandler(clientX, clientY);
}
// main mouse up handler
handleMouseUp() {
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove, true);
this.commonDrawEndHandler();
}
commonDrawStartHandler(clientX, clientY) {
const { getTransformedSvgPoint } = this.props.actions;
const transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// saving initial X and Y coordinates for further displaying of the textarea
this.initialX = transformedSvgPoint.x;
this.initialY = transformedSvgPoint.y;
this.setState({
textBoxX: transformedSvgPoint.x,
textBoxY: transformedSvgPoint.y,
isDrawing: true,
});
}
sendLastMessage() {
if (!this.state.isWritingText) {
return;
@ -161,6 +241,14 @@ export default class TextDrawListener extends Component {
}
resetState() {
// resetting the current drawing state
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove, true);
// touchend, touchmove and touchcancel are removed on devices
window.removeEventListener('touchend', this.handleTouchEnd, { passive: false });
window.removeEventListener('touchmove', this.handleTouchMove, { passive: false });
window.removeEventListener('touchcancel', this.handleTouchCancel, true);
// resetting the text shape session values
this.props.actions.resetTextShapeSession();
// resetting the current state
@ -182,11 +270,11 @@ export default class TextDrawListener extends Component {
});
}
mouseMoveText(event) {
commonDrawMoveHandler(clientX, clientY) {
const { checkIfOutOfBounds, getTransformedSvgPoint } = this.props.actions;
// get the transformed svg coordinate
let transformedSvgPoint = getTransformedSvgPoint(event);
let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// check if it's out of bounds
transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@ -197,9 +285,9 @@ export default class TextDrawListener extends Component {
// calculating the width and height of the displayed text box
const width = transformedSvgPoint.x > this.initialX ?
transformedSvgPoint.x - this.initialX : this.initialX - transformedSvgPoint.x;
transformedSvgPoint.x - this.initialX : this.initialX - transformedSvgPoint.x;
const height = transformedSvgPoint.y > this.initialY ?
transformedSvgPoint.y - this.initialY : this.initialY - transformedSvgPoint.y;
transformedSvgPoint.y - this.initialY : this.initialY - transformedSvgPoint.y;
this.setState({
textBoxWidth: width,
@ -210,13 +298,14 @@ export default class TextDrawListener extends Component {
}
mouseUpText() {
commonDrawEndHandler() {
// TODO - find if the size is large enough to display the text area
if (!this.state.isDrawing && this.state.isWritingText) {
return;
}
const { generateNewShapeId,
const {
generateNewShapeId,
getCurrentShapeId,
setTextShapeActiveId,
} = this.props.actions;
@ -284,8 +373,9 @@ export default class TextDrawListener extends Component {
<div
role="presentation"
className={styles.text}
style={{ width: '100%', height: '100%' }}
style={{ width: '100%', height: '100%', touchAction: 'none' }}
onMouseDown={this.handleMouseDown}
onTouchStart={this.handleTouchStart}
>
{this.state.isDrawing ?
<svg

View File

@ -1,16 +1,51 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '/imports/ui/components/button/component';
import _ from 'lodash';
import styles from '../styles';
export default class ToolbarMenuItem extends Component {
constructor() {
super();
this.handleItemClick = this.handleItemClick.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleOnMouseUp = this.handleOnMouseUp.bind(this);
this.setRef = this.setRef.bind(this);
}
handleItemClick() {
// generating a unique ref string for the toolbar-item
componentWillMount() {
this.uniqueRef = _.uniqueId('toolbar-menu-item');
}
componentDidMount() {
// adding and removing touchstart events can be done via standard React way
// by passing onTouchStart={this.funcName} once they stop triggering mousedown events
// see https://github.com/facebook/react/issues/9809
this[this.uniqueRef].addEventListener('touchstart', this.handleTouchStart);
}
componentWillUnmount() {
this[this.uniqueRef].removeEventListener('touchstart', this.handleTouchStart);
}
setRef(ref) {
this[this.uniqueRef] = ref;
}
// we have to use touchStart and on mouseUp in order to be able to use the toolbar
// with the text shape on mobile devices
// (using the toolbar while typing text shouldn't move focus out of the textarea)
handleTouchStart(event) {
event.preventDefault();
const { objectToReturn, onItemClick } = this.props;
// if there is a submenu name, then pass it to onClick
// if not - it's probably "Undo", "Clear All", "Multi-user", etc.
// in the second case we'll pass undefined and it will work fine anyway
onItemClick(objectToReturn);
}
handleOnMouseUp() {
const { objectToReturn, onItemClick } = this.props;
// if there is a submenu name, then pass it to onClick
// if not - it's probably "Undo", "Clear All", "Multi-user", etc.
@ -24,14 +59,15 @@ export default class ToolbarMenuItem extends Component {
<Button
hideLabel
role="button"
color={'default'}
size={'md'}
color="default"
size="md"
label={this.props.label}
icon={this.props.icon ? this.props.icon : null}
customIcon={this.props.customIcon ? this.props.customIcon : null}
onClick={this.handleItemClick}
onMouseUp={this.handleOnMouseUp}
onBlur={this.props.onBlur}
className={this.props.className}
setRef={this.setRef}
/>
{this.props.children}
</div>

View File

@ -1,20 +1,51 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '/imports/ui/components/button/component';
import _ from 'lodash';
import styles from '../styles';
export default class ToolbarSubmenuItem extends Component {
constructor() {
super();
this.handleItemClick = this.handleItemClick.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleOnMouseUp = this.handleOnMouseUp.bind(this);
this.setRef = this.setRef.bind(this);
}
handleItemClick() {
// generating a unique ref string for the toolbar-item
componentWillMount() {
this.uniqueRef = _.uniqueId('toolbar-submenu-item');
}
componentDidMount() {
// adding and removing touchstart events can be done via standard React way
// by passing onTouchStart={this.funcName} once they stop triggering mousedown events
// see https://github.com/facebook/react/issues/9809
this[this.uniqueRef].addEventListener('touchstart', this.handleTouchStart);
}
componentWillUnmount() {
this[this.uniqueRef].removeEventListener('touchstart', this.handleTouchStart);
}
setRef(ref) {
this[this.uniqueRef] = ref;
}
// we have to use touchStart and on mouseUp in order to be able to use the toolbar
// with the text shape on mobile devices
// (using the toolbar while typing text shouldn't move focus out of the textarea)
handleTouchStart(event) {
event.preventDefault();
const { objectToReturn, onItemClick } = this.props;
// if there is a submenu name, then pass it to onClick
// if not - it's probably "Undo", "Clear All", "Multi-user", etc.
// in the second case we'll pass undefined and it will work fine anyway
// returning the selected object
onItemClick(objectToReturn);
}
handleOnMouseUp() {
const { objectToReturn, onItemClick } = this.props;
// returning the selected object
onItemClick(objectToReturn);
}
@ -24,13 +55,14 @@ export default class ToolbarSubmenuItem extends Component {
<Button
hideLabel
role="button"
color={'default'}
size={'md'}
color="default"
size="md"
label={this.props.label}
icon={this.props.icon}
customIcon={this.props.customIcon}
onClick={this.handleItemClick}
onMouseUp={this.handleOnMouseUp}
className={this.props.className}
setRef={this.setRef}
/>
</div>
);