diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss index 40d8d74bf344ec61f15f3619063525bf48db5191..8fa3556868afa4885a4406fd7a545f337e195d9c 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/styles.scss @@ -21,7 +21,7 @@ } } -.left{ +.left { position: absolute; @include mq($small-only) { bottom: $sm-padding-x; @@ -31,9 +31,7 @@ .centerWithActions { @include mq($xsmall-only) { - position: absolute; - bottom: $sm-padding-x; - right: $sm-padding-x; + justify-content: flex-end; } } diff --git a/bigbluebutton-html5/imports/ui/components/app/styles.scss b/bigbluebutton-html5/imports/ui/components/app/styles.scss index 1b9cd56587e184540b519853916113a6fc223879..fb64e90c09bc70b49d4221169a278f9c19a6b035 100644 --- a/bigbluebutton-html5/imports/ui/components/app/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/app/styles.scss @@ -156,11 +156,6 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al flex-direction: row; position: relative; - @include mq($small-only) { - padding-bottom: $actionsbar-height; - margin-bottom: $actionsbar-height; - } - @include mq($portrait) { flex-direction: column; } @@ -187,10 +182,4 @@ $bars-padding: $lg-padding-x - .45rem; // -.45 so user-list and chat title is al padding: $bars-padding; position: relative; order: 3; - - @include mq($small-only) { - position: absolute; - bottom: 0; - width: 100%; - } } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index a712cb599670a68bbb6eccb1e2f291a3befee987..6fb59a58467d40ec0b7c5a049245612e955a2577 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import _ from 'lodash'; import WhiteboardOverlayContainer from '/imports/ui/components/whiteboard/whiteboard-overlay/container'; import WhiteboardToolbarContainer from '/imports/ui/components/whiteboard/whiteboard-toolbar/container'; import PollingContainer from '/imports/ui/components/polling/container'; @@ -20,22 +19,25 @@ export default class PresentationArea extends Component { this.state = { presentationWidth: 0, presentationHeight: 0, + showSlide: false, }; this.getSvgRef = this.getSvgRef.bind(this); - this.ticking = false; - this.svggroup = false; - this.viewBox = false; - this.handleResize = _.throttle(this.handleResize.bind(this), 66); } componentDidMount() { - this.handleResize(); - window.addEventListener('resize', this.handleResize, false); + // adding an event listener to scale the whiteboard on 'resize' events sent by chat/userlist etc + window.addEventListener('resize', () => { + setTimeout(this.handleResize.bind(this), 0); + }); + + this.getInitialPresentationSizes(); } componentWillUnmount() { - window.removeEventListener('resize', this.handleResize, false); + window.removeEventListener('resize', () => { + setTimeout(this.handleResize.bind(this), 0); + }); } // returns a ref to the svg element, which is required by a WhiteboardOverlay @@ -44,24 +46,83 @@ export default class PresentationArea extends Component { return this.svggroup; } - handleResize() { - if (!this.viewBox) { - return; - } + getPresentationSizesAvailable() { + const { refPresentationArea, refWhiteboardArea } = this; + const presentationSizes = {}; - if (!this.ticking) { - window.requestAnimationFrame(() => { - this.ticking = false; + if (refPresentationArea && refWhiteboardArea) { + // By default presentation sizes are equal to the sizes of the refPresentationArea + // direct parent of the svg wrapper + let { clientWidth, clientHeight } = refPresentationArea; + + // if a user is a presenter - this means there is a whiteboard toolbar on the right + // and we have to get the width/height of the refWhiteboardArea + // (inner hidden div with absolute position) + if (this.props.userIsPresenter || this.props.multiUser) { + ({ clientWidth, clientHeight } = refWhiteboardArea); + } + + presentationSizes.presentationHeight = clientHeight; + presentationSizes.presentationWidth = clientWidth; + } + return presentationSizes; + } - const viewBoxRect = this.viewBox.getBoundingClientRect(); - this.setState({ - presentationWidth: viewBoxRect.width, - presentationHeight: viewBoxRect.height, - }); + getInitialPresentationSizes() { + // determining the presentationWidth and presentationHeight (available space for the svg) + // on the initial load + const presentationSizes = this.getPresentationSizesAvailable(); + if (Object.keys(presentationSizes).length > 0) { + // setting the state of the available space for the svg + // and set the showSlide to true to start rendering the slide + this.setState({ + presentationHeight: presentationSizes.presentationHeight, + presentationWidth: presentationSizes.presentationWidth, + showSlide: true, }); } + } - this.ticking = true; + handleResize() { + const presentationSizes = this.getPresentationSizesAvailable(); + if (Object.keys(presentationSizes).length > 0) { + // updating the size of the space available for the slide + this.setState(presentationSizes); + } + } + + calculateSize() { + const originalWidth = this.props.currentSlide.calculatedData.width; + const originalHeight = this.props.currentSlide.calculatedData.height; + const { presentationHeight, presentationWidth } = this.state; + + let adjustedWidth; + let adjustedHeight; + + // Slide has a portrait orientation + if (originalWidth <= originalHeight) { + adjustedWidth = (presentationHeight * originalWidth) / originalHeight; + if (presentationWidth < adjustedWidth) { + adjustedHeight = (presentationHeight * presentationWidth) / adjustedWidth; + adjustedWidth = presentationWidth; + } else { + adjustedHeight = presentationHeight; + } + + // Slide has a landscape orientation + } else { + adjustedHeight = (presentationWidth * originalHeight) / originalWidth; + if (presentationHeight < adjustedHeight) { + adjustedWidth = (presentationWidth * presentationHeight) / adjustedHeight; + adjustedHeight = presentationHeight; + } else { + adjustedWidth = presentationWidth; + } + } + return { + width: adjustedWidth, + height: adjustedHeight, + }; } // renders the whole presentation area @@ -71,6 +132,9 @@ export default class PresentationArea extends Component { !this.props.currentSlide.calculatedData) { return null; } + // to control the size of the svg wrapper manually + // and adjust cursor's thickness, so that svg didn't scale it automatically + const adjustedSizes = this.calculateSize(); // a reference to the slide object const slideObj = this.props.currentSlide; @@ -87,61 +151,71 @@ export default class PresentationArea extends Component { } = slideObj.calculatedData; return ( - <TransitionGroup className={styles.slideArea} id="presentationAreaData"> - <CSSTransition - key={slideObj.id} - classNames={{ - enter: styles.enter, - enterActive: styles.enterActive, - appear: styles.appear, - appearActive: styles.appearActive, - }} - appear - enter - exit={false} - timeout={{ enter: 400 }} - > - <svg - preserveAspectRatio="xMidYMid meet" - ref={(ref) => { if (ref) this.svggroup = ref; }} - viewBox={`${x} ${y} ${viewBoxWidth} ${viewBoxHeight}`} - version="1.1" - xmlns="http://www.w3.org/2000/svg" - className={styles.svgStyles} + <div + style={{ + width: adjustedSizes.width, + height: adjustedSizes.height, + WebkitTransition: 'width 0.2s', /* Safari */ + transition: 'width 0.2s', + }} + id="presentationAreaData" + > + <TransitionGroup> + <CSSTransition + key={slideObj.id} + classNames={{ + enter: styles.enter, + enterActive: styles.enterActive, + appear: styles.appear, + appearActive: styles.appearActive, + }} + appear + enter + exit={false} + timeout={{ enter: 400 }} > - <defs> - <clipPath id="viewBox" ref={(ref) => { if (ref && this.viewBox !== ref) this.viewBox = ref; }}> - <rect x={x} y={y} width="100%" height="100%" fill="none" /> - </clipPath> - </defs> - <g clipPath="url(#viewBox)"> - <Slide - id="slideComponent" - imageUri={imageUri} - svgWidth={width} - svgHeight={height} - onLoad={this.handleResize} - /> - <AnnotationGroupContainer - width={width} - height={height} - whiteboardId={slideObj.id} - /> - <CursorWrapperContainer - widthRatio={slideObj.widthRatio} - physicalWidthRatio={this.state.presentationWidth / width} - slideWidth={width} - slideHeight={height} - /> - </g> - {this.renderOverlays(slideObj)} - </svg> - </CSSTransition> - </TransitionGroup> + <svg + width={width} + height={height} + ref={(ref) => { if (ref != null) { this.svggroup = ref; } }} + viewBox={`${x} ${y} ${viewBoxWidth} ${viewBoxHeight}`} + version="1.1" + xmlns="http://www.w3.org/2000/svg" + className={styles.svgStyles} + > + <defs> + <clipPath id="viewBox"> + <rect x={x} y={y} width="100%" height="100%" fill="none" /> + </clipPath> + </defs> + <g clipPath="url(#viewBox)"> + <Slide + id="slideComponent" + imageUri={imageUri} + svgWidth={width} + svgHeight={height} + /> + <AnnotationGroupContainer + width={width} + height={height} + whiteboardId={slideObj.id} + /> + <CursorWrapperContainer + widthRatio={slideObj.widthRatio} + physicalWidthRatio={adjustedSizes.width / width} + slideWidth={width} + slideHeight={height} + /> + </g> + {this.renderOverlays(slideObj, adjustedSizes)} + </svg> + </CSSTransition> + </TransitionGroup> + </div> ); } - renderOverlays(slideObj) { + renderOverlays(slideObj, adjustedSizes) { if (!this.props.userIsPresenter && !this.props.multiUser) { return null; } @@ -171,8 +245,8 @@ export default class PresentationArea extends Component { viewBoxY={y} viewBoxWidth={viewBoxWidth} viewBoxHeight={viewBoxHeight} - physicalSlideWidth={(this.state.presentationWidth / slideObj.widthRatio) * 100} - physicalSlideHeight={(this.state.presentationHeight / slideObj.heightRatio) * 100} + physicalSlideWidth={(adjustedSizes.width / slideObj.widthRatio) * 100} + physicalSlideHeight={(adjustedSizes.height / slideObj.heightRatio) * 100} /> </PresentationOverlayContainer> ); @@ -193,25 +267,39 @@ export default class PresentationArea extends Component { renderWhiteboardToolbar() { if (!this.props.currentSlide || - !this.props.currentSlide.calculatedData || - !(this.props.userIsPresenter || this.props.multiUser)) { + !this.props.currentSlide.calculatedData) { return null; } + const adjustedSizes = this.calculateSize(); return ( - <WhiteboardToolbarContainer whiteboardId={this.props.currentSlide.id} /> + <WhiteboardToolbarContainer + whiteboardId={this.props.currentSlide.id} + height={adjustedSizes.height} + /> ); } render() { return ( <div className={styles.presentationContainer} id="presentationContainer"> - <div className={styles.presentationArea}> - {this.renderWhiteboardToolbar()} - {this.renderPresentationArea()} - {this.renderPresentationToolbar()} + <div + ref={(ref) => { this.refPresentationArea = ref; }} + className={styles.presentationArea} + > + <div + ref={(ref) => { this.refWhiteboardArea = ref; }} + className={styles.whiteboardSizeAvailable} + /> + {this.state.showSlide ? + this.renderPresentationArea() + : null } + {this.props.userIsPresenter || this.props.multiUser ? + this.renderWhiteboardToolbar() + : null } </div> <PollingContainer /> + {this.renderPresentationToolbar()} </div> ); } diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss index 48b8c01e05b03ea58cb8a597507d43fc969e03ed..668de4d91f34da4c1025ded38bb9c662ee0d5b3f 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/styles.scss @@ -16,6 +16,8 @@ $controls-background: $color-white !default; position: absolute; bottom: .8rem; box-shadow: 0 0 10px -2px rgba(0, 0, 0, .25); + align-self: center; + justify-content: center; @include mq("#{$landscape} and (max-height:#{upper-bound($small-range)}), #{$small-only}") { transform: scale(.75); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss index 988d3456f378dece0a7e9be0b4b61c03878beb36..30477bafefa3c18b8e433a6c5e0612b955b1d37e 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/presentation/styles.scss @@ -19,51 +19,45 @@ transition: opacity 400ms ease-in; } -.slideArea { - flex: 1; -} - .presentationArea { order: 1; display: flex; - flex-direction: column; + flex-direction: row; align-items: center; justify-content: center; + height: 100%; + width:100%; overflow: hidden; position: relative; - flex: 1; -} - -.slideArea { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - flex: 1; - width: 100%; } .whiteboardSizeAvailable { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + height: 100%; + width: 100%; z-index: -1; } .svgStyles { + object-fit: contain; + width: 100%; + height: 100%; + max-width: 100%; max-height: 100%; + //always show an arrow by default cursor: default; + //double click on the whiteboard shouldn't change the cursor + -moz-user-select: -moz-none; + -webkit-user-select: none; + -ms-user-select: none; user-select: none; } .presentationContainer { display: flex; flex-direction: column; - width: 100%; height: 100%; - flex: 1; + width: 100%; }