forked from muxinc/media-chrome
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Larger refactor for fullscreen based on discussion with Wes.
- Loading branch information
1 parent
610b0d3
commit 9444718
Showing
3 changed files
with
184 additions
and
130 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,179 @@ | ||
import { WebkitPresentationModes } from '../constants.js'; | ||
import { containsComposedNode } from './element-utils.js'; | ||
import { document } from './server-safe-globals.js'; | ||
|
||
export const fullscreenApi = { | ||
enter: | ||
'requestFullscreen' in document | ||
// NOTE: (re)defining these types, but more narrowly for API expectations. These should probably be centralized + derived | ||
// once migrated to TypeScript types (CJP) | ||
|
||
/** | ||
* @typedef {Partial<HTMLVideoElement> & { | ||
* webkitDisplayingFullscreen?: boolean; | ||
* webkitPresentationMode?: 'fullscreen'|'picture-in-picture'; | ||
* webkitEnterFullscreen?: () => any; | ||
* }} MediaStateOwner | ||
*/ | ||
|
||
/** | ||
* @typedef {Partial<Document|ShadowRoot>} RootNodeStateOwner | ||
*/ | ||
|
||
/** | ||
* @typedef {Partial<HTMLElement>} FullScreenElementStateOwner | ||
*/ | ||
|
||
/** | ||
* @typedef {object} StateOwners | ||
* @property {MediaStateOwner} [media] | ||
* @property {RootNodeStateOwner} [documentElement] | ||
* @property {FullScreenElementStateOwner} [fullscreenElement] | ||
*/ | ||
|
||
/** @type {(stateOwners: StateOwners) => Promise<undefined> | undefined} */ | ||
export const enterFullscreen = (stateOwners) => { | ||
const { media, fullscreenElement } = stateOwners; | ||
|
||
// NOTE: Since the fullscreenElement can change and may be a web component, | ||
// we should not define this at the module level. As an optimization, | ||
// we could only define/update this somehow based on state owner changes. (CJP) | ||
const enterFullscreenKey = | ||
fullscreenElement && 'requestFullscreen' in fullscreenElement | ||
? 'requestFullscreen' | ||
: 'webkitRequestFullScreen' in document | ||
: fullscreenElement && 'webkitRequestFullScreen' in fullscreenElement | ||
? 'webkitRequestFullScreen' | ||
: undefined, | ||
exit: | ||
'exitFullscreen' in document | ||
? 'exitFullscreen' | ||
: 'webkitExitFullscreen' in document | ||
? 'webkitExitFullscreen' | ||
: 'webkitCancelFullScreen' in document | ||
? 'webkitCancelFullScreen' | ||
: undefined, | ||
element: | ||
'fullscreenElement' in document | ||
? 'fullscreenElement' | ||
: 'webkitFullscreenElement' in document | ||
? 'webkitFullscreenElement' | ||
: undefined, | ||
error: | ||
'fullscreenerror' in document | ||
? 'fullscreenerror' | ||
: 'webkitfullscreenerror' in document | ||
? 'webkitfullscreenerror' | ||
: undefined, | ||
enabled: | ||
'fullscreenEnabled' in document | ||
? 'fullscreenEnabled' | ||
: 'webkitFullscreenEnabled' in document | ||
? 'webkitFullscreenEnabled' | ||
: undefined, | ||
: undefined; | ||
|
||
// Entering fullscreen cases (browser-specific) | ||
if (enterFullscreenKey) { | ||
// NOTE: Since the "official" enter fullscreen method yields a Promise that rejects | ||
// if already in fullscreen, this accounts for those cases. | ||
const maybePromise = fullscreenElement[enterFullscreenKey]?.(); | ||
if (maybePromise instanceof Promise) { | ||
return maybePromise.catch(() => {}); | ||
} | ||
} else if (media?.webkitEnterFullscreen) { | ||
// Media element fullscreen using iOS API | ||
media.webkitEnterFullscreen(); | ||
} else if (media?.requestFullscreen) { | ||
// So media els don't have to implement multiple APIs. | ||
media.requestFullscreen(); | ||
} | ||
}; | ||
|
||
const exitFullscreenKey = | ||
'exitFullscreen' in document | ||
? 'exitFullscreen' | ||
: 'webkitExitFullscreen' in document | ||
? 'webkitExitFullscreen' | ||
: 'webkitCancelFullScreen' in document | ||
? 'webkitCancelFullScreen' | ||
: undefined; | ||
|
||
/** @type {(stateOwners: StateOwners) => Promise<undefined> | undefined} */ | ||
export const exitFullscreen = (stateOwners) => { | ||
const { documentElement } = stateOwners; | ||
|
||
// Exiting fullscreen case (generic) | ||
if (exitFullscreenKey) { | ||
const maybePromise = documentElement?.[exitFullscreenKey]?.(); | ||
// NOTE: Since the "official" exit fullscreen method yields a Promise that rejects | ||
// if not in fullscreen, this accounts for those cases. | ||
if (maybePromise instanceof Promise) { | ||
return maybePromise.catch(() => {}); | ||
} | ||
} | ||
}; | ||
|
||
const fullscreenElementKey = | ||
'fullscreenElement' in document | ||
? 'fullscreenElement' | ||
: 'webkitFullscreenElement' in document | ||
? 'webkitFullscreenElement' | ||
: undefined; | ||
|
||
/** @type {(stateOwners: StateOwners) => FullScreenElementStateOwner | null | undefined} */ | ||
export const getFullscreenElement = (stateOwners) => { | ||
const { documentElement, media } = stateOwners; | ||
const docFullscreenElement = documentElement?.[fullscreenElementKey]; | ||
if ( | ||
!docFullscreenElement && | ||
'webkitDisplayingFullscreen' in media && | ||
'webkitPresentationMode' in media && | ||
media.webkitDisplayingFullscreen && | ||
media.webkitPresentationMode === WebkitPresentationModes.FULLSCREEN | ||
) { | ||
return media; | ||
} | ||
return docFullscreenElement; | ||
}; | ||
|
||
/** @type {(stateOwners: StateOwners) => boolean} */ | ||
export const isFullscreen = (stateOwners) => { | ||
const { media, documentElement, fullscreenElement = media } = stateOwners; | ||
|
||
// Need a documentElement and a media StateOwner to be in fullscreen, so we're not fullscreen | ||
if (!media || !documentElement) return false; | ||
|
||
const currentFullscreenElement = getFullscreenElement(stateOwners); | ||
|
||
// If there is no current fullscreenElement, we're definitely not in fullscreen. | ||
if (!currentFullscreenElement) return false; | ||
|
||
// If documentElement.fullscreenElement is the media or fullscreenElement StateOwner, we're definitely in fullscreen | ||
if ( | ||
currentFullscreenElement === fullscreenElement || | ||
currentFullscreenElement === media | ||
) { | ||
return true; | ||
} | ||
|
||
// In this case (most modern browsers, sans e.g. iOS), the fullscreenElement may be | ||
// a web component that is "visible" from the documentElement, but should | ||
// have its own fullscreenElement on its shadowRoot for whatever | ||
// is "visible" at that level. Since the (also named) fullscreenElement StateOwner | ||
// may be nested inside an indeterminite number of web components, traverse each layer | ||
// until we either find the fullscreen StateOwner or complete the recursive check. | ||
if (currentFullscreenElement.localName.includes('-')) { | ||
let currentRoot = currentFullscreenElement.shadowRoot; | ||
|
||
// NOTE: This is for (non-iOS) Safari < 16.4, which did not support ShadowRoot::fullscreenElement. | ||
// We can remove this if/when we decide those versions are old enough/not used enough to handle | ||
// (e.g. at the time of writing, < 16.4 ~= 1% of global market, per caniuse https://caniuse.com/mdn-api_shadowroot_fullscreenelement) (CJP) | ||
|
||
// We can simply check if the fullscreenElement key (typically 'fullscreenElement') is defined on the shadowRoot to determine whether or not | ||
// it is supported. | ||
if (!(fullscreenElementKey in currentRoot)) { | ||
// For these cases, if documentElement.fullscreenElement (aka document.fullscreenElement) contains our fullscreenElement StateOwner, | ||
// we'll assume that means we're in fullscreen. That should be valid for all current actual and planned supported | ||
// web component use cases. | ||
return containsComposedNode( | ||
currentFullscreenElement, | ||
/** @TODO clean up type assumptions (e.g. Node) (CJP) */ | ||
// @ts-ignore | ||
fullscreenElement | ||
); | ||
} | ||
|
||
while (currentRoot?.[fullscreenElementKey]) { | ||
if (currentRoot[fullscreenElementKey] === fullscreenElement) return true; | ||
currentRoot = currentRoot[fullscreenElementKey]?.shadowRoot; | ||
} | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
const fullscreenEnabledKey = | ||
'fullscreenEnabled' in document | ||
? 'fullscreenEnabled' | ||
: 'webkitFullscreenEnabled' in document | ||
? 'webkitFullscreenEnabled' | ||
: undefined; | ||
|
||
/** @type {(stateOwners: StateOwners) => boolean} */ | ||
export const isFullscreenEnabled = (stateOwners) => { | ||
const { documentElement, media } = stateOwners; | ||
return ( | ||
!!documentElement?.[fullscreenEnabledKey] || | ||
(media && 'webkitSupportsFullscreen' in media) | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters