feat(editor): Make popped out log view window maximizable (#18223)

This commit is contained in:
Suguru Inoue
2025-08-12 13:13:32 +02:00
committed by GitHub
parent 514825bd51
commit aeef79df53
14 changed files with 188 additions and 233 deletions

View File

@@ -121,16 +121,6 @@ declare global {
getVariant: (name: string) => string | boolean | undefined;
override: (name: string, value: string) => void;
};
// https://developer.mozilla.org/en-US/docs/Web/API/DocumentPictureInPicture
documentPictureInPicture?: {
window: Window | null;
requestWindow: (options?: {
width?: number;
height?: number;
preferInitialWindowPlacement?: boolean;
disallowReturnToOpener?: boolean;
}) => Promise<Window>;
};
Cypress: unknown;
}
}

View File

@@ -9,7 +9,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useToast } from '@/composables/useToast';
import { useI18n } from '@n8n/i18n';
import { nonExistingJsonPath, PiPWindowSymbol } from '@/constants';
import { nonExistingJsonPath, PopOutWindowKey } from '@/constants';
import { useClipboard } from '@/composables/useClipboard';
import { usePinnedData } from '@/composables/usePinnedData';
import { inject, computed, ref } from 'vue';
@@ -39,8 +39,8 @@ const props = withDefaults(
},
);
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const isInPiPWindow = computed(() => pipWindow?.value !== undefined);
const popOutWindow = inject(PopOutWindowKey, ref<Window | undefined>());
const isInPopOutWindow = computed(() => popOutWindow?.value !== undefined);
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
@@ -195,7 +195,7 @@ function handleCopyClick(commandData: { command: string }) {
v-else
trigger="click"
:teleported="
!isInPiPWindow // disabling teleport ensures the menu is rendered in PiP window
!isInPopOutWindow // disabling teleport ensures the menu is rendered in pop-out window
"
@command="handleCopyClick"
>

View File

@@ -1,6 +1,6 @@
import { inject, onBeforeUnmount, onMounted, ref } from 'vue';
import { useClipboard as useClipboardCore, useThrottleFn } from '@vueuse/core';
import { PiPWindowSymbol } from '@/constants';
import { PopOutWindowKey } from '@/constants';
type ClipboardEventFn = (data: string, event?: ClipboardEvent) => void;
@@ -9,9 +9,9 @@ export function useClipboard({
}: {
onPaste?: ClipboardEventFn;
} = {}) {
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const popOutWindow = inject(PopOutWindowKey, ref<Window | undefined>());
const { copy, copied, isSupported, text } = useClipboardCore({
navigator: pipWindow?.value?.navigator ?? window.navigator,
navigator: popOutWindow?.value?.navigator ?? window.navigator,
legacy: true,
});

View File

@@ -1,9 +1,10 @@
import { useSettingsStore } from '@/stores/settings.store';
import type { Ref } from 'vue';
const DEFAULT_TITLE = 'n8n';
const DEFAULT_TAGLINE = 'Workflow Automation';
export function useDocumentTitle() {
export function useDocumentTitle(windowRef?: Ref<Window | undefined>) {
const settingsStore = useSettingsStore();
const { releaseChannel } = settingsStore.settings;
const suffix =
@@ -13,7 +14,7 @@ export function useDocumentTitle() {
const set = (title: string) => {
const sections = [title || DEFAULT_TAGLINE, suffix];
document.title = sections.join(' - ');
(windowRef?.value?.document ?? document).title = sections.join(' - ');
};
const reset = () => {

View File

@@ -1,4 +1,4 @@
import { PiPWindowSymbol } from '@/constants';
import { PopOutWindowKey } from '@/constants';
import { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
import { useActiveElement, useEventListener } from '@vueuse/core';
import type { MaybeRefOrGetter } from 'vue';
@@ -30,8 +30,8 @@ export const useKeybindings = (
disabled: MaybeRefOrGetter<boolean>;
},
) => {
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const activeElement = useActiveElement({ window: pipWindow?.value });
const popOutWindow = inject(PopOutWindowKey, ref<Window | undefined>());
const activeElement = useActiveElement({ window: popOutWindow?.value });
const { isCtrlKeyPressed } = useDeviceSupport();
const isDisabled = computed(() => toValue(options?.disabled));
@@ -150,5 +150,5 @@ export const useKeybindings = (
}
}
useEventListener(pipWindow?.value?.document ?? document, 'keydown', onKeyDown);
useEventListener(popOutWindow?.value?.document ?? document, 'keydown', onKeyDown);
};

View File

@@ -964,10 +964,10 @@ export const CanvasKey = 'canvas' as unknown as InjectionKey<CanvasInjectionData
export const CanvasNodeKey = 'canvasNode' as unknown as InjectionKey<CanvasNodeInjectionData>;
export const CanvasNodeHandleKey =
'canvasNodeHandle' as unknown as InjectionKey<CanvasNodeHandleInjectionData>;
export const PiPWindowSymbol = 'PiPWindow' as unknown as InjectionKey<Ref<Window | undefined>>;
export const ExpressionLocalResolveContextSymbol = Symbol(
'ExpressionLocalResolveContext',
) as InjectionKey<ComputedRef<ExpressionLocalResolveContext | undefined>>;
export const PopOutWindowKey: InjectionKey<Ref<Window | undefined>> = Symbol('PopOutWindow');
export const ExpressionLocalResolveContextSymbol: InjectionKey<
ComputedRef<ExpressionLocalResolveContext | undefined>
> = Symbol('ExpressionLocalResolveContext');
export const APP_MODALS_ELEMENT_ID = 'app-modals';
export const CODEMIRROR_TOOLTIP_CONTAINER_ELEMENT_ID = 'cm-tooltip-container';

View File

@@ -16,16 +16,19 @@ import { useLogsStore } from '@/stores/logs.store';
import { useLogsPanelLayout } from '@/features/logs/composables/useLogsPanelLayout';
import { type KeyMap } from '@/composables/useKeybindings';
import LogsViewKeyboardEventListener from './LogsViewKeyboardEventListener.vue';
import { useWorkflowsStore } from '@/stores/workflows.store';
const props = withDefaults(defineProps<{ isReadOnly?: boolean }>(), { isReadOnly: false });
const container = useTemplateRef('container');
const logsContainer = useTemplateRef('logsContainer');
const pipContainer = useTemplateRef('pipContainer');
const pipContent = useTemplateRef('pipContent');
const popOutContainer = useTemplateRef('popOutContainer');
const popOutContent = useTemplateRef('popOutContent');
const logsStore = useLogsStore();
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const workflowName = computed(() => workflowsStore.workflow.name);
const {
height,
@@ -36,7 +39,7 @@ const {
isPoppedOut,
isCollapsingDetailsPanel,
isOverviewPanelFullWidth,
pipWindow,
popOutWindow,
onResize,
onResizeEnd,
onToggleOpen,
@@ -45,7 +48,7 @@ const {
onChatPanelResizeEnd,
onOverviewPanelResize,
onOverviewPanelResizeEnd,
} = useLogsPanelLayout(pipContainer, pipContent, container, logsContainer);
} = useLogsPanelLayout(workflowName, popOutContainer, popOutContent, container, logsContainer);
const {
currentSessionId,
@@ -136,20 +139,20 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
</script>
<template>
<div ref="pipContainer">
<!-- force re-create with key for shortcuts to work in PiP window -->
<div ref="popOutContainer">
<!-- force re-create with key for shortcuts to work in pop-out window -->
<LogsViewKeyboardEventListener
:key="String(!!pipWindow)"
:key="String(!!popOutWindow)"
:key-map="keyMap"
:container="container"
/>
<div ref="pipContent" :class="$style.pipContent">
<div ref="popOutContent" :class="[$style.popOutContent, isPoppedOut ? $style.poppedOut : '']">
<N8nResizeWrapper
:height="height"
:height="isPoppedOut ? undefined : height"
:supported-directions="['top']"
:is-resizing-enabled="!isPoppedOut"
:class="$style.resizeWrapper"
:style="{ height: isOpen ? `${height}px` : 'auto' }"
:style="{ height: isOpen && !isPoppedOut ? `${height}px` : 'auto' }"
@resize="onResize"
@resizeend="onResizeEnd"
>
@@ -161,12 +164,12 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
:width="chatPanelWidth"
:style="{ width: `${chatPanelWidth}px` }"
:class="$style.chat"
:window="pipWindow"
:window="popOutWindow"
@resize="onChatPanelResize"
@resizeend="onChatPanelResizeEnd"
>
<ChatMessagesPanel
:key="`canvas-chat-${currentSessionId}${isPoppedOut ? '-pip' : ''}`"
:key="`canvas-chat-${currentSessionId}${isPoppedOut ? '-pop-out' : ''}`"
data-test-id="canvas-chat"
:is-open="isOpen"
:is-read-only="isReadOnly"
@@ -189,7 +192,7 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
:style="{ width: isLogDetailsVisuallyOpen ? `${overviewPanelWidth}px` : '' }"
:supported-directions="['right']"
:is-resizing-enabled="isLogDetailsOpen"
:window="pipWindow"
:window="popOutWindow"
@resize="onOverviewPanelResize"
@resizeend="handleResizeOverviewPanelEnd"
>
@@ -222,7 +225,7 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
:class="$style.logDetails"
:is-open="isOpen"
:log-entry="selected"
:window="pipWindow"
:window="popOutWindow"
:latest-info="latestNodeNameById[selected.node.id]"
:panels="logsStore.detailsState"
:collapsing-input-table-column-name="inputCollapsingColumnName"
@@ -245,14 +248,7 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
</template>
<style lang="scss" module>
@media all and (display-mode: picture-in-picture) {
.resizeWrapper {
height: 100% !important;
max-height: 100vh !important;
}
}
.pipContent {
.popOutContent {
height: 100%;
position: relative;
overflow: hidden;
@@ -264,6 +260,10 @@ function handleChangeOutputTableColumnCollapsing(columnName: string | null) {
flex-basis: 0;
border-top: var(--border-base);
background-color: var(--color-background-light);
.poppedOut & {
border-top: none;
}
}
.container {

View File

@@ -71,7 +71,7 @@ function handleSelectMenuItem(selected: string) {
activator-icon="ellipsis"
activator-size="small"
:items="menuItems"
:teleported="false /* for PiP window */"
:teleported="false /* for pop-out window */"
@select="handleSelectMenuItem"
/>
<KeyboardShortcutTooltip

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
import { type KeyMap, useKeybindings } from '@/composables/useKeybindings';
import { PiPWindowSymbol } from '@/constants';
import { PopOutWindowKey } from '@/constants';
import { useActiveElement } from '@vueuse/core';
import { ref, computed, toRef, inject } from 'vue';
const { container, keyMap } = defineProps<{ keyMap: KeyMap; container: HTMLElement | null }>();
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const popOutWindow = inject(PopOutWindowKey, ref<Window | undefined>());
const activeElement = useActiveElement({ window: pipWindow?.value });
const activeElement = useActiveElement({ window: popOutWindow?.value });
const isBlurred = computed(() => {
if (pipWindow?.value) {
return pipWindow.value.document.activeElement === null;
if (popOutWindow?.value) {
return popOutWindow.value.document.activeElement === null;
}
return (

View File

@@ -8,7 +8,7 @@ import { waitingNodeTooltip } from '@/utils/executionUtils';
import { N8nLink, N8nText } from '@n8n/design-system';
import { computed, inject, ref } from 'vue';
import { I18nT } from 'vue-i18n';
import { PiPWindowSymbol } from '@/constants';
import { PopOutWindowKey } from '@/constants';
import { isSubNodeLog } from '../logs.utils';
const { title, logEntry, paneType, collapsingTableColumnName } = defineProps<{
@@ -25,7 +25,7 @@ const emit = defineEmits<{
const locale = useI18n();
const ndvStore = useNDVStore();
const pipWindow = inject(PiPWindowSymbol, ref<Window | undefined>());
const popOutWindow = inject(PopOutWindowKey, ref<Window | undefined>());
const displayMode = ref<IRunDataDisplayMode>(paneType === 'input' ? 'schema' : 'table');
const isMultipleInput = computed(
@@ -74,7 +74,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
<RunData
v-if="runDataProps"
v-bind="runDataProps"
:key="`run-data${pipWindow ? '-pip' : ''}`"
:key="`run-data${popOutWindow ? '-pop-out' : ''}`"
:class="$style.component"
:workflow-object="logEntry.workflow"
:workflow-execution="logEntry.execution"

View File

@@ -1,9 +1,9 @@
import { computed, type ShallowRef } from 'vue';
import { computed, type ComputedRef, type ShallowRef } from 'vue';
import { useTelemetry } from '@/composables/useTelemetry';
import { watch } from 'vue';
import { useLogsStore } from '@/stores/logs.store';
import { useResizablePanel } from '@/composables/useResizablePanel';
import { usePiPWindow } from '@/features/logs/composables/usePiPWindow';
import { usePopOutWindow } from '@/features/logs/composables/usePopOutWindow';
import {
LOGS_PANEL_STATE,
LOCAL_STORAGE_OVERVIEW_PANEL_WIDTH,
@@ -12,8 +12,9 @@ import {
} from '@/features/logs/logs.constants';
export function useLogsPanelLayout(
pipContainer: Readonly<ShallowRef<HTMLElement | null>>,
pipContent: Readonly<ShallowRef<HTMLElement | null>>,
workflowName: ComputedRef<string>,
popOutContainer: Readonly<ShallowRef<HTMLElement | null>>,
popOutContent: Readonly<ShallowRef<HTMLElement | null>>,
container: Readonly<ShallowRef<HTMLElement | null>>,
logsContainer: Readonly<ShallowRef<HTMLElement | null>>,
) {
@@ -51,13 +52,20 @@ export function useLogsPanelLayout(
: resizer.isResizing.value && resizer.size.value > 0,
);
const isCollapsingDetailsPanel = computed(() => overviewPanelResizer.isFullSize.value);
const popOutWindowTitle = computed(() => `Logs - ${workflowName.value}`);
const shouldPopOut = computed(() => logsStore.state === LOGS_PANEL_STATE.FLOATING);
const { canPopOut, isPoppedOut, pipWindow } = usePiPWindow({
const {
canPopOut,
isPoppedOut,
popOutWindow: popOutWindow,
} = usePopOutWindow({
title: popOutWindowTitle,
initialHeight: 400,
initialWidth: window.document.body.offsetWidth * 0.8,
container: pipContainer,
content: pipContent,
shouldPopOut: computed(() => logsStore.state === LOGS_PANEL_STATE.FLOATING),
container: popOutContainer,
content: popOutContent,
shouldPopOut,
onRequestClose: () => {
if (!isOpen.value) {
return;
@@ -123,7 +131,7 @@ export function useLogsPanelLayout(
isCollapsingDetailsPanel,
isPoppedOut,
isOverviewPanelFullWidth: overviewPanelResizer.isFullSize,
pipWindow,
popOutWindow,
onToggleOpen: handleToggleOpen,
onPopOut: handlePopOut,
onResize: resizer.onResize,

View File

@@ -1,116 +0,0 @@
/* eslint-disable vue/one-component-per-file */
import { computed, defineComponent, h, ref } from 'vue';
import { usePiPWindow } from './usePiPWindow';
import { waitFor } from '@testing-library/vue';
import { renderComponent } from '@/__tests__/render';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
describe(usePiPWindow, () => {
const documentPictureInPicture: NonNullable<Window['documentPictureInPicture']> = {
window: null,
requestWindow: async () =>
({
document: { body: { append: vi.fn(), removeChild: vi.fn() } },
addEventListener: vi.fn(),
close: vi.fn(),
}) as unknown as Window,
};
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }));
});
describe('canPopOut', () => {
it('should return false if window.documentPictureInPicture is not available', () => {
const MyComponent = defineComponent({
setup() {
const container = ref<HTMLDivElement | null>(null);
const content = ref<HTMLDivElement | null>(null);
const pipWindow = usePiPWindow({
container,
content,
shouldPopOut: computed(() => true),
onRequestClose: vi.fn(),
});
return () =>
h(
'div',
{ ref: container },
h('div', { ref: content }, String(pipWindow.canPopOut.value)),
);
},
});
const { queryByText } = renderComponent(MyComponent);
expect(queryByText('false')).toBeInTheDocument();
});
it('should return true if window.documentPictureInPicture is available', () => {
Object.assign(window, { documentPictureInPicture });
const MyComponent = defineComponent({
setup() {
const container = ref<HTMLDivElement | null>(null);
const content = ref<HTMLDivElement | null>(null);
const pipWindow = usePiPWindow({
container,
content,
shouldPopOut: computed(() => true),
onRequestClose: vi.fn(),
});
return () =>
h(
'div',
{ ref: container },
h('div', { ref: content }, String(pipWindow.canPopOut.value)),
);
},
});
const { queryByText } = renderComponent(MyComponent);
expect(queryByText('true')).toBeInTheDocument();
});
});
describe('isPoppedOut', () => {
beforeEach(() => {
Object.assign(window, { documentPictureInPicture });
});
it('should be set to true when popped out', async () => {
const shouldPopOut = ref(false);
const MyComponent = defineComponent({
setup() {
const container = ref<HTMLDivElement | null>(null);
const content = ref<HTMLDivElement | null>(null);
const pipWindow = usePiPWindow({
container,
content,
shouldPopOut: computed(() => shouldPopOut.value),
onRequestClose: vi.fn(),
});
return () =>
h(
'div',
{ ref: container },
h('div', { ref: content }, String(pipWindow.isPoppedOut.value)),
);
},
});
const { queryByText } = renderComponent(MyComponent);
expect(queryByText('false')).toBeInTheDocument();
shouldPopOut.value = true;
await waitFor(() => expect(queryByText('true')).toBeInTheDocument());
});
});
});

View File

@@ -0,0 +1,58 @@
/* eslint-disable vue/one-component-per-file */
import { computed, defineComponent, h, ref } from 'vue';
import { usePopOutWindow } from './usePopOutWindow';
import { waitFor } from '@testing-library/vue';
import { renderComponent } from '@/__tests__/render';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
describe(usePopOutWindow, () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }));
});
describe('isPoppedOut', () => {
beforeEach(() => {
Object.assign(window, {
open: () =>
({
document: { body: { append: vi.fn() } },
addEventListener: vi.fn(),
close: vi.fn(),
}) as unknown as Window,
});
});
it('should be set to true when popped out', async () => {
const shouldPopOut = ref(false);
const MyComponent = defineComponent({
setup() {
const container = ref<HTMLDivElement | null>(null);
const content = ref<HTMLDivElement | null>(null);
const popOutWindow = usePopOutWindow({
title: computed(() => ''),
container,
content,
shouldPopOut: computed(() => shouldPopOut.value),
onRequestClose: vi.fn(),
});
return () =>
h(
'div',
{ ref: container },
h('div', { ref: content }, String(popOutWindow.isPoppedOut.value)),
);
},
});
const { queryByText } = renderComponent(MyComponent);
expect(queryByText('false')).toBeInTheDocument();
shouldPopOut.value = true;
await waitFor(() => expect(queryByText('true')).toBeInTheDocument());
});
});
});

View File

@@ -1,6 +1,5 @@
import { PiPWindowSymbol } from '@/constants';
import { useUIStore } from '@/stores/ui.store';
import { applyThemeToBody } from '@/stores/ui.utils';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { PopOutWindowKey } from '@/constants';
import { useProvideTooltipAppendTo } from '@n8n/design-system/composables/useTooltipAppendTo';
import {
computed,
@@ -14,7 +13,8 @@ import {
watch,
} from 'vue';
interface UsePiPWindowOptions {
interface UsePopOutWindowOptions {
title: ComputedRef<string>;
initialWidth?: number;
initialHeight?: number;
container: Readonly<ShallowRef<HTMLElement | null>>;
@@ -23,10 +23,10 @@ interface UsePiPWindowOptions {
onRequestClose: () => void;
}
interface UsePiPWindowReturn {
interface UsePopOutWindowReturn {
isPoppedOut: ComputedRef<boolean>;
canPopOut: ComputedRef<boolean>;
pipWindow?: Ref<Window | undefined>;
popOutWindow?: Ref<Window | undefined>;
}
function isStyle(node: Node): node is HTMLElement {
@@ -58,63 +58,76 @@ function syncStyleMutations(destination: Window, mutations: MutationRecord[]) {
}
}
function copyFavicon(source: Window, target: Window) {
const iconUrl = source.document.querySelector('link[rel=icon]')?.getAttribute('href');
if (iconUrl) {
const link = target.document.createElement('link');
link.setAttribute('rel', 'icon');
link.setAttribute('href', iconUrl);
target.document.head.appendChild(link);
}
}
/**
* A composable that allows to pop out given content in document PiP (picture-in-picture) window
* A composable that allows to pop out given content in child window
*/
export function usePiPWindow({
export function usePopOutWindow({
title,
container,
content,
initialHeight,
initialWidth,
shouldPopOut,
onRequestClose,
}: UsePiPWindowOptions): UsePiPWindowReturn {
const pipWindow = ref<Window>();
}: UsePopOutWindowOptions): UsePopOutWindowReturn {
const popOutWindow = ref<Window>();
const isUnmounting = ref(false);
const canPopOut = computed(
() =>
!!window.documentPictureInPicture /* Browser supports the API */ &&
window.parent === window /* Not in iframe */,
);
const isPoppedOut = computed(() => !!pipWindow.value);
const canPopOut = computed(() => window.parent === window /* Not in iframe */);
const isPoppedOut = computed(() => !!popOutWindow.value);
const tooltipContainer = computed(() =>
isPoppedOut.value ? (content.value ?? undefined) : undefined,
);
const uiStore = useUIStore();
const observer = new MutationObserver((mutations) => {
if (pipWindow.value) {
syncStyleMutations(pipWindow.value, mutations);
if (popOutWindow.value) {
syncStyleMutations(popOutWindow.value, mutations);
}
});
const documentTitle = useDocumentTitle(popOutWindow);
// Copy over dynamic styles to PiP window to support lazily imported modules
// Copy over dynamic styles to child window to support lazily imported modules
observer.observe(document.head, { childList: true, subtree: true });
provide(PiPWindowSymbol, pipWindow);
provide(PopOutWindowKey, popOutWindow);
useProvideTooltipAppendTo(tooltipContainer);
async function showPip() {
async function showPopOut() {
if (!content.value) {
return;
}
pipWindow.value =
pipWindow.value ??
(await window.documentPictureInPicture?.requestWindow({
width: initialWidth,
height: initialHeight,
disallowReturnToOpener: true,
}));
if (!popOutWindow.value) {
// Chrome ignores these options but effective in Firefox
const options = `popup=yes,width=${initialWidth},height=${initialHeight},left=100,top=100,toolbar=no,menubar=no,scrollbars=yes,resizable=yes`;
// Copy style sheets over from the initial document
// so that the content looks the same.
[...document.styleSheets].forEach((styleSheet) => {
popOutWindow.value = window.open('', '_blank', options) ?? undefined;
}
if (!popOutWindow.value) {
return;
}
copyFavicon(window, popOutWindow.value);
for (const styleSheet of [...document.styleSheets]) {
try {
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
const style = document.createElement('style');
style.textContent = cssRules;
pipWindow.value?.document.head.appendChild(style);
popOutWindow.value.document.head.appendChild(style);
} catch (e) {
const link = document.createElement('link');
@@ -122,18 +135,18 @@ export function usePiPWindow({
link.type = styleSheet.type;
link.media = styleSheet.media as unknown as string;
link.href = styleSheet.href as string;
pipWindow.value?.document.head.appendChild(link);
popOutWindow.value.document.head.appendChild(link);
}
});
}
// Move the content to the Picture-in-Picture window.
pipWindow.value?.document.body.append(content.value);
pipWindow.value?.addEventListener('pagehide', () => !isUnmounting.value && onRequestClose());
// Move the content to child window.
popOutWindow.value.document.body.append(content.value);
popOutWindow.value.addEventListener('pagehide', () => !isUnmounting.value && onRequestClose());
}
function hidePiP() {
pipWindow.value?.close();
pipWindow.value = undefined;
function hidePopOut() {
popOutWindow.value?.close();
popOutWindow.value = undefined;
if (content.value) {
container.value?.appendChild(content.value);
@@ -141,17 +154,15 @@ export function usePiPWindow({
}
// `requestAnimationFrame()` to make sure the content is already rendered
watch(shouldPopOut, (value) => (value ? requestAnimationFrame(showPip) : hidePiP()), {
watch(shouldPopOut, (value) => (value ? requestAnimationFrame(showPopOut) : hidePopOut()), {
immediate: true,
});
// It seems "prefers-color-scheme: dark" media query matches in PiP window by default
// So we're enforcing currently applied theme in the main window by setting data-theme in PiP's body element
watch(
[() => uiStore.appliedTheme, pipWindow],
([theme, pip]) => {
if (pip) {
applyThemeToBody(theme, pip);
[title, popOutWindow],
([newTitle, win]) => {
if (win) {
documentTitle.set(newTitle);
}
},
{ immediate: true },
@@ -163,8 +174,11 @@ export function usePiPWindow({
onBeforeUnmount(() => {
isUnmounting.value = true;
pipWindow.value?.close();
if (popOutWindow.value) {
popOutWindow.value.close();
onRequestClose();
}
});
return { canPopOut, isPoppedOut, pipWindow };
return { canPopOut, isPoppedOut, popOutWindow };
}