'use strict'; let permissionStatus = null; let screenDetails = null; let popup = null; let popupObserverInterval = null; let handlingMultiScreenRequest = false; function showWarning(text) { const warning = document.getElementById('warning'); if (warning && warning.innerHTML !== text) { if (text) console.warn(text); warning.hidden = !text; warning.innerHTML = text; } } window.addEventListener('load', async () => { console.log(screen); if (!('getScreenDetails' in self) || !('isExtended' in screen) || !('onchange' in screen)) { showWarning("Please try a browser that supports multi-screen features; see the demo instructions"); } else { screen.addEventListener('change', () => { updateScreens(/*requestPermission=*/false); }); window.addEventListener('resize', () => { updateScreens(/*requestPermission=*/false); }); permissionStatus = await navigator.permissions.query({name:'window-management'}); permissionStatus.addEventListener('change', (p) => { permissionStatus = p; updateScreens(/*requestPermission=*/false); }); console.log(77,permissionStatus); } // document.getElementById('openPopupButton').addEventListener('click', updateScreens(/*requestPermission=*/false)); // updateScreens(/*requestPermission=*/false); }); window.addEventListener('keyup', handleWindowKeyup); function setScreenListeners() { let screens = screenDetails ? screenDetails.screens : [ window.screen ]; for (const s of screens) s.onchange = () => { updateScreens(/*requestPermission=*/false); }; } async function getScreenDetailsWithWarningAndFallback(requestPermission = false) { console.log(88,screenDetails,self); if ('getScreenDetails' in self) { console.log(99,permissionStatus); if (!screenDetails && ((permissionStatus && permissionStatus.state === 'granted') || (permissionStatus && permissionStatus.state === 'prompt' && !requestPermission))) { screenDetails = await window.getScreenDetails().catch(e =>{ console.error(e); return null; }); console.log(99,screenDetails); if (screenDetails) { screenDetails.addEventListener('screenschange', () => { updateScreens(/*requestPermission=*/false); setScreenListeners(); }); setScreenListeners(); } } if (screenDetails && screenDetails.screens.length > 1) showWarning(); // Clear any warning. else if (screenDetails && screenDetails.screens.length == 1) showWarning("Please extend your desktop over multiple screens for full demo functionality"); else if (requestPermission || (permissionStatus && permissionStatus.state === 'denied')) showWarning("Please allow the Window Management permission for full demo functionality"); if (screenDetails) { return screenDetails.screens; } } return [ window.screen ]; } async function showScreens(screens) { for (const screen of screens) { if (screen.left === undefined) screen.left = screen.availLeft; if (screen.top === undefined) screen.top = screen.availTop; } const context = screensCanvas.getContext('2d'); context.clearRect(0, 0, screensCanvas.width, screensCanvas.height); let scale = 1.0/10.0; let screenSpace = { left:0, top:0, right:0, bottom:0 }; for (const screen of screens) { screenSpace.left = Math.min(screenSpace.left, screen.left); screenSpace.top = Math.min(screenSpace.top, screen.top); screenSpace.right = Math.max(screenSpace.right, screen.left + screen.width); screenSpace.bottom = Math.max(screenSpace.bottom, screen.top + screen.height); } let origin = { left:screenSpace.left, top:screenSpace.top }; scale = Math.min(screensCanvas.getBoundingClientRect().width / (screenSpace.right-screenSpace.left), screensCanvas.getBoundingClientRect().height / (screenSpace.bottom-screenSpace.top), 0.5); const colors = [ "#FF2222", "#22FF22", "#2222FF" ]; const availColors = [ "#FFAAAA", "#AAFFAA", "#AAAAFF" ]; for (let i = 0; i < screens.length; ++i) { const screen = screens[i]; const rect = { left:(screen.left-origin.left)*scale, top:(screen.top-origin.top)*scale, width:screen.width*scale, height:screen.height*scale }; context.fillStyle = colors[i%colors.length]; context.fillRect(rect.left, rect.top, rect.width, rect.height); const availrect = { left:(screen.availLeft-origin.left)*scale, top:(screen.availTop-origin.top)*scale, width:screen.availWidth*scale, height:screen.availHeight*scale }; context.fillStyle = availColors[i%colors.length]; context.fillRect(availrect.left, availrect.top, availrect.width, availrect.height); context.fillStyle = "#000000"; context.font = "13px Arial"; context.fillText(screen == window.screen ? '[window.screen]' : `[${i}] "${screen.label}" ${screen.isPrimary ? '(Primary)': ''}`, rect.left+10, rect.top+20); context.fillText(`${screen.left},${screen.top} ${screen.width}x${screen.height}`, rect.left+10, rect.top+35); context.fillText(`devicePixelRatio:${screen.devicePixelRatio}, colorDepth:${screen.colorDepth}`, rect.left+10, rect.top+50); context.fillText(`isExtended:${screen.isExtended}` + (screen == window.screen ? `` : `, isInternal:${screen.isInternal}`), rect.left+10, rect.top+65); } const rect = { left:(window.screenLeft-origin.left)*scale, top:(window.screenTop-origin.top)*scale, width:window.outerWidth*scale, height:window.outerHeight*scale }; context.strokeStyle = "#444444"; context.fillStyle = "#444444"; context.strokeRect(rect.left, rect.top, rect.width, rect.height); context.fillText(`window ${window.screenLeft},${window.screenTop} ${window.outerWidth}x${window.outerHeight}`, rect.left+10, rect.top+rect.height-10); } async function updateScreens(requestPermission = true) { console.log(555); const screens = await getScreenDetailsWithWarningAndFallback(requestPermission); if (document.getElementById('screensCanvas')) showScreens(screens); if (document.getElementById('openPopupDropdown')) { console.log(66,screens); openPopupDropdown.innerHTML = ``; for (let i = 0; i < screens.length; ++i) openPopupDropdown.innerHTML += screens[i] == window.screen ? `` : ``; console.log(openPopupDropdown); } if (document.getElementById('toggleFullscreenDropdown')) { toggleFullscreenDropdown.innerHTML = ``; for (let i = 0; i < screens.length; ++i) toggleFullscreenDropdown.innerHTML += screens[i] == window.screen ? `` : ``; } if (document.getElementById('fullscreenSlideDropdown')) { fullscreenSlideDropdown.innerHTML = ``; for (let i = 0; i < screens.length; ++i) fullscreenSlideDropdown.innerHTML += screens[i] == window.screen ? `` : ``; } if (document.getElementById('fullscreenSlideAndOpenNotesWindowDropdown')) { fullscreenSlideAndOpenNotesWindowDropdown.innerHTML = ``; for (let i = 0; i < screens.length; ++i) fullscreenSlideAndOpenNotesWindowDropdown.innerHTML += screens[i] == window.screen ? `` : ``; } if (document.getElementById('fullscreenOpenerDropdown')) { fullscreenOpenerDropdown.innerHTML = ``; for (let i = 0; i < screens.length; ++i) fullscreenOpenerDropdown.innerHTML += screens[i] == window.screen ? `` : ``; } return screens; } function getFeaturesFromOptions(options) { return "left=" + options.x + ",top=" + options.y + ",width=" + options.width + ",height=" + options.height; } function openWindow(options = null) { if (!options || !options.url) { options = { url: openWindowUrlInput.value, x: openWindowLeftInput.value, y: openWindowTopInput.value, width: openWindowWidthInput.value, height: openWindowHeightInput.value }; } if (popupObserverInterval) clearInterval(popupObserverInterval); const features = getFeaturesFromOptions(options); popup = window.open(options.url, '_blank', features); console.log('INFO: Requested popup with features: "' + features + '" result: ' + popup); if (popup) { popupObserverInterval = setInterval(() => { if (popup.closed) { console.log('INFO: The latest-opened popup was closed'); clearInterval(popupObserverInterval); popupObserverInterval = null; popup = null; } }, 300); } return popup; } // TODO: Add some worthwhile multi-window opening example? // async function openWindows() { // let count = openWindowsCountInput.value; // let popups = []; // const screens = await getScreenDetailsWithWarningAndFallback(); // const perScreen = Math.ceil(count / screens.length); // console.log(`openWindows count:${count}, screens:${screens.length}, perScreen:${perScreen}`); // for (const s of screens) { // const cols = Math.ceil(Math.sqrt(perScreen)); // const rows = Math.ceil(perScreen / cols); // for (let r = 0; r < rows; ++r) { // for (let c = 0; c < cols && count-- > 0; ++c) { // const options = { // x: s.availLeft + s.availWidth * c / cols, // y: s.availTop + s.availHeight * r / rows, // width: s.availWidth / cols, // height: s.availHeight / rows, // }; // const url = `data:text/html;charset=utf-8,row:${r} col:${c}

row:${r} col:${c}

`; // console.log(`INFO: opening window row:${r} col:${c}, (${options.x},${options.y} ${options.width}x${options.height}`); // popups.push(window.open(url, '_blank', getFeaturesFromOptions(options))); // } // } // } // const interval = setInterval(() => { // if (popups.some(p => p.closed)) { // popups.forEach(p => p.close()); // clearInterval(interval); // } // }, 300); // } async function toggleElementFullscreen(element, screenId) { const screens = await getScreenDetailsWithWarningAndFallback(); if (Number.isInteger(screenId) && screenId >= 0 && screenId < screens.length) { console.log('INFO: Requesting fullscreen on screen: ' + screenId); await element.requestFullscreen({ screen: screens[screenId] }); await ensureWindowIsOnScreen(window, screenId); } else if (document.fullscreenElement == element) { console.log('INFO: Exiting fullscreen'); await document.exitFullscreen(); } else { console.log('INFO: Requesting fullscreen'); await element.requestFullscreen(); } } async function toggleFullscreen(screenId) { await toggleElementFullscreen(document.documentElement, screenId); } async function openNotesWindow(screenId) { const screens = await getScreenDetailsWithWarningAndFallback(); const s = screens[screenId] || screens[0] || window.screen; const options = { url:'./notes.html', x:s.availLeft, y:s.availTop, width:s.availWidth, height:s.availHeight }; return openWindow(options); } async function openPopup(screenId) { const screens = await getScreenDetailsWithWarningAndFallback(/*requestPermission=*/true); const s = screens[screenId] || screens[0] || window.screen; const options = { url:'./popup.html', x:s.availLeft, y:s.availTop, width:s.availWidth, height:s.availHeight }; return openWindow(options); } async function fullscreenSlide(screenId) { await toggleElementFullscreen(slideIframe, screenId); } async function fullscreenSlideAndOpenNotesWindow(screenId) { const screens = await getScreenDetailsWithWarningAndFallback(); if (!Number.isInteger(screenId) || screenId < 0 || screenId >= screens.length) screenId = 0; await fullscreenSlide(screenId); // Await potential async fullscreen space transitions. If the Mac preference // "Displays have separate Spaces" is disabled, then opening a popup window // while the target display is transitioning to another space may put the // window in the wrong space (i.e. it won't be visible). See crbug.com/1401041 await new Promise(r => setTimeout(r, 700)); // Find the screen where the window was actually made fullscreen. This may not // match the request if the window was made fullscreen within the tab area, // which happens when the tab content is being captured for video streaming. console.log(777,screenDetails); screenId = screenDetails.screens.indexOf(screenDetails.currentScreen); const notesWindow = await openNotesWindow(screenId == 0 ? 1 : 0); if (notesWindow && screens.length > 1) { const interval = setInterval(() => { if (!document.fullscreenElement && !notesWindow.closed) { notesWindow.close(); clearInterval(interval); } else if (document.fullscreenElement && notesWindow.closed) { document.exitFullscreen(); clearInterval(interval); } }, 300); } } async function handleWindowMessage(messageEvent) { if (typeof(messageEvent.data) === "object" && messageEvent.data.capability === "fullscreen") { // Request to use a fullscreen capability delegated by another window; reply with the result. const element = document.fullscreenElement ? document.fullscreenElement : document.documentElement; console.log("INFO: Requesting fullscreen via delegated capability"); toggleElementFullscreen(element, messageEvent.data.targetScreenId) .then(() => { messageEvent.source.postMessage(true); }) .catch(() => { messageEvent.source.postMessage(false); }); } } async function handleWindowKeyup(keyEvent) { // If the event targeted a same-origin iframe, allow the parent to handle it instead. if (window !== window.parent && window.parent.origin === window.origin) { window.parent.handleWindowKeyup(keyEvent); return; } // if (keyEvent.code === "Escape" && !["/index.html", "/window-placement-demo/"].includes(window.location.pathname)) { // window.close(); // Close auxiliary windows on [Esc]. // } else if (keyEvent.code === "Enter" && openWindowControls && openWindowControls.contains(keyEvent.target)) { // openWindow(); // Open a window when on [Enter] targeting an "Open window" input element. // } else if (keyEvent.code === "KeyS" && screen.isExtended) { // // Bail to avoid issuing concurrent conflicting asynchronous multi-screen requests. // // Browsers may drop fullscreen for security if popup and fullscreen displays coincide. // if (handlingMultiScreenRequest) { // console.log("INFO: Throttling multi-screen placement requests"); // return; // } // handlingMultiScreenRequest = true; // // Initiate a fullscreen + popup multi-screen experience, or swap their screens on [s]. // if (popup && !popup.closed) // await fullscreenThisWindowAndMovePopup(); // else if (opener && !opener.closed) // await fullscreenOpenerAndMoveThisPopup(); // else if (document.getElementById("slideIframe")) // await fullscreenSlideAndOpenNotesWindow(); // handlingMultiScreenRequest = false; // } // } async function isWindowOnScreen(w, screenId) { const windowScreenDetails = await w.getScreenDetails(); const screen = windowScreenDetails.screens[screenId]; const center = { x: w.screenLeft + w.outerWidth / 2, y: w.screenTop + w.outerHeight / 2 }; return center.x >= screen.left && (center.x < screen.left + screen.width) && center.y >= screen.top && (center.y < screen.top + screen.height) && windowScreenDetails.currentScreen == screen; } async function waitForWindowOnScreen(w, screenId, resolve, timestamp = Date.now()) { if (!w || w.closed || Date.now() - timestamp > 3000) resolve(false); else if (await isWindowOnScreen(w, screenId)) resolve(true); else setTimeout(waitForWindowOnScreen.bind(this, w, screenId, resolve, timestamp), 100); } async function ensureWindowIsOnScreen(w, screenId) { return new Promise(resolve => { waitForWindowOnScreen(w, screenId, resolve); }); } async function movePopupToScreen(p, screenId) { const screens = await getScreenDetailsWithWarningAndFallback(); if (!Number.isInteger(screenId) || screenId < 0 || screenId >= screens.length) screenId = 0; const s = screens[screenId]; // Fill the target screen if the window does so on the current screen, otherwise center. const fillError = 100; const popupScreenDetails = await p.getScreenDetails(); const popupScreen = popupScreenDetails.currentScreen; console.log("INFO: Moving popup to screen: " + screenId); if (Math.abs(p.outerWidth - popupScreen.availWidth) < fillError && Math.abs(p.outerHeight - popupScreen.availHeight) < fillError) { p.moveTo(s.availLeft, s.availTop); // Wait for the window to be moved to the target screen, and then resize it to fill. if (await ensureWindowIsOnScreen(p, screenId)) p.resizeTo(s.availWidth, s.availHeight); } else { const w = p.outerWidth; const h = p.outerHeight; // Compute coordinates centering the window on the target screen. const l = s.left + Math.round(s.width - w) / 2; const t = s.top + Math.round(s.height - h) / 2; p.moveTo(l, t); await ensureWindowIsOnScreen(p, screenId); } } // Make this window fullscreen on the popup's screen (or a fallback). // Also move the popup to another screen as needed to avoid being covered. async function fullscreenThisWindowAndMovePopup() { const screens = await getScreenDetailsWithWarningAndFallback(); // This function requires multiple screens, transient user activation, and a popup. if (screens.length < 2 || !navigator.userActivation.isActive || !popup || popup.closed) return; const popupScreenDetails = await popup.getScreenDetails(); const popupId = popupScreenDetails.screens.indexOf(popupScreenDetails.currentScreen); const fullscreenId = screenDetails.screens.indexOf(screenDetails.currentScreen); let fullscreenTargetId = popupId; // If this window is already fullscreen, make sure the request targets another screen. if (document.fullscreenElement && fullscreenTargetId == fullscreenId) fullscreenTargetId = (fullscreenTargetId + 1) % screenDetails.screens.length; const element = document.fullscreenElement ? document.fullscreenElement : document.documentElement; await toggleElementFullscreen(element, fullscreenTargetId); if (await ensureWindowIsOnScreen(window, fullscreenTargetId)) { // Move the popup to this window's previous screen, or the next available screen. const popupTargetId = (screens[fullscreenId] === screenDetails.currentScreen) ? ((fullscreenId + 1) % screens.length) : fullscreenId; await movePopupToScreen(popup, popupTargetId); } } async function delegateFullscreen(w, screenId) { return new Promise((resolve) => { console.log('INFO: Requesting to delegate a fullscreen request capability'); window.addEventListener('message', (messageEvent) => { if (typeof(messageEvent.data) === 'boolean') { console.log('INFO Target window reported fullscreen delegation result: ' + messageEvent.data); resolve(messageEvent.data); } }, { once: true }); w.postMessage({ capability: 'fullscreen', targetScreenId: screenId }, { targetOrigin: window.origin, delegate: 'fullscreen' }); }); } // Make the opener fullscreen on the target screen (or this popup's screen, or a fallback). // Also move this popup to another screen as needed to avoid being covered. async function fullscreenOpenerAndMoveThisPopup(screenId = null) { const screens = await getScreenDetailsWithWarningAndFallback(); // This function requires transient user activation, and an opener. if (!navigator.userActivation.isActive || !opener || opener.closed) return; // Make the opener fullscreen on the specified target screen, or this window's screen. if (!Number.isInteger(screenId) || screenId < 0 || screenId >= screens.length) screenId = screens.indexOf(screenDetails.currentScreen); const openerScreenDetails = await opener.getScreenDetails(); const openerId = openerScreenDetails.screens.indexOf(openerScreenDetails.currentScreen); // Delegate this window's transient user activation so the opener can request fullscreen. // Wait for the opener to actually move to the target screen if it reports successful delegation. if (await delegateFullscreen(opener, screenId) && await ensureWindowIsOnScreen(opener, screenId)) { // If the opener was made fullscreen on the same screen as this popup and there's another screen. if (screens[screenId] === screenDetails.currentScreen && screens.length > 1) { // Move this popup to the opener's previous screen, or the next available screen. const popupTargetId = (screens[openerId] === screenDetails.currentScreen) ? ((openerId + 1) % screens.length) : openerId; await movePopupToScreen(window, popupTargetId); } } }