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

View File

@ -21,15 +21,19 @@ export default function clearAnnotations(meetingId, whiteboardId, userId) {
return Logger.error(`Removing Annotations from collection: ${err}`); return Logger.error(`Removing Annotations from collection: ${err}`);
} }
if (!meetingId) {
return Logger.info('Cleared Annotations (all)');
}
if (userId) { 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); 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'; import PropTypes from 'prop-types';
const propTypes = { const propTypes = {
@ -24,13 +24,47 @@ const propTypes = {
* Defines the button click handler * Defines the button click handler
* @defaultValue undefined * @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 = { const defaultProps = {
disabled: false, disabled: false,
tagName: 'button', 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. * keyboard users to comply with ARIA standards.
*/ */
export default class ButtonBase extends Component { export default class ButtonBase extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -60,36 +94,38 @@ export default class ButtonBase extends Component {
if (!this.props.disabled && typeof eventHandler === 'function') { if (!this.props.disabled && typeof eventHandler === 'function') {
return eventHandler(...args); return eventHandler(...args);
} }
return null;
} }
// Define Mouse Event Handlers // Define Mouse Event Handlers
internalClickHandler(event) { internalClickHandler(...args) {
return this.validateDisabled(this.props.onClick, ...arguments); return this.validateDisabled(this.props.onClick, ...args);
} }
internalDoubleClickHandler(event) { internalDoubleClickHandler(...args) {
return this.validateDisabled(this.props.onDoubleClick, ...arguments); return this.validateDisabled(this.props.onDoubleClick, ...args);
} }
internalMouseDownHandler(event) { internalMouseDownHandler(...args) {
return this.validateDisabled(this.props.onMouseDown, ...arguments); return this.validateDisabled(this.props.onMouseDown, ...args);
} }
internalMouseUpHandler() { internalMouseUpHandler(...args) {
return this.validateDisabled(this.props.onMouseUp, ...arguments); return this.validateDisabled(this.props.onMouseUp, ...args);
} }
// Define Keyboard Event Handlers // Define Keyboard Event Handlers
internalKeyPressHandler() { internalKeyPressHandler(...args) {
return this.validateDisabled(this.props.onKeyPress, ...arguments); return this.validateDisabled(this.props.onKeyPress, ...args);
} }
internalKeyDownHandler() { internalKeyDownHandler(...args) {
return this.validateDisabled(this.props.onKeyDown, ...arguments); return this.validateDisabled(this.props.onKeyDown, ...args);
} }
internalKeyUpHandler() { internalKeyUpHandler(...args) {
return this.validateDisabled(this.props.onKeyUp, ...arguments); return this.validateDisabled(this.props.onKeyUp, ...args);
} }
render() { render() {
@ -111,8 +147,12 @@ export default class ButtonBase extends Component {
delete remainingProps.onKeyDown; delete remainingProps.onKeyDown;
delete remainingProps.onKeyUp; delete remainingProps.onKeyUp;
// Delete setRef callback if it exists
delete remainingProps.setRef;
return ( return (
<Component <Component
ref={this.props.setRef}
aria-label={this.props.label} aria-label={this.props.label}
aria-disabled={this.props.disabled} aria-disabled={this.props.disabled}

View File

@ -1,8 +1,21 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames'; import cx from 'classnames';
import styles from '../styles'; import styles from '../styles';
const DropdownListSeparator = (props, { style, className }) => ( const DropdownListSeparator = ({ style, className }) =>
<li style={style} className={cx(styles.separator, 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; export default DropdownListSeparator;

View File

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

View File

@ -18,6 +18,17 @@ export default class PresentationOverlay extends Component {
// id of the setInterval() // id of the setInterval()
this.intervalId = 0; 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.mouseMoveHandler = this.mouseMoveHandler.bind(this);
this.checkCursor = this.checkCursor.bind(this); this.checkCursor = this.checkCursor.bind(this);
this.mouseEnterHandler = this.mouseEnterHandler.bind(this); this.mouseEnterHandler = this.mouseEnterHandler.bind(this);
@ -67,7 +78,77 @@ export default class PresentationOverlay extends Component {
return point; 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) { mouseMoveHandler(event) {
if (this.touchStarted) {
return;
}
// for the case where you change settings in one of the lists (which are displayed on the slide) // 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 // the mouse starts pointing to the slide right away and mouseEnter doesn't fire
// so we call it manually here // so we call it manually here
@ -75,11 +156,15 @@ export default class PresentationOverlay extends Component {
this.mouseEnterHandler(); this.mouseEnterHandler();
} }
this.currentClientX = event.nativeEvent.clientX; this.currentClientX = event.clientX;
this.currentClientY = event.nativeEvent.clientY; this.currentClientY = event.clientY;
} }
mouseEnterHandler() { mouseEnterHandler() {
if (this.touchStarted) {
return;
}
const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL); const intervalId = setInterval(this.checkCursor, CURSOR_INTERVAL);
this.intervalId = intervalId; this.intervalId = intervalId;
} }
@ -105,10 +190,11 @@ export default class PresentationOverlay extends Component {
height={this.props.slideHeight} height={this.props.slideHeight}
> >
<div <div
onTouchStart={this.handleTouchStart}
onMouseOut={this.mouseOutHandler} onMouseOut={this.mouseOutHandler}
onMouseEnter={this.mouseEnterHandler} onMouseEnter={this.mouseEnterHandler}
onMouseMove={this.mouseMoveHandler} onMouseMove={this.mouseMoveHandler}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%', touchAction: 'none' }}
> >
{this.props.children} {this.props.children}
</div> </div>

View File

@ -55,6 +55,19 @@ export default class TextDrawComponent extends Component {
} }
componentDidMount() { 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) { if (this.props.isActive && this.props.annotation.status !== DRAW_END) {
this.handleFocus(); this.handleFocus();
} }

View File

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

View File

@ -22,8 +22,13 @@ export default class PencilDrawListener extends Component {
this.mouseDownHandler = this.mouseDownHandler.bind(this); this.mouseDownHandler = this.mouseDownHandler.bind(this);
this.mouseMoveHandler = this.mouseMoveHandler.bind(this); this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
this.mouseUpHandler = this.mouseUpHandler.bind(this); this.mouseUpHandler = this.mouseUpHandler.bind(this);
this.resetState = this.resetState.bind(this);
this.sendLastMessage = this.sendLastMessage.bind(this); this.sendLastMessage = this.sendLastMessage.bind(this);
this.sendCoordinates = this.sendCoordinates.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() { componentDidMount() {
@ -38,11 +43,8 @@ export default class PencilDrawListener extends Component {
this.sendLastMessage(); this.sendLastMessage();
} }
// main mouse down handler commonDrawStartHandler(clientX, clientY) {
mouseDownHandler(event) { // changing isDrawing to true
if (!this.isDrawing) {
window.addEventListener('mouseup', this.mouseUpHandler);
window.addEventListener('mousemove', this.mouseMoveHandler, true);
this.isDrawing = true; this.isDrawing = true;
const { const {
@ -52,7 +54,7 @@ export default class PencilDrawListener extends Component {
} = this.props.actions; } = this.props.actions;
// sending the first message // sending the first message
let transformedSvgPoint = getTransformedSvgPoint(event); let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// transforming svg coordinate to percentages relative to the slide width/height // transforming svg coordinate to percentages relative to the slide width/height
transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint); transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
@ -63,16 +65,9 @@ export default class PencilDrawListener extends Component {
// All the DRAW_UPDATE messages will be send on timer by sendCoordinates func // All the DRAW_UPDATE messages will be send on timer by sendCoordinates func
this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY); 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();
}
} }
// main mouse move handler commonDrawMoveHandler(clientX, clientY) {
mouseMoveHandler(event) {
if (this.isDrawing) { if (this.isDrawing) {
const { const {
checkIfOutOfBounds, checkIfOutOfBounds,
@ -81,7 +76,7 @@ export default class PencilDrawListener extends Component {
} = this.props.actions; } = this.props.actions;
// get the transformed svg coordinate // get the transformed svg coordinate
let transformedSvgPoint = getTransformedSvgPoint(event); let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// check if it's out of bounds // check if it's out of bounds
transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint); 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 // main mouse up handler
mouseUpHandler() { mouseUpHandler() {
this.sendLastMessage(); this.sendLastMessage();
@ -153,21 +200,30 @@ export default class PencilDrawListener extends Component {
getCurrentShapeId(), getCurrentShapeId(),
[Math.round(physicalSlideWidth), Math.round(physicalSlideHeight)], [Math.round(physicalSlideWidth), Math.round(physicalSlideHeight)],
); );
this.resetState();
}
}
resetState() {
// resetting the current info // resetting the current info
this.points = []; this.points = [];
this.isDrawing = false; this.isDrawing = false;
// mouseup and mousemove are removed on desktop
window.removeEventListener('mouseup', this.mouseUpHandler); window.removeEventListener('mouseup', this.mouseUpHandler);
window.removeEventListener('mousemove', this.mouseMoveHandler, true); 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() { render() {
return ( return (
<div <div
onTouchStart={this.handleTouchStart}
role="presentation" role="presentation"
className={styles.pencil} className={styles.pencil}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%', touchAction: 'none' }}
onMouseDown={this.mouseDownHandler} onMouseDown={this.mouseDownHandler}
/> />
); );

View File

@ -40,6 +40,10 @@ export default class ShapeDrawListener extends Component {
this.resetState = this.resetState.bind(this); this.resetState = this.resetState.bind(this);
this.sendLastMessage = this.sendLastMessage.bind(this); this.sendLastMessage = this.sendLastMessage.bind(this);
this.sendCoordinates = this.sendCoordinates.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() { componentDidMount() {
@ -54,16 +58,7 @@ export default class ShapeDrawListener extends Component {
this.sendLastMessage(); this.sendLastMessage();
} }
// main mouse down handler commonDrawStartHandler(clientX, clientY) {
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);
this.isDrawing = true; this.isDrawing = true;
const { const {
@ -73,7 +68,7 @@ export default class ShapeDrawListener extends Component {
} = this.props.actions; } = this.props.actions;
// sending the first message // sending the first message
let transformedSvgPoint = getTransformedSvgPoint(event); let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// transforming svg coordinate to percentages relative to the slide width/height // transforming svg coordinate to percentages relative to the slide width/height
transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint); transformedSvgPoint = svgCoordinateToPercentages(transformedSvgPoint);
@ -97,12 +92,9 @@ export default class ShapeDrawListener extends Component {
// All the messages will be send on timer by sendCoordinates func // All the messages will be send on timer by sendCoordinates func
this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY); this.intervalId = setInterval(this.sendCoordinates, MESSAGE_FREQUENCY);
return true;
} }
// main mouse move handler commonDrawMoveHandler(clientX, clientY) {
handleMouseMove(event) {
if (!this.isDrawing) { if (!this.isDrawing) {
return; return;
} }
@ -114,7 +106,7 @@ export default class ShapeDrawListener extends Component {
} = this.props.actions; } = this.props.actions;
// get the transformed svg coordinate // get the transformed svg coordinate
let transformedSvgPoint = getTransformedSvgPoint(event); let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// check if it's out of bounds // check if it's out of bounds
transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint); transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@ -126,6 +118,58 @@ export default class ShapeDrawListener extends Component {
this.currentCoordinate = transformedSvgPoint; 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 // main mouse up handler
handleMouseUp() { handleMouseUp() {
this.sendLastMessage(); this.sendLastMessage();
@ -190,6 +234,10 @@ export default class ShapeDrawListener extends Component {
// resetting the current drawing state // resetting the current drawing state
window.removeEventListener('mouseup', this.handleMouseUp); window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove, true); 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.isDrawing = false;
this.currentStatus = undefined; this.currentStatus = undefined;
this.initialCoordinate = { this.initialCoordinate = {
@ -238,12 +286,13 @@ export default class ShapeDrawListener extends Component {
} }
render() { render() {
const tool = this.props.drawSettings.tool; const { tool } = this.props.drawSettings;
return ( return (
<div <div
onTouchStart={this.handleTouchStart}
role="presentation" role="presentation"
className={styles[tool]} className={styles[tool]}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%', touchAction: 'none' }}
onMouseDown={this.handleMouseDown} 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 // current text shape status, it may change between DRAW_START, DRAW_UPDATE, DRAW_END
this.currentStatus = ''; 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.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this); this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this); this.handleMouseUp = this.handleMouseUp.bind(this);
this.resetState = this.resetState.bind(this); this.resetState = this.resetState.bind(this);
this.sendLastMessage = this.sendLastMessage.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() { componentDidMount() {
@ -92,46 +104,85 @@ export default class TextDrawListener extends Component {
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('beforeunload', this.sendLastMessage); window.removeEventListener('beforeunload', this.sendLastMessage);
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mousemove', this.handleMouseMove, true);
// sending the last message on componentDidUnmount // sending the last message on componentDidUnmount
// for example in case when you switched a tool while drawing text shape // for example in case when you switched a tool while drawing text shape
this.sendLastMessage(); 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 // main mouse down handler
handleMouseDown(event) { 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 our current drawing state is not drawing the box and not writing the text
if (!this.state.isDrawing && !this.state.isWritingText) { if (!this.state.isDrawing && !this.state.isWritingText) {
window.addEventListener('mouseup', this.handleMouseUp); window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('mousemove', this.handleMouseMove, true); window.addEventListener('mousemove', this.handleMouseMove, true);
// saving initial X and Y coordinates for further displaying of the textarea const { clientX, clientY } = event;
this.initialX = event.nativeEvent.offsetX; this.commonDrawStartHandler(clientX, clientY);
this.initialY = event.nativeEvent.offsetY;
this.setState({
textBoxX: event.nativeEvent.offsetX,
textBoxY: event.nativeEvent.offsetY,
isDrawing: true,
});
// second case is when a user finished writing the text and publishes the final result // second case is when a user finished writing the text and publishes the final result
} else { } 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() { sendLastMessage() {
if (!this.state.isWritingText) { if (!this.state.isWritingText) {
return; return;
@ -161,6 +241,14 @@ export default class TextDrawListener extends Component {
} }
resetState() { 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 // resetting the text shape session values
this.props.actions.resetTextShapeSession(); this.props.actions.resetTextShapeSession();
// resetting the current state // 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; const { checkIfOutOfBounds, getTransformedSvgPoint } = this.props.actions;
// get the transformed svg coordinate // get the transformed svg coordinate
let transformedSvgPoint = getTransformedSvgPoint(event); let transformedSvgPoint = getTransformedSvgPoint(clientX, clientY);
// check if it's out of bounds // check if it's out of bounds
transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint); transformedSvgPoint = checkIfOutOfBounds(transformedSvgPoint);
@ -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 // TODO - find if the size is large enough to display the text area
if (!this.state.isDrawing && this.state.isWritingText) { if (!this.state.isDrawing && this.state.isWritingText) {
return; return;
} }
const { generateNewShapeId, const {
generateNewShapeId,
getCurrentShapeId, getCurrentShapeId,
setTextShapeActiveId, setTextShapeActiveId,
} = this.props.actions; } = this.props.actions;
@ -284,8 +373,9 @@ export default class TextDrawListener extends Component {
<div <div
role="presentation" role="presentation"
className={styles.text} className={styles.text}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%', touchAction: 'none' }}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onTouchStart={this.handleTouchStart}
> >
{this.state.isDrawing ? {this.state.isDrawing ?
<svg <svg

View File

@ -1,16 +1,51 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from '/imports/ui/components/button/component'; import Button from '/imports/ui/components/button/component';
import _ from 'lodash';
import styles from '../styles'; import styles from '../styles';
export default class ToolbarMenuItem extends Component { export default class ToolbarMenuItem extends Component {
constructor() { constructor() {
super(); 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; const { objectToReturn, onItemClick } = this.props;
// if there is a submenu name, then pass it to onClick // if there is a submenu name, then pass it to onClick
// if not - it's probably "Undo", "Clear All", "Multi-user", etc. // if not - it's probably "Undo", "Clear All", "Multi-user", etc.
@ -24,14 +59,15 @@ export default class ToolbarMenuItem extends Component {
<Button <Button
hideLabel hideLabel
role="button" role="button"
color={'default'} color="default"
size={'md'} size="md"
label={this.props.label} label={this.props.label}
icon={this.props.icon ? this.props.icon : null} icon={this.props.icon ? this.props.icon : null}
customIcon={this.props.customIcon ? this.props.customIcon : null} customIcon={this.props.customIcon ? this.props.customIcon : null}
onClick={this.handleItemClick} onMouseUp={this.handleOnMouseUp}
onBlur={this.props.onBlur} onBlur={this.props.onBlur}
className={this.props.className} className={this.props.className}
setRef={this.setRef}
/> />
{this.props.children} {this.props.children}
</div> </div>

View File

@ -1,20 +1,51 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from '/imports/ui/components/button/component'; import Button from '/imports/ui/components/button/component';
import _ from 'lodash';
import styles from '../styles'; import styles from '../styles';
export default class ToolbarSubmenuItem extends Component { export default class ToolbarSubmenuItem extends Component {
constructor() { constructor() {
super(); 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; const { objectToReturn, onItemClick } = this.props;
// if there is a submenu name, then pass it to onClick // returning the selected object
// if not - it's probably "Undo", "Clear All", "Multi-user", etc. onItemClick(objectToReturn);
// in the second case we'll pass undefined and it will work fine anyway }
handleOnMouseUp() {
const { objectToReturn, onItemClick } = this.props;
// returning the selected object
onItemClick(objectToReturn); onItemClick(objectToReturn);
} }
@ -24,13 +55,14 @@ export default class ToolbarSubmenuItem extends Component {
<Button <Button
hideLabel hideLabel
role="button" role="button"
color={'default'} color="default"
size={'md'} size="md"
label={this.props.label} label={this.props.label}
icon={this.props.icon} icon={this.props.icon}
customIcon={this.props.customIcon} customIcon={this.props.customIcon}
onClick={this.handleItemClick} onMouseUp={this.handleOnMouseUp}
className={this.props.className} className={this.props.className}
setRef={this.setRef}
/> />
</div> </div>
); );