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) {
+ // //