mirror of https://github.com/jitsi/jitsi-meet
feat(jitsipopover): convert to InlineDialog (#1804)
* feat(small-video): use InlineDialog for stats and remote menu - Remove JitsiPopover and use InlineDialog instead. - Bring the remote menu icon into react. - Make vertical filmstrip position:fixed so popper (AtlasKit dependency) sets InlineDialogs and eventually tooltips to position:fixed. * ref(remote-menu): hook KickButton to redux * ref(remote-menu): hook MuteButton to redux * modify padding, toggle dialogs * pixel push margins to align dialogs, adjust padding of dialogs * add comment about margin for dialog, add file I forgot * modify indicator markup so the icon can be moved down while trigger stays at top of toolbarpull/1888/head jitsi-meet_2354
parent
cd910e3074
commit
725d39ddcd
@ -1,92 +0,0 @@ |
|||||||
.jitsipopover { |
|
||||||
position: absolute; |
|
||||||
top: 0; |
|
||||||
left: 0; |
|
||||||
z-index: $jitsipopoverZ; |
|
||||||
display: table; |
|
||||||
visibility: hidden; |
|
||||||
max-width: 300px; |
|
||||||
min-width: 100px; |
|
||||||
text-align: left; |
|
||||||
color: $popoverFontColor; |
|
||||||
background-color: $popoverBg; |
|
||||||
background-clip: padding-box; |
|
||||||
border-radius: $borderRadius; |
|
||||||
/*-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);*/ |
|
||||||
/*box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);*/ |
|
||||||
white-space: normal; |
|
||||||
margin-top: -$popoverMenuPadding; |
|
||||||
|
|
||||||
|
|
||||||
&__menu-padding, |
|
||||||
&__menu-padding-top { |
|
||||||
position: absolute; |
|
||||||
width: 100px; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Invisible padding is added to the bottom of the popover to extend its |
|
||||||
* height so it does not close when moving the mouse from the trigger |
|
||||||
* element towards the popover itself. |
|
||||||
*/ |
|
||||||
&__menu-padding { |
|
||||||
bottom: -$popoverMenuPadding; |
|
||||||
height: $popoverMenuPadding; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Invisible padding is added to the top of the popover to extend its height |
|
||||||
* so it does not close automatically when its height is shrunk from showing |
|
||||||
* less video statistics. |
|
||||||
*/ |
|
||||||
&__menu-padding-top { |
|
||||||
height: 20px; |
|
||||||
top: -20px; |
|
||||||
} |
|
||||||
|
|
||||||
&__showmore { |
|
||||||
display: block; |
|
||||||
text-align: center; |
|
||||||
width: 90px; |
|
||||||
margin: 10px auto; |
|
||||||
} |
|
||||||
|
|
||||||
> .arrow { |
|
||||||
position: absolute; |
|
||||||
display: block; |
|
||||||
left: 50%; |
|
||||||
bottom: -5px; |
|
||||||
margin-left: -5px; |
|
||||||
width: 0; |
|
||||||
height: 0; |
|
||||||
border-color: transparent; |
|
||||||
border-top-color: $popoverBg; |
|
||||||
border-style: solid; |
|
||||||
border-width: 5px; |
|
||||||
border-bottom-width: 0; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Override default "top" styles to support popovers appearing from the |
|
||||||
* left of the popover trigger element. |
|
||||||
*/ |
|
||||||
&.left { |
|
||||||
margin-left: -$popoverMenuPadding; |
|
||||||
margin-top: 0; |
|
||||||
|
|
||||||
.arrow { |
|
||||||
border-color: transparent transparent transparent $popoverBg; |
|
||||||
border-width: 5px 0px 5px 5px; |
|
||||||
margin-left: 0; |
|
||||||
margin-top: -5px; |
|
||||||
} |
|
||||||
|
|
||||||
.jitsipopover { |
|
||||||
&__menu-padding { |
|
||||||
bottom: 0; |
|
||||||
height: 100%; |
|
||||||
width: $popoverMenuPadding; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,276 +0,0 @@ |
|||||||
/* global $ */ |
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */ |
|
||||||
import React, { Component } from 'react'; |
|
||||||
import ReactDOM from 'react-dom'; |
|
||||||
import { I18nextProvider } from 'react-i18next'; |
|
||||||
|
|
||||||
import { i18next } from '../../../react/features/base/i18n'; |
|
||||||
/* eslint-enable no-unused-vars */ |
|
||||||
|
|
||||||
const positionConfigurations = { |
|
||||||
left: { |
|
||||||
|
|
||||||
// Align the popover's right side to the target element.
|
|
||||||
my: 'right', |
|
||||||
|
|
||||||
// Align the popover to the left side of the target element.
|
|
||||||
at: 'left', |
|
||||||
|
|
||||||
// Force the popover to fit within the viewport.
|
|
||||||
collision: 'fit', |
|
||||||
|
|
||||||
/** |
|
||||||
* Callback invoked by jQuery UI tooltip. |
|
||||||
* |
|
||||||
* @param {Object} position - The top and bottom position the popover |
|
||||||
* element should be set at. |
|
||||||
* @param {Object} element. - Additional size and position information |
|
||||||
* about the popover element and target. |
|
||||||
* @param {Object} elements.element - Has position and size related data |
|
||||||
* for the popover element itself. |
|
||||||
* @param {Object} elements.target - Has position and size related data |
|
||||||
* for the target element the popover displays from. |
|
||||||
*/ |
|
||||||
using: function setPositionLeft(position, elements) { |
|
||||||
const { element, target } = elements; |
|
||||||
|
|
||||||
$('.jitsipopover').css({ |
|
||||||
left: element.left, |
|
||||||
top: element.top, |
|
||||||
visibility: 'visible' |
|
||||||
}); |
|
||||||
|
|
||||||
// Move additional padding to the right edge of the popover and
|
|
||||||
// allow css to take care of width. The padding is used to maintain
|
|
||||||
// a hover state between the target and the popover.
|
|
||||||
$('.jitsipopover > .jitsipopover__menu-padding') |
|
||||||
.css({ left: element.width }); |
|
||||||
|
|
||||||
// Find the distance from the top of the popover to the center of
|
|
||||||
// the target and use that value to position the arrow to point to
|
|
||||||
// it.
|
|
||||||
const verticalCenterOfTarget = target.height / 2; |
|
||||||
const verticalDistanceFromTops = target.top - element.top; |
|
||||||
const verticalPositionOfTargetCenter |
|
||||||
= verticalDistanceFromTops + verticalCenterOfTarget; |
|
||||||
|
|
||||||
$('.jitsipopover > .arrow').css({ |
|
||||||
left: element.width, |
|
||||||
top: verticalPositionOfTargetCenter |
|
||||||
}); |
|
||||||
} |
|
||||||
}, |
|
||||||
top: { |
|
||||||
my: "bottom", |
|
||||||
at: "top", |
|
||||||
collision: "fit", |
|
||||||
using: function setPositionTop(position, elements) { |
|
||||||
const { element, target } = elements; |
|
||||||
const calcLeft = target.left - element.left + target.width / 2; |
|
||||||
const paddingLeftPosition = calcLeft - 50; |
|
||||||
const $jistiPopover = $('.jitsipopover'); |
|
||||||
|
|
||||||
$jistiPopover.css({ |
|
||||||
left: element.left, |
|
||||||
top: element.top, |
|
||||||
visibility: 'visible' |
|
||||||
}); |
|
||||||
$jistiPopover.find('.arrow').css({ left: calcLeft }); |
|
||||||
$jistiPopover.find('.jitsipopover__menu-padding') |
|
||||||
.css({ left: paddingLeftPosition }); |
|
||||||
$jistiPopover.find('.jitsipopover__menu-padding-top') |
|
||||||
.css({ left: paddingLeftPosition }); |
|
||||||
} |
|
||||||
} |
|
||||||
}; |
|
||||||
export default (function () { |
|
||||||
/** |
|
||||||
* The default options |
|
||||||
*/ |
|
||||||
const defaultOptions = { |
|
||||||
skin: 'white', |
|
||||||
content: '', |
|
||||||
hasArrow: true, |
|
||||||
onBeforePosition: undefined, |
|
||||||
position: 'top' |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Constructs new JitsiPopover and attaches it to the element |
|
||||||
* @param element jquery selector |
|
||||||
* @param options the options for the popover. |
|
||||||
* @constructor |
|
||||||
*/ |
|
||||||
function JitsiPopover(element, options) |
|
||||||
{ |
|
||||||
this.options = Object.assign({}, defaultOptions, options); |
|
||||||
this.elementIsHovered = false; |
|
||||||
this.popoverIsHovered = false; |
|
||||||
this.popoverShown = false; |
|
||||||
|
|
||||||
element.data("jitsi_popover", this); |
|
||||||
this.element = element; |
|
||||||
this.template = this.getTemplate(); |
|
||||||
var self = this; |
|
||||||
this.element.on("mouseenter", function () { |
|
||||||
self.elementIsHovered = true; |
|
||||||
self.show(); |
|
||||||
}).on("mouseleave", function () { |
|
||||||
self.elementIsHovered = false; |
|
||||||
setTimeout(function () { |
|
||||||
self.hide(); |
|
||||||
}, 10); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns template for popover |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.getTemplate = function () { |
|
||||||
const { hasArrow, position, skin } = this.options; |
|
||||||
|
|
||||||
let arrow = ''; |
|
||||||
if (hasArrow) { |
|
||||||
arrow = '<div class="arrow"></div>'; |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
`<div class="jitsipopover ${skin} ${position}">
|
|
||||||
<div class="jitsipopover__menu-padding-top"></div> |
|
||||||
${arrow} |
|
||||||
<div class="jitsipopover__content"></div> |
|
||||||
<div class="jitsipopover__menu-padding"></div> |
|
||||||
</div>` |
|
||||||
); |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Shows the popover |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.show = function () { |
|
||||||
if(!JitsiPopover.enabled) |
|
||||||
return; |
|
||||||
this.createPopover(); |
|
||||||
this.popoverShown = true; |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Hides the popover if not hovered or popover is not shown. |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.hide = function () { |
|
||||||
if(!this.elementIsHovered && !this.popoverIsHovered && |
|
||||||
this.popoverShown) { |
|
||||||
this.forceHide(); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Hides the popover and clears the document elements added by popover. |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.forceHide = function () { |
|
||||||
this.remove(); |
|
||||||
this.popoverShown = false; |
|
||||||
if(this.popoverIsHovered) { //the browser is not firing hover events
|
|
||||||
//when the element was on hover if got removed.
|
|
||||||
this.popoverIsHovered = false; |
|
||||||
this.onHoverPopover(this.popoverIsHovered); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates the popover html. |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.createPopover = function () { |
|
||||||
let $popover = $('.jitsipopover'); |
|
||||||
|
|
||||||
if (!$popover.length) { |
|
||||||
$('body').append(this.template); |
|
||||||
|
|
||||||
$popover = $('.jitsipopover'); |
|
||||||
|
|
||||||
$popover.on('mouseenter', () => { |
|
||||||
this.popoverIsHovered = true; |
|
||||||
if (typeof this.onHoverPopover === 'function') { |
|
||||||
this.onHoverPopover(this.popoverIsHovered); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
$popover.on('mouseleave', () => { |
|
||||||
this.popoverIsHovered = false; |
|
||||||
this.hide(); |
|
||||||
if (typeof this.onHoverPopover === 'function') { |
|
||||||
this.onHoverPopover(this.popoverIsHovered); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
const $popoverContent = $popover.find('.jitsipopover__content'); |
|
||||||
|
|
||||||
/* jshint ignore:start */ |
|
||||||
ReactDOM.render( |
|
||||||
<I18nextProvider i18n = { i18next }> |
|
||||||
{ this.options.content } |
|
||||||
</I18nextProvider>, |
|
||||||
$popoverContent.get(0), |
|
||||||
() => { |
|
||||||
this.refreshPosition(); |
|
||||||
}); |
|
||||||
/* jshint ignore:end */ |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Adds a hover listener to the popover. |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.addOnHoverPopover = function (listener) { |
|
||||||
this.onHoverPopover = listener; |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Refreshes the position of the popover. |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.refreshPosition = function () { |
|
||||||
const positionOptions = Object.assign( |
|
||||||
{}, |
|
||||||
positionConfigurations[this.options.position], |
|
||||||
{ |
|
||||||
of: this.element |
|
||||||
} |
|
||||||
); |
|
||||||
$(".jitsipopover").position(positionOptions); |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Updates the content of popover. |
|
||||||
* @param content new content |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.updateContent = function (content) { |
|
||||||
this.options.content = content; |
|
||||||
if (!this.popoverShown) { |
|
||||||
return; |
|
||||||
} |
|
||||||
this.createPopover(); |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* Unmounts any present child React Component and removes the popover itself |
|
||||||
* from the DOM. |
|
||||||
* |
|
||||||
* @returns {void} |
|
||||||
*/ |
|
||||||
JitsiPopover.prototype.remove = function () { |
|
||||||
const $popover = $('.jitsipopover'); |
|
||||||
const $popoverContent = $popover.find('.jitsipopover__content'); |
|
||||||
|
|
||||||
if ($popoverContent.length) { |
|
||||||
ReactDOM.unmountComponentAtNode($popoverContent.get(0)); |
|
||||||
} |
|
||||||
|
|
||||||
$popover.off(); |
|
||||||
$popover.remove(); |
|
||||||
}; |
|
||||||
|
|
||||||
JitsiPopover.enabled = true; |
|
||||||
|
|
||||||
return JitsiPopover; |
|
||||||
})(); |
|
||||||
@ -0,0 +1,194 @@ |
|||||||
|
import AKInlineDialog from '@atlaskit/inline-dialog'; |
||||||
|
import React, { Component } from 'react'; |
||||||
|
|
||||||
|
import { |
||||||
|
MuteButton, |
||||||
|
KickButton, |
||||||
|
RemoteControlButton, |
||||||
|
RemoteVideoMenu, |
||||||
|
VolumeSlider |
||||||
|
} from './'; |
||||||
|
|
||||||
|
declare var $: Object; |
||||||
|
declare var interfaceConfig: Object; |
||||||
|
|
||||||
|
/** |
||||||
|
* React {@code Component} for displaying an icon associated with opening the |
||||||
|
* the {@code RemoteVideoMenu}. |
||||||
|
* |
||||||
|
* @extends {Component} |
||||||
|
*/ |
||||||
|
class RemoteVideoMenuTriggerButton extends Component { |
||||||
|
static propTypes = { |
||||||
|
/** |
||||||
|
* A value between 0 and 1 indicating the volume of the participant's |
||||||
|
* audio element. |
||||||
|
*/ |
||||||
|
initialVolumeValue: React.PropTypes.number, |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether or not the participant is currently muted. |
||||||
|
*/ |
||||||
|
isAudioMuted: React.PropTypes.bool, |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether or not the participant is a conference moderator. |
||||||
|
*/ |
||||||
|
isModerator: React.PropTypes.bool, |
||||||
|
|
||||||
|
/** |
||||||
|
* Callback to invoke when the popover has been displayed. |
||||||
|
*/ |
||||||
|
onMenuDisplay: React.PropTypes.func, |
||||||
|
|
||||||
|
/** |
||||||
|
* Callback to invoke choosing to start a remote control session with |
||||||
|
* the participant. |
||||||
|
*/ |
||||||
|
onRemoteControlToggle: React.PropTypes.func, |
||||||
|
|
||||||
|
/** |
||||||
|
* Callback to invoke when changing the level of the participant's |
||||||
|
* audio element. |
||||||
|
*/ |
||||||
|
onVolumeChange: React.PropTypes.func, |
||||||
|
|
||||||
|
/** |
||||||
|
* The ID for the participant on which the remote video menu will act. |
||||||
|
*/ |
||||||
|
participantID: React.PropTypes.string, |
||||||
|
|
||||||
|
/** |
||||||
|
* The current state of the participant's remote control session. |
||||||
|
*/ |
||||||
|
remoteControlState: React.PropTypes.number |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Initializes a new {#@code RemoteVideoMenuTriggerButton} instance. |
||||||
|
* |
||||||
|
* @param {Object} props - The read-only properties with which the new |
||||||
|
* instance is to be initialized. |
||||||
|
*/ |
||||||
|
constructor(props) { |
||||||
|
super(props); |
||||||
|
|
||||||
|
this.state = { |
||||||
|
showRemoteMenu: false |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* The internal reference to topmost DOM/HTML element backing the React |
||||||
|
* {@code Component}. Accessed directly for associating an element as |
||||||
|
* the trigger for a popover. |
||||||
|
* |
||||||
|
* @private |
||||||
|
* @type {HTMLDivElement} |
||||||
|
*/ |
||||||
|
this._rootElement = null; |
||||||
|
|
||||||
|
// Bind event handlers so they are only bound once for every instance.
|
||||||
|
this._onRemoteMenuClose = this._onRemoteMenuClose.bind(this); |
||||||
|
this._onRemoteMenuToggle = this._onRemoteMenuToggle.bind(this); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements React's {@link Component#render()}. |
||||||
|
* |
||||||
|
* @inheritdoc |
||||||
|
* @returns {ReactElement} |
||||||
|
*/ |
||||||
|
render() { |
||||||
|
return ( |
||||||
|
<AKInlineDialog |
||||||
|
content = { this._renderRemoteVideoMenu() } |
||||||
|
isOpen = { this.state.showRemoteMenu } |
||||||
|
onClose = { this._onRemoteMenuClose } |
||||||
|
position = { interfaceConfig.VERTICAL_FILMSTRIP |
||||||
|
? 'left middle' : 'top center' } |
||||||
|
shouldFlip = { true }> |
||||||
|
<span |
||||||
|
className = 'popover-trigger remote-video-menu-trigger' |
||||||
|
onClick = { this._onRemoteMenuToggle }> |
||||||
|
<i |
||||||
|
className = 'icon-thumb-menu' |
||||||
|
title = 'Remote user controls' /> |
||||||
|
</span> |
||||||
|
</AKInlineDialog> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Closes the {@code RemoteVideoMenu}. |
||||||
|
* |
||||||
|
* @private |
||||||
|
* @returns {void} |
||||||
|
*/ |
||||||
|
_onRemoteMenuClose() { |
||||||
|
this.setState({ showRemoteMenu: false }); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Opens or closes the {@code RemoteVideoMenu}. |
||||||
|
* |
||||||
|
* @private |
||||||
|
* @returns {void} |
||||||
|
*/ |
||||||
|
_onRemoteMenuToggle() { |
||||||
|
const willShowRemoteMenu = !this.state.showRemoteMenu; |
||||||
|
|
||||||
|
if (willShowRemoteMenu) { |
||||||
|
this.props.onMenuDisplay(); |
||||||
|
} |
||||||
|
|
||||||
|
this.setState({ showRemoteMenu: willShowRemoteMenu }); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@code RemoteVideoMenu} with buttons for interacting with |
||||||
|
* the remote participant. |
||||||
|
* |
||||||
|
* @private |
||||||
|
* @returns {ReactElement} |
||||||
|
*/ |
||||||
|
_renderRemoteVideoMenu() { |
||||||
|
const { |
||||||
|
initialVolumeValue, |
||||||
|
isAudioMuted, |
||||||
|
isModerator, |
||||||
|
onRemoteControlToggle, |
||||||
|
onVolumeChange, |
||||||
|
remoteControlState, |
||||||
|
participantID |
||||||
|
} = this.props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<RemoteVideoMenu id = { participantID }> |
||||||
|
{ isModerator |
||||||
|
? <MuteButton |
||||||
|
isAudioMuted = { isAudioMuted } |
||||||
|
onClick = { this._onRemoteMenuClose } |
||||||
|
participantID = { participantID } /> |
||||||
|
: null } |
||||||
|
{ isModerator |
||||||
|
? <KickButton |
||||||
|
onClick = { this._onRemoteMenuClose } |
||||||
|
participantID = { participantID } /> |
||||||
|
: null } |
||||||
|
{ remoteControlState |
||||||
|
? <RemoteControlButton |
||||||
|
onClick = { onRemoteControlToggle } |
||||||
|
participantID = { participantID } |
||||||
|
remoteControlState = { remoteControlState } /> |
||||||
|
: null } |
||||||
|
{ onVolumeChange |
||||||
|
? <VolumeSlider |
||||||
|
initialValue = { initialVolumeValue } |
||||||
|
onChange = { onVolumeChange } /> |
||||||
|
: null } |
||||||
|
</RemoteVideoMenu> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default RemoteVideoMenuTriggerButton; |
||||||
Loading…
Reference in new issue