Added support for setting separate window in fullscreen

master
Håvar Aambø Fosstveit 2018-11-15 13:13:39 +01:00
parent 8035fd39bc
commit ca54a49dd3
7 changed files with 430 additions and 53 deletions

View File

@ -0,0 +1,107 @@
const key = {
fullscreenEnabled : 0,
fullscreenElement : 1,
requestFullscreen : 2,
exitFullscreen : 3,
fullscreenchange : 4,
fullscreenerror : 5
};
const webkit = [
'webkitFullscreenEnabled',
'webkitFullscreenElement',
'webkitRequestFullscreen',
'webkitExitFullscreen',
'webkitfullscreenchange',
'webkitfullscreenerror'
];
const moz = [
'mozFullScreenEnabled',
'mozFullScreenElement',
'mozRequestFullScreen',
'mozCancelFullScreen',
'mozfullscreenchange',
'mozfullscreenerror'
];
const ms = [
'msFullscreenEnabled',
'msFullscreenElement',
'msRequestFullscreen',
'msExitFullscreen',
'MSFullscreenChange',
'MSFullscreenError'
];
export default class FullScreen
{
constructor(document)
{
this.document = document;
this.vendor = (
('fullscreenEnabled' in this.document && Object.keys(key)) ||
(webkit[0] in this.document && webkit) ||
(moz[0] in this.document && moz) ||
(ms[0] in this.document && ms) ||
[]
);
}
requestFullscreen(element)
{
element[this.vendor[key.requestFullscreen]]();
}
requestFullscreenFunction(element)
{
element[this.vendor[key.requestFullscreen]];
}
addEventListener(type, handler)
{
this.document.addEventListener(this.vendor[key[type]], handler);
}
removeEventListener(type, handler)
{
this.document.removeEventListener(this.vendor[key[type]], handler);
}
get exitFullscreen()
{
return this.document[this.vendor[key.exitFullscreen]].bind(this.document);
}
get fullscreenEnabled()
{
return Boolean(this.document[this.vendor[key.fullscreenEnabled]]);
}
set fullscreenEnabled(val) {}
get fullscreenElement()
{
return this.document[this.vendor[key.fullscreenElement]];
}
set fullscreenElement(val) {}
get onfullscreenchange()
{
return this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()];
}
set onfullscreenchange(handler)
{
this.document[`on${this.vendor[key.fullscreenchange]}`.toLowerCase()] = handler;
}
get onfullscreenerror()
{
return this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()];
}
set onfullscreenerror(handler)
{
this.document[`on${this.vendor[key.fullscreenerror]}`.toLowerCase()] = handler;
}
}

View File

@ -40,7 +40,7 @@ const FullScreenView = (props) =>
<div className='controls'> <div className='controls'>
<div <div
className={classnames('button', 'fullscreen', 'room-controls', { className={classnames('button', 'exitfullscreen', 'room-controls', {
visible : toolbarsVisible visible : toolbarsVisible
})} })}
onClick={(e) => onClick={(e) =>

View File

@ -4,46 +4,52 @@ import { connect } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import fscreen from 'fscreen'; import FullScreen from './FullScreen';
class Sidebar extends Component class Sidebar extends Component
{ {
state = { constructor(props)
fullscreen : false {
}; super(props);
this.fullscreen = new FullScreen(document);
this.state = {
fullscreen : false
};
}
handleToggleFullscreen = () => handleToggleFullscreen = () =>
{ {
if (fscreen.fullscreenElement) if (this.fullscreen.fullscreenElement)
{ {
fscreen.exitFullscreen(); this.fullscreen.exitFullscreen();
} }
else else
{ {
fscreen.requestFullscreen(document.documentElement); this.fullscreen.requestFullscreen(document.documentElement);
} }
}; };
handleFullscreenChange = () => handleFullscreenChange = () =>
{ {
this.setState({ this.setState({
fullscreen : fscreen.fullscreenElement !== null fullscreen : this.fullscreen.fullscreenElement !== null
}); });
}; };
componentDidMount() componentDidMount()
{ {
if (fscreen.fullscreenEnabled) if (this.fullscreen.fullscreenEnabled)
{ {
fscreen.addEventListener('fullscreenchange', this.handleFullscreenChange); this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
} }
} }
componentWillUnmount() componentWillUnmount()
{ {
if (fscreen.fullscreenEnabled) if (this.fullscreen.fullscreenEnabled)
{ {
fscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange); this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
} }
} }
@ -85,7 +91,7 @@ class Sidebar extends Component
})} })}
data-component='Sidebar' data-component='Sidebar'
> >
{fscreen.fullscreenEnabled && ( {this.fullscreen.fullscreenEnabled && (
<div <div
className={classnames('button', 'fullscreen', { className={classnames('button', 'fullscreen', {
on : this.state.fullscreen on : this.state.fullscreen

View File

@ -0,0 +1,286 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import FullScreen from '../FullScreen';
import classnames from 'classnames';
class NewWindow extends React.PureComponent
{
static defaultProps =
{
url : '',
name : '',
title : '',
features : { width: '800px', height: '600px' },
onBlock : null,
onUnload : null,
center : 'parent',
copyStyles : true
};
handleToggleFullscreen = () =>
{
if (this.fullscreen.fullscreenElement)
{
this.fullscreen.exitFullscreen();
}
else
{
this.fullscreen.requestFullscreen(this.window.document.documentElement);
}
};
handleFullscreenChange = () =>
{
this.setState({
fullscreen : this.fullscreen.fullscreenElement !== null
});
};
constructor(props)
{
super(props);
this.container = document.createElement('div');
this.window = null;
this.windowCheckerInterval = null;
this.released = false;
this.fullscreen = null;
this.state = {
mounted : false,
fullscreen : false
};
}
render()
{
if (!this.state.mounted)
return null;
return ReactDOM.createPortal([
<div key='newwindow' data-component='FullScreenView'>
<div className='controls'>
{this.fullscreen.fullscreenEnabled && (
<div
className={classnames('button', {
fullscreen : !this.state.fullscreen,
exitFullscreen : this.state.fullscreen
})}
onClick={this.handleToggleFullscreen}
data-tip='Fullscreen'
data-place='right'
data-type='dark'
/>
)}
</div>
{this.props.children}
</div>
], this.container);
}
componentDidMount()
{
this.openChild();
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({ mounted: true });
this.fullscreen = new FullScreen(this.window.document);
if (this.fullscreen.fullscreenEnabled)
{
this.fullscreen.addEventListener('fullscreenchange', this.handleFullscreenChange);
}
}
openChild()
{
const {
url,
title,
name,
features,
onBlock,
center
} = this.props;
if (center === 'parent')
{
features.left =
(window.top.outerWidth / 2) + window.top.screenX - (features.width / 2);
features.top =
(window.top.outerHeight / 2) + window.top.screenY - (features.height / 2);
}
else if (center === 'screen')
{
const screenLeft =
window.screenLeft !== undefined ? window.screenLeft : screen.left;
const screenTop =
window.screenTop !== undefined ? window.screenTop : screen.top;
const width = window.innerWidth
? window.innerWidth
: document.documentElement.clientWidth
? document.documentElement.clientWidth
: screen.width;
const height = window.innerHeight
? window.innerHeight
: document.documentElement.clientHeight
? document.documentElement.clientHeight
: screen.height;
features.left = (width / 2) - (features.width / 2) + screenLeft;
features.top = (height / 2) - (features.height / 2) + screenTop;
}
this.window = window.open(url, name, toWindowFeatures(features));
this.windowCheckerInterval = setInterval(() =>
{
if (!this.window || this.window.closed)
{
this.release();
}
}, 50);
if (this.window)
{
this.window.document.title = title;
this.window.document.body.appendChild(this.container);
if (this.props.copyStyles)
{
setTimeout(() => copyStyles(document, this.window.document), 0);
}
this.window.addEventListener('beforeunload', () => this.release());
}
else if (typeof onBlock === 'function')
{
onBlock(null);
}
}
componentWillUnmount()
{
if (this.window)
{
if (this.fullscreen.fullscreenEnabled)
{
this.fullscreen.removeEventListener('fullscreenchange', this.handleFullscreenChange);
}
this.window.close();
}
}
release()
{
if (this.released)
{
return;
}
this.released = true;
clearInterval(this.windowCheckerInterval);
const { onUnload } = this.props;
if (typeof onUnload === 'function')
{
onUnload(null);
}
}
}
NewWindow.propTypes = {
children : PropTypes.node,
url : PropTypes.string,
name : PropTypes.string,
title : PropTypes.string,
features : PropTypes.object,
onUnload : PropTypes.func,
onBlock : PropTypes.func,
center : PropTypes.oneOf([ 'parent', 'screen' ]),
copyStyles : PropTypes.bool
};
function copyStyles(source, target)
{
Array.from(source.styleSheets).forEach((styleSheet) =>
{
let rules;
try
{
rules = styleSheet.cssRules;
}
catch (err) {}
if (rules)
{
const newStyleEl = source.createElement('style');
Array.from(styleSheet.cssRules).forEach((cssRule) =>
{
const { cssText, type } = cssRule;
let returnText = cssText;
if ([ 3, 5 ].includes(type))
{
returnText = cssText
.split('url(')
.map((line) =>
{
if (line[1] === '/')
{
return `${line.slice(0, 1)}${
window.location.origin
}${line.slice(1)}`;
}
return line;
})
.join('url(');
}
newStyleEl.appendChild(source.createTextNode(returnText));
});
target.head.appendChild(newStyleEl);
}
else if (styleSheet.href)
{
const newLinkEl = source.createElement('link');
newLinkEl.rel = 'stylesheet';
newLinkEl.href = styleSheet.href;
target.head.appendChild(newLinkEl);
}
});
}
function toWindowFeatures(obj)
{
return Object.keys(obj)
.reduce((features, name) =>
{
const value = obj[name];
if (typeof value === 'boolean')
{
features.push(`${name}=${value ? 'yes' : 'no'}`);
}
else
{
features.push(`${name}=${value}`);
}
return features;
}, [])
.join(',');
}
export default NewWindow;

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import NewWindow from 'react-new-window'; import NewWindow from './NewWindow';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes'; import * as appPropTypes from '../appPropTypes';
import * as stateActions from '../../redux/stateActions'; import * as stateActions from '../../redux/stateActions';
import FullView from '../FullView'; import FullView from '../FullView';
@ -12,8 +11,7 @@ const VideoWindow = (props) =>
const { const {
advancedMode, advancedMode,
consumer, consumer,
toggleConsumerWindow, toggleConsumerWindow
toolbarsVisible
} = props; } = props;
if (!consumer) if (!consumer)
@ -32,34 +30,12 @@ const VideoWindow = (props) =>
return ( return (
<NewWindow onUnload={toggleConsumerWindow}> <NewWindow onUnload={toggleConsumerWindow}>
<div data-component='FullScreenView'> <FullView
{consumerVisible && !consumer.supported ? advancedMode={advancedMode}
<div className='incompatible-video'> videoTrack={consumer ? consumer.track : null}
<p>incompatible video</p> videoVisible={consumerVisible}
</div> videoProfile={consumerProfile}
:null />
}
<div className='controls'>
<div
className={classnames('button', 'fullscreen', 'room-controls', {
visible : toolbarsVisible
})}
onClick={(e) =>
{
e.stopPropagation();
toggleConsumerWindow();
}}
/>
</div>
<FullView
advancedMode={advancedMode}
videoTrack={consumer ? consumer.track : null}
videoVisible={consumerVisible}
videoProfile={consumerProfile}
/>
</div>
</NewWindow> </NewWindow>
); );
}; };
@ -68,15 +44,13 @@ VideoWindow.propTypes =
{ {
advancedMode : PropTypes.bool, advancedMode : PropTypes.bool,
consumer : appPropTypes.Consumer, consumer : appPropTypes.Consumer,
toggleConsumerWindow : PropTypes.func.isRequired, toggleConsumerWindow : PropTypes.func.isRequired
toolbarsVisible : PropTypes.bool
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
{ {
return { return {
consumer : state.consumers[state.room.windowConsumer], consumer : state.consumers[state.room.windowConsumer]
toolbarsVisible : state.room.toolbarsVisible
}; };
}; };
@ -85,7 +59,7 @@ const mapDispatchToProps = (dispatch) =>
return { return {
toggleConsumerWindow : () => toggleConsumerWindow : () =>
{ {
dispatch(stateActions.toggleConsumerWindow(null)); dispatch(stateActions.toggleConsumerWindow());
} }
}; };
}; };

View File

@ -14,7 +14,6 @@
"domready": "^1.0.8", "domready": "^1.0.8",
"drag-drop": "^4.2.0", "drag-drop": "^4.2.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"fscreen": "^1.0.2",
"hark": "^1.2.2", "hark": "^1.2.2",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"magnet-uri": "^5.2.3", "magnet-uri": "^5.2.3",

View File

@ -44,10 +44,15 @@
height: 5vmin; height: 5vmin;
} }
&.fullscreen { &.exitfullscreen {
background-image: url('/resources/images/icon_fullscreen_exit_black.svg'); background-image: url('/resources/images/icon_fullscreen_exit_black.svg');
background-color: rgba(#fff, 0.7); background-color: rgba(#fff, 0.7);
} }
&.fullscreen {
background-image: url('/resources/images/icon_fullscreen_black.svg');
background-color: rgba(#fff, 0.7);
}
} }
} }