diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 589475619ebb..bce3049d0b59 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(); @@ -251,6 +252,7 @@ class ActionsBar extends PureComponent { hasPinnedSharedNotes={isSharedNotesPinned} hasGenericContent={hasGenericContent} hasCameraAsContent={hasCameraAsContent} + isPresentationDetached={isPresentationDetached} /> ) : null} 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'} /> ); diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index f66f24836f96..ff4a820df342 100644 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -284,6 +284,7 @@ class App extends Component { const { hideActionsBar, presentationIsOpen, + isPresentationDetached, } = this.props; if (hideActionsBar) return null; @@ -292,6 +293,7 @@ class App extends Component { ); } @@ -336,6 +338,9 @@ class App extends Component { isNotificationEnabled, isNonMediaLayout, isRaiseHandEnabled, + popupWindow, + isPresentationDetached, + toggleDetachPresentation, } = this.props; const { @@ -384,6 +389,9 @@ class App extends Component { fitToWidth={presentationFitToWidth} darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} + popupWindow={popupWindow} + isPresentationDetached={isPresentationDetached} + toggleDetachPresentation={toggleDetachPresentation} /> ) : null diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index bfe94e8e9465..c500aff0d151 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'; @@ -112,9 +112,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 ?? ''; @@ -168,6 +183,9 @@ const AppContainer = (props) => { isBreakout: currentMeeting?.isBreakout ?? false, meetingName: currentMeeting?.name ?? '', meetingId: currentMeeting?.meetingId ?? '', + isPresentationDetached, + popupWindow, + toggleDetachPresentation, }} {...props} /> diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx index 4eaa1572cba4..6dc2fbed1c2e 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/component.tsx @@ -40,6 +40,8 @@ import { ChatLoading } from '../component'; import Storage from '/imports/ui/services/storage/in-memory'; import browserInfo from '/imports/utils/browserInfo'; import deviceInfo from '/imports/utils/deviceInfo'; +import { originalHTMLElement } from '/imports/utils/HTMLElementBackup'; +import { originalRAF, originalCAF } from '/imports/utils/animationFrameBackup'; const PAGE_SIZE = 50; const CLEANUP_TIMEOUT = 3000; @@ -71,7 +73,7 @@ interface ChatListProps { } const isElement = (el: unknown): el is HTMLElement => { - return el instanceof HTMLElement; + return el instanceof originalHTMLElement; }; const isMap = (map: unknown): map is Map => { @@ -403,7 +405,7 @@ const ChatMessageList: React.FC = ({ const value = (timestamp - initialTimestamp) / 300; if (value <= 1) { container.scrollTop = initialPosition + (value * scrollPositionDiff); - requestAnimationFrame(animateScrollPosition); + originalRAF(animateScrollPosition); } else { container.scrollTop = container.scrollHeight - container.offsetHeight; setIsScrollingDisabled(false); @@ -420,10 +422,10 @@ const ChatMessageList: React.FC = ({ initialTimestamp = timestamp; initialPosition = scrollTop; scrollPositionDiff = scrollHeight - offsetHeight - scrollTop; - requestAnimationFrame(animateScrollPosition); + originalRAF(animateScrollPosition); }; - requestAnimationFrame(startScrollAnimation); + originalRAF(startScrollAnimation); }, []); const renderUnreadNotification = useMemo(() => { @@ -537,7 +539,7 @@ const ChatMessageList: React.FC = ({ }, ) => { if (currentFrame < stabilityFrames) { - const frameId = requestAnimationFrame(() => { + const frameId = originalRAF(() => { pollScrollEndEvent(setFrameId, onScrollEnd, { stabilityFrames, currentFrame: currentFrame + 1, @@ -552,10 +554,10 @@ const ChatMessageList: React.FC = ({ const startScrollEndEventPolling = useCallback((onScrollEnd: () => void) => { if (scrollEndFrameRef.current != null) { - cancelAnimationFrame(scrollEndFrameRef.current); + originalCAF(scrollEndFrameRef.current); scrollEndFrameRef.current = undefined; } - scrollEndFrameRef.current = requestAnimationFrame(() => { + scrollEndFrameRef.current = originalRAF(() => { pollScrollEndEvent((frameId) => { scrollEndFrameRef.current = frameId; }, onScrollEnd); @@ -608,7 +610,7 @@ const ChatMessageList: React.FC = ({ clearInterval(scrollActivityCheckInterval.current); } if (scrollEndFrameRef.current) { - cancelAnimationFrame(scrollEndFrameRef.current); + originalCAF(scrollEndFrameRef.current); } }, []); diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx index ef155c3f5341..8ae9519b2412 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx @@ -54,6 +54,7 @@ import { isMobile } from '/imports/utils/deviceInfo'; import { layoutSelect } from '/imports/ui/components/layout/context'; import { Layout } from '/imports/ui/components/layout/layoutTypes'; import { useModalRegistration } from '/imports/ui/core/singletons/modalController'; +import { originalRAF, originalCAF } from '/imports/utils/animationFrameBackup'; interface ChatMessageProps { message: Message; @@ -249,7 +250,7 @@ const ChatMessage = React.forwardRef(({ useImperativeHandle(ref, () => ({ requestFocus() { setTimeout(() => { - requestAnimationFrame(startScrollAnimation); + originalRAF(startScrollAnimation); }, 0); }, sequence: message.messageSequence, @@ -257,20 +258,20 @@ const ChatMessage = React.forwardRef(({ const startScrollAnimation = (timestamp: number) => { if ((containerRef.current?.offsetTop || 0) > (scrollRef.current?.scrollTop || 0)) { - requestAnimationFrame(startBackgroundAnimation); + originalRAF(startBackgroundAnimation); return; } animationInitialScrollPosition.current = scrollRef.current?.scrollTop || 0; animationScrollPositionDiff.current = (scrollRef.current?.scrollTop || 0) - ((containerRef.current?.offsetTop || 0) - ((scrollRef.current?.offsetHeight || 0) / 2)); animationInitialTimestamp.current = timestamp; - requestAnimationFrame(animateScrollPosition); + originalRAF(animateScrollPosition); }; const startBackgroundAnimation = (timestamp: number) => { animationInitialTimestamp.current = timestamp; animationInitialBgColor.current = containerRef.current?.style.backgroundColor ?? ''; - requestAnimationFrame(animateBackgroundColor); + originalRAF(animateBackgroundColor); }; const animateScrollPosition = (timestamp: number) => { @@ -282,10 +283,10 @@ const ChatMessage = React.forwardRef(({ if (!scrollContainer || !messageContainer) return; if (value <= 1) { scrollContainer.scrollTop = initialPosition - (value * diff); - requestAnimationFrame(animateScrollPosition); + originalRAF(animateScrollPosition); } else { scrollContainer.scrollTop = initialPosition - diff; - requestAnimationFrame(startBackgroundAnimation); + originalRAF(startBackgroundAnimation); } }; @@ -294,7 +295,7 @@ const ChatMessage = React.forwardRef(({ const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION; if (value < 1) { chatMessageContentWrapperRef.current.style.backgroundColor = `rgb(${colorBlueLighterChannel} / ${1 - value})`; - requestAnimationFrame(animateBackgroundColor); + originalRAF(animateBackgroundColor); } else { chatMessageContentWrapperRef.current.style.backgroundColor = animationInitialBgColor.current; } @@ -325,7 +326,7 @@ const ChatMessage = React.forwardRef(({ }, ) => { if (currentFrame < stabilityFrames) { - const frameId = requestAnimationFrame(() => { + const frameId = originalRAF(() => { pollScrollEndEvent(setFrameId, { stabilityFrames, currentFrame: currentFrame + 1, @@ -342,10 +343,10 @@ const ChatMessage = React.forwardRef(({ const startScrollEndEventPolling = useCallback(() => { if (scrollEndFrameRef.current != null) { - cancelAnimationFrame(scrollEndFrameRef.current); + originalCAF(scrollEndFrameRef.current); scrollEndFrameRef.current = undefined; } - scrollEndFrameRef.current = requestAnimationFrame(() => { + scrollEndFrameRef.current = originalRAF(() => { pollScrollEndEvent((frameId) => { scrollEndFrameRef.current = frameId; }); @@ -366,7 +367,7 @@ const ChatMessage = React.forwardRef(({ return () => { scrollRef?.current?.removeEventListener('scroll', callbackFunction); if (scrollEndFrameRef.current !== undefined) { - cancelAnimationFrame(scrollEndFrameRef.current); + originalCAF(scrollEndFrameRef.current); scrollEndFrameRef.current = undefined; } }; 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); + } } }; diff --git a/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx b/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx index 605c648796f0..b2d08f92e3f6 100644 --- a/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/emoji-rain/component.jsx @@ -2,6 +2,7 @@ import React, { useRef, useState, useEffect } from 'react'; import { getSettingsSingletonInstance } from '/imports/ui/services/settings'; import Service from './service'; import logger from '/imports/startup/client/logger'; +//import { originalRAF } from '/imports/utils/animationFrameBackup'; const EmojiRain = ({ reactions }) => { const Settings = getSettingsSingletonInstance(); @@ -57,6 +58,8 @@ const EmojiRain = ({ reactions }) => { } requestAnimationFrame(() => setTimeout(() => flyingEmojis.forEach((emoji) => { + // No effect observed (emoji rain works without using originalRAF). So removed. + //originalRAF(() => setTimeout(() => flyingEmojis.forEach((emoji) => { const { shapeElement, endPosition } = emoji; shapeElement.style.left = `${endPosition.x}px`; shapeElement.style.top = `${endPosition.y}px`; 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 34360f104cae..10cc75bded83 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; diff --git a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx index 14193e2ed3b7..ba7743245997 100644 --- a/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/notifications-bar/container.jsx @@ -66,7 +66,7 @@ const NotificationsBarContainer = () => { const errorMessage = useMemo(() => { const isCritical = rttStatus === STATUS_CRITICAL; - + if (!connected) { const code = isCritical ? 3002 : 3001; const msg = intl.formatMessage( diff --git a/bigbluebutton-html5/imports/ui/components/presentation/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/component.jsx index b99761690076..7062dd851073 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'; @@ -18,9 +20,11 @@ import browserInfo from '/imports/utils/browserInfo'; import { addAlert } from '../screenreader-alert/service'; import { debounce } from '/imports/utils/debounce'; import { throttle } from '/imports/utils/throttle'; +import { originalRAF, originalCAF } from '/imports/utils/animationFrameBackup'; 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 +62,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 +106,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 +336,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 +375,278 @@ class Presentation extends PureComponent { } } + detachPresentation() { + const { + slidePosition, + isPresentationDetached, + popupWindow, + toggleDetachPresentation, + } = this.props; + + + if (!isPresentationDetached) { + // Quit fullscreen first when detach fullscreen presentation + // This will however keep the popup window size same as fullscreen. + if (window.document.fullscreenElement != null) { + this.onFullscreenChange(); + } + + 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'; + // unnecessary div + //const container = popup.document.createElement('div'); + //popup.document.body.appendChild(container); + + // Copying the attributes of , so that the bbb-icons font looks a bit smaller + const mainHtml = document.documentElement; // メインウィンドウの + const popupHtml = popup.document.documentElement; + // class + popupHtml.className = mainHtml.className; + // style, which includes font-size: 14px + popupHtml.style.cssText = mainHtml.style.cssText; + //// dir + //if (mainHtml.hasAttribute('dir')) { + // popupHtml.setAttribute('dir', mainHtml.getAttribute('dir')); + //} else { + // popupHtml.removeAttribute('dir'); + //} + //// lang + //if (mainHtml.hasAttribute('lang')) { + // popupHtml.setAttribute('lang', mainHtml.getAttribute('lang')); + //} else { + // popupHtml.removeAttribute('lang'); + //} + // Copy all attributes ( extensions including DarkReader, which may not work anyway) + for (const attr of mainHtml.attributes) { + popupHtml.setAttribute(attr.name, attr.value); + } + + // headの中身をコピー + const headElements = document.head.cloneNode(true).childNodes; + headElements.forEach((node) => { + // script要素など重複実行したくないものを除外する + if (node.nodeName !== 'SCRIPT') { + popup.document.head.appendChild(node.cloneNode(true)); + } + }); + + // Firefox specific configuration + const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); + if (isFirefox) { + // 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); + + // Explicitely copy bbb-icons.css to show bbb-icons + fetch('stylesheets/bbb-icons.css') + .then(res => res.text()) + .then(css => { + const style = popup.document.createElement('style'); + style.textContent = css; + popup.document.head.appendChild(style); + }); + // Explicitly set FontFace to show bbb-icons + const fonts = [ + { name: 'bbb-icons', url: '/html5client/fonts/BbbIcons/bbb-icons.woff2' }, + ]; + fonts.forEach(({ name, url }) => { + const font = new FontFace(name, `url(${window.location.origin}${url})`); + font.load().then(loaded => popup.document.fonts.add(loaded)); + }); + } + + // 追加: 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) { + // //