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