media-chrome
Version:
Custom elements (web components) for making audio and video player controls that look great in your website or app.
184 lines (158 loc) • 6.98 kB
text/typescript
import { WebkitPresentationModes } from '../constants.js';
import { containsComposedNode } from './element-utils.js';
import { document } from './server-safe-globals.js';
// 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;
try {
// 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'
: fullscreenElement && 'webkitRequestFullScreen' in fullscreenElement
? 'webkitRequestFullScreen'
: 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();
}
} catch (e) {
console.error(e);
}
};
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)
);
};