From 68d7f1be77ab2661273f17d67e148c4794e55f85 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:57:20 +0900 Subject: [PATCH 01/87] Update component.jsx --- .../imports/ui/components/actions-bar/component.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 3acf15e86137..eea05f3af191 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -154,6 +154,7 @@ class ActionsBar extends PureComponent { showScreenshareQuickSwapButton, isReactionsButtonEnabled, isRaiseHandEnabled, + isPresentationDetached, } = this.props; const Settings = getSettingsSingletonInstance(); @@ -250,6 +251,7 @@ class ActionsBar extends PureComponent { hasPinnedSharedNotes={isSharedNotesPinned} hasGenericContent={hasGenericContent} hasCameraAsContent={hasCameraAsContent} + isPresentationDetached={isPresentationDetached} /> ) : null} From 6a718fde8166e5e7834604ccc080d80de370ce85 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:58:56 +0900 Subject: [PATCH 02/87] Update component.jsx --- .../components/actions-bar/presentation-options/component.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx index ff05f294b778..ec92e13c81ec 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/presentation-options/component.jsx @@ -46,6 +46,7 @@ const PresentationOptionsContainer = ({ hasPinnedSharedNotes, hasGenericContent, hasCameraAsContent, + isPresentationDetached, }) => { let buttonType = 'presentation'; if (hasExternalVideo) { @@ -100,7 +101,7 @@ const PresentationOptionsContainer = ({ } }} id="restore-presentation" - disabled={!isThereCurrentPresentation} + disabled={!isThereCurrentPresentation || isPresentationDetached} data-test={!presentationIsOpen ? 'restorePresentation' : 'minimizePresentation'} /> ); From aabe8a33e7c9a52f3fc70c1bad068c7cfd9e483c Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:05:46 +0900 Subject: [PATCH 03/87] Update component.jsx --- .../imports/ui/components/app/component.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index acfb827be6d7..eab9e1302f44 100644 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -285,6 +285,7 @@ class App extends Component { const { hideActionsBar, presentationIsOpen, + isPresentationDetached, } = this.props; if (hideActionsBar) return null; @@ -293,6 +294,7 @@ class App extends Component { ); } @@ -337,6 +339,9 @@ class App extends Component { isNotificationEnabled, isNonMediaLayout, isRaiseHandEnabled, + popupWindow, + isPresentationDetached, + toggleDetachPresentation, } = this.props; const { @@ -388,6 +393,9 @@ class App extends Component { fitToWidth={presentationFitToWidth} darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} + popupWindow={popupWindow} + isPresentationDetached={isPresentationDetached} + toggleDetachPresentation={toggleDetachPresentation} /> ) : null From 380ce89ae1942cc47153f787346b64ae75185e23 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:09:59 +0900 Subject: [PATCH 04/87] Update container.jsx --- .../imports/ui/components/app/container.jsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index f538a3d2439a..0950abc58e97 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useMutation, useReactiveVar } from '@apollo/client'; import AudioCaptionsLiveContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/live/component'; import getFromUserSettings from '/imports/ui/services/users-settings'; @@ -106,9 +106,24 @@ const AppContainer = (props) => { const shouldShowScreenshare = (viewScreenshare || isPresenter) && (currentMeeting?.componentsFlags?.hasScreenshare || currentMeeting?.componentsFlags?.hasCameraAsContent) && showScreenshare; - const shouldShowPresentation = (!shouldShowScreenshare && !isSharedNotesPinned - && !shouldShowExternalVideo && !shouldShowGenericMainContent - && (presentationIsOpen || presentationRestoreOnUpdate)) && isPresentationEnabled; + + const [popupWindow, setPopupWindow] = useState(null); + const [isPresentationDetached, setIsPresentationDetached] = useState(false); + + const toggleDetachPresentation = (popup) => { + setPopupWindow(popup); + setIsPresentationDetached(Boolean(popup)); + }; + + const hasPresentationContent = + (presentationIsOpen || presentationRestoreOnUpdate) && isPresentationEnabled; + const noOtherMainContent = + !shouldShowScreenshare && !isSharedNotesPinned && + !shouldShowExternalVideo && !shouldShowGenericMainContent; + const shouldShowPresentation = isPresentationDetached + ? hasPresentationContent + : noOtherMainContent && hasPresentationContent; + const currentPageInfoData = currentPageInfo?.pres_page_curr[0] ?? {}; const fitToWidth = currentPageInfoData?.fitToWidth ?? false; const pageId = currentPageInfoData?.pageId ?? ''; @@ -161,6 +176,9 @@ const AppContainer = (props) => { isBreakout: currentMeeting?.isBreakout ?? false, meetingName: currentMeeting?.name ?? '', meetingId: currentMeeting?.meetingId ?? '', + isPresentationDetached, + popupWindow, + toggleDetachPresentation, }} {...props} /> From 4976e471b158f6410b2360a0b036e28b47148a8a Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:15:21 +0900 Subject: [PATCH 05/87] Update service.js --- .../common/fullscreen-button/service.js | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js b/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js index 550df065a10c..964139b8a894 100644 --- a/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js +++ b/bigbluebutton-html5/imports/ui/components/common/fullscreen-button/service.js @@ -1,25 +1,25 @@ -function getFullscreenElement() { - if (document.fullscreenElement) return document.fullscreenElement; - if (document.webkitFullscreenElement) return document.webkitFullscreenElement; - if (document.mozFullScreenElement) return document.mozFullScreenElement; - if (document.msFullscreenElement) return document.msFullscreenElement; +function getFullscreenElement(d = document) { + if (d.fullscreenElement) return d.fullscreenElement; + if (d.webkitFullscreenElement) return d.webkitFullscreenElement; + if (d.mozFullScreenElement) return d.mozFullScreenElement; + if (d.msFullscreenElement) return d.msFullscreenElement; return null; } -const isFullScreen = (element) => { - if (getFullscreenElement() && getFullscreenElement() === element) { +const isFullScreen = (element, doc = document) => { + if (getFullscreenElement(doc) && getFullscreenElement(doc) === element) { return true; } return false; }; -function cancelFullScreen() { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); +function cancelFullScreen(doc = document) { + if (doc.exitFullscreen) { + doc.exitFullscreen(); + } else if (doc.mozCancelFullScreen) { + doc.mozCancelFullScreen(); + } else if (doc.webkitExitFullscreen) { + doc.webkitExitFullscreen(); } } @@ -39,13 +39,20 @@ function fullscreenRequest(element) { element.focus(); } -const toggleFullScreen = (ref = null) => { - const element = ref || document.documentElement; - - if (isFullScreen(element)) { - cancelFullScreen(); +const toggleFullScreen = (ref = null, isDetached = false, p) => { + const element = isDetached ? p.document.documentElement : (ref || document.documentElement); + if (isDetached) { + if (isFullScreen(element, p.document)) { + cancelFullScreen(p.document); + } else { + fullscreenRequest(element); + } } else { - fullscreenRequest(element); + if (isFullScreen(element)) { + cancelFullScreen(); + } else { + fullscreenRequest(element); + } } }; From c85ff13826975f5a55d4eb9b07043097e2bfe448 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:18:24 +0900 Subject: [PATCH 06/87] Update customLayout.jsx --- .../layout/layout-manager/customLayout.jsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx index ebc29fc616de..f3feb5bdbc94 100644 --- a/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx +++ b/bigbluebutton-html5/imports/ui/components/layout/layout-manager/customLayout.jsx @@ -461,10 +461,13 @@ const CustomLayout = (props) => { } if ( - fullscreenElement === 'Presentation' || - fullscreenElement === 'Screenshare' || - fullscreenElement === 'ExternalVideo' || - fullscreenElement === 'GenericContent' + (fullscreenElement === 'Presentation' || + fullscreenElement === 'Screenshare' || + fullscreenElement === 'ExternalVideo' || + fullscreenElement === 'GenericContent') && + // this is indispensable for showing a normal-sized operatable external video + // when popup is fullscreen within the sub-monitor + document.getElementById('presentationInnerWrapper') ) { mediaBounds.width = windowWidth(); mediaBounds.height = windowHeight(); @@ -803,4 +806,4 @@ const CustomLayout = (props) => { return null; }; -export default CustomLayout; \ No newline at end of file +export default CustomLayout; From 119c46111ff882378c6b3396e5b530b3f98db2ff Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:05:49 +0900 Subject: [PATCH 07/87] Update component.jsx --- .../ui/components/presentation/component.jsx | 282 ++++++++++++++++-- 1 file changed, 264 insertions(+), 18 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index 1fbad7ddadc0..6e19a662f5a3 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx @@ -1,4 +1,5 @@ import React, { PureComponent } from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import WhiteboardContainer from '/imports/ui/components/whiteboard/container'; import { HUNDRED_PERCENT, MAX_PERCENT, MIN_PERCENT } from '/imports/utils/slideCalcUtils'; @@ -6,6 +7,7 @@ import { SPACE } from '/imports/utils/keyCodes'; import { defineMessages, injectIntl } from 'react-intl'; import Session from '/imports/ui/services/storage/in-memory'; import PresentationToolbarContainer from './presentation-toolbar/container'; +import PresentationMenuContainer from './presentation-menu/container'; import PresentationMenu from './presentation-menu/container'; import DownloadPresentationButton from './download-presentation-button/component'; import Styled from './styles'; @@ -21,6 +23,7 @@ import { throttle } from '/imports/utils/throttle'; import LocatedErrorBoundary from '/imports/ui/components/common/error-boundary/located-error-boundary/component'; import FallbackView from '/imports/ui/components/common/fallback-errors/fallback-view/component'; import TooltipContainer from '/imports/ui/components/common/tooltip/container'; +import { StyleSheetManager } from 'styled-components'; const intlMessages = defineMessages({ presentationLabel: { @@ -58,9 +61,9 @@ const FULLSCREEN_CHANGE_EVENT = isSafari ? 'webkitfullscreenchange' : 'fullscreenchange'; -const getToolbarHeight = () => { +const getToolbarHeight = (doc = document) => { let height = 0; - const toolbarEl = document.getElementById('presentationToolbarWrapper'); + const toolbarEl = doc.getElementById('presentationToolbarWrapper'); if (toolbarEl) { const { clientHeight } = toolbarEl; height = clientHeight; @@ -102,6 +105,8 @@ class Presentation extends PureComponent { this.setIsToolbarVisible = this.setIsToolbarVisible.bind(this); this.handlePanShortcut = this.handlePanShortcut.bind(this); this.renderPresentationMenu = this.renderPresentationMenu.bind(this); + this.renderPresentationContents = this.renderPresentationContents.bind(this); + this.detachPresentation = this.detachPresentation.bind(this); this.onResize = () => setTimeout(this.handleResize.bind(this), 0); this.setPresentationRef = this.setPresentationRef.bind(this); @@ -330,9 +335,19 @@ class Presentation extends PureComponent { componentWillUnmount() { Session.setItem('componentPresentationWillUnmount', true); - const { fullscreenContext, layoutContextDispatch } = this.props; + const { + fullscreenContext, + layoutContextDispatch, + isPresentationDetached, + popupWindow, + } = this.props; - window.removeEventListener('resize', this.onResize, false); + if (isPresentationDetached) { + popupWindow.removeEventListener('resize', this.onResize, false); + } else { + window.removeEventListener('resize', this.onResize, false); + } + if (this.refPresentationContainer) { this.refPresentationContainer.removeEventListener( FULLSCREEN_CHANGE_EVENT, @@ -359,6 +374,161 @@ class Presentation extends PureComponent { } } + detachPresentation() { + const { + slidePosition, + isPresentationDetached, + popupWindow, + toggleDetachPresentation, + } = this.props; + + if (!isPresentationDetached) { + const svgDimensions = this.calculateSize(slidePosition); + const toolbarHeight = getToolbarHeight(); + const popup = window.open('', '_blank', + `innerwidth=${svgDimensions.width},innerheight=${svgDimensions.height + toolbarHeight},resizable,scrollbars`); + if (!popup) return; + popup.document.title = 'BigBlueButton Portal Window'; + const container = popup.document.createElement('div'); + popup.document.body.appendChild(container); + + // headの中身をコピー + const headElements = document.head.cloneNode(true).childNodes; + headElements.forEach((node) => { + // script要素など重複実行したくないものを除外する + if (node.nodeName !== 'SCRIPT') { + popup.document.head.appendChild(node.cloneNode(true)); + } + }); + + // Add base URL (perhaps only necessary for Firefox to show tldraw icons + const base = popup.document.createElement('base'); + base.href = window.location.origin + '/'; + popup.document.head.appendChild(base); + + // 追加: document.styleSheets からすべての stylesheet を popup に複製 + Array.from(document.styleSheets).forEach((styleSheet) => { + try { + if (styleSheet.href) { + // 形式 + const link = popup.document.createElement('link'); + link.rel = 'stylesheet'; + link.href = styleSheet.href; + popup.document.head.appendChild(link); + } else if (styleSheet.cssRules) { + //