UNPKG

bigscreen

Version:

A simple library for using the JavaScript Fullscreen API.

436 lines (373 loc) 14.3 kB
// A library to make it easier to use the JavaScript Fullscreen API. (function(root, document, iframe) { 'use strict'; var iOS7 = /i(Pad|Phone|Pod)/.test(navigator.userAgent) && parseInt(navigator.userAgent.replace(/^.*OS (\d+)_(\d+).*$/, '$1.$2'), 10) >= 7; var fn = (function() { var testElement = document.createElement('video'); var browserProperties = { request: ['requestFullscreen', 'webkitRequestFullscreen', 'webkitRequestFullScreen', 'mozRequestFullScreen', 'msRequestFullscreen'], exit: ['exitFullscreen', 'webkitCancelFullScreen', 'webkitExitFullscreen', 'mozCancelFullScreen', 'msExitFullscreen'], enabled: ['fullscreenEnabled', 'webkitFullscreenEnabled', 'mozFullScreenEnabled', 'msFullscreenEnabled'], element: ['fullscreenElement', 'webkitFullscreenElement', 'webkitCurrentFullScreenElement', 'mozFullScreenElement', 'msFullscreenElement'], change: ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], error: ['fullscreenerror', 'webkitfullscreenerror', 'mozfullscreenerror', 'MSFullscreenError'] }; var properties = {}; // Loop thorugh each property/event/function and find the ones that work // in this browser. for (var prop in browserProperties) { for (var i = 0, length = browserProperties[prop].length; i < length; i++) { if (browserProperties[prop][i] in testElement || browserProperties[prop][i] in document || 'on' + browserProperties[prop][i].toLowerCase() in document) { properties[prop] = browserProperties[prop][i]; break; } } } return properties; }()); // Find a child video in the element passed. function _getVideo(element) { var videoElement = null; if (element.tagName === 'VIDEO') { videoElement = element; } else { var videos = element.getElementsByTagName('video'); if (videos[0]) { videoElement = videos[0]; } } return videoElement; } var lastVideoElement = null; var hasControls = null; var emptyFunction = function() {}; var elements = []; var chromeAndroid = false; if (navigator.userAgent.indexOf('Android') > -1 && navigator.userAgent.indexOf('Chrome') > -1) { chromeAndroid = parseInt(navigator.userAgent.replace(/^.*Chrome\/(\d+).*$/, '$1'), 10) || true; } // Attempt to put a child video into full screen using webkitEnterFullscreen. // The metadata must be loaded in order for it to work, so load it automatically // if it isn't already. function videoEnterFullscreen(element) { var videoElement = _getVideo(element); if (videoElement && videoElement.webkitEnterFullscreen) { try { if (videoElement.readyState < videoElement.HAVE_METADATA) { videoElement.addEventListener('loadedmetadata', function onMetadataLoaded() { videoElement.removeEventListener('loadedmetadata', onMetadataLoaded, false); videoElement.webkitEnterFullscreen(); hasControls = !!videoElement.getAttribute('controls'); }, false); videoElement.load(); } else { videoElement.webkitEnterFullscreen(); hasControls = !!videoElement.getAttribute('controls'); } lastVideoElement = videoElement; } catch (err) { return callOnError('not_supported', element); } return true; } return callOnError(fn.request === undefined ? 'not_supported' : 'not_enabled', element); } // There is a bug in older versions of WebKit that will fire `webkitfullscreenchange` twice when // entering full screen from inside an iframe, and won't fire it when exiting. We can listen for // a resize event once we enter to tell when it returns to normal size (and thus has exited full // screen). See the [Safari bug](rdar://11927884). function resizeExitHack() { if (!bigscreen.element) { callOnExit(); removeWindowResizeHack(); } } // Add the listener for the resize hack, but only when inside an iframe in WebKit. function addWindowResizeHack() { if (iframe && fn.change === 'webkitfullscreenchange') { window.addEventListener('resize', resizeExitHack, false); } } function removeWindowResizeHack() { if (iframe && fn.change === 'webkitfullscreenchange') { window.removeEventListener('resize', resizeExitHack, false); } } var callOnEnter = function(actualElement) { // Return if the element entering has actually entered already. In older WebKit versions the // browser will fire 2 `webkitfullscreenchange` events when entering full screen from inside an // iframe. This is the result of the same bug as the resizeExitHack. var lastElement = elements[elements.length - 1]; if (!lastElement) { return; } if ((actualElement === lastElement.element || actualElement === lastVideoElement) && lastElement.hasEntered) { return; } // If the element is a video, store it here for the enabled check. if (actualElement.tagName === 'VIDEO') { lastVideoElement = actualElement; } // Call the global enter handler only if this is the first element. if (elements.length === 1) { bigscreen.onenter(bigscreen.element); } // Call the stored callback for the request call and record that we did so we don't do it // again if there is a duplicate call (see above). lastElement.enter.call(lastElement.element, actualElement || lastElement.element); lastElement.hasEntered = true; }; var callOnExit = function() { // Fix a bug present in some versions of WebKit that will show the native controls when // exiting, even if they were not showing before. In iOS 7, this actually causes the // native controls to show up, although once they hide they stay hidden. if (lastVideoElement && !hasControls && !iOS7) { lastVideoElement.setAttribute('controls', 'controls'); lastVideoElement.removeAttribute('controls'); } lastVideoElement = null; hasControls = null; var element = elements.pop(); // Check to make sure that the element exists. This function will get called a second // time from the iframe resize hack. if (element) { element.exit.call(element.element); // When the browser has fully exited full screen, make sure to loop // through and call the rest of the callbacks and then the global exit. if (!bigscreen.element) { elements.forEach(function(el) { el.exit.call(el.element); }); elements = []; bigscreen.onexit(); } } }; // Make a callback to the error handlers and clear the element from the stack when // an error occurs. var callOnError = function(reason, element) { if (elements.length > 0) { var obj = elements.pop(); element = element || obj.element; obj.error.call(element, reason); bigscreen.onerror(element, reason); } }; var bigscreen = { // ### request // The meat of BigScreen is here. Run through a bunch of checks to try to get // something into full screen that's a child of the element passed in. request: function(element, enterCallback, exitCallback, errorCallback) { element = element || document.body; elements.push({ element: element, enter: enterCallback || emptyFunction, exit: exitCallback || emptyFunction, error: errorCallback || emptyFunction }); // iOS only supports webkitEnterFullscreen on videos, so try that. // Browsers that don't support full screen at all will also go through this, // but they will fire an error. if (fn.request === undefined) { videoEnterFullscreen(element); return; } // `document.fullscreenEnabled` defined, but is `false`, so try a video if there is one. if (iframe && document[fn.enabled] === false) { videoEnterFullscreen(element); return; } // Chrome on Android reports that fullscreen is enabled, but it isn't really on < 32. if (chromeAndroid !== false && chromeAndroid < 32) { videoEnterFullscreen(element); return; } // If we're in an iframe, it needs to have the `allowfullscreen` attribute in order for element full screen // to work. Safari 5.1 supports element full screen, but doesn't have `document.webkitFullScreenEnabled`, // so the only way to tell if it will work is to just try it. if (iframe && fn.enabled === undefined) { fn.enabled = 'webkitFullscreenEnabled'; element[fn.request](); setTimeout(function() { // It didn't work, so set `webkitFullscreenEnabled` to false so we don't // have to try again next time. Then try to fall back to video full screen. if (!document[fn.element]) { document[fn.enabled] = false; videoEnterFullscreen(element); } // It worked! set `webkitFullscreenEnabled` so we know next time. else { document[fn.enabled] = true; } }, 250); return; } try { element[fn.request](); // If there's no element after 100ms, it didn't work. This check is for Safari 5.1 // which fails to fire a `webkitfullscreenerror` if the request wasn't from a user // action. setTimeout(function() { if (!document[fn.element]) { callOnError(iframe ? 'not_enabled' : 'not_allowed', element); } }, 100); } catch (err) { callOnError('not_enabled', element); } }, // ### exit // Pops the last full screen element off the stack. exit: function() { // Remove the resize hack here if exit is called manually, so it doesn't fire twice. removeWindowResizeHack(); document[fn.exit](); }, // ### toggle // Shortcut function if you only plan on putting one element into full screen. toggle: function(element, enterCallback, exitCallback, errorCallback) { if (bigscreen.element) { bigscreen.exit(); } else { bigscreen.request(element, enterCallback, exitCallback, errorCallback); } }, // ### videoEnabled // Mobile Safari and earlier versions of desktop Safari support sending a `<video>` into full screen, // even if the `allowfullscreen` attribute isn't present on the iframe. Checks can't be performed to // verify full screen capabilities unless we know about that element, and it has loaded its metadata. videoEnabled: function(element) { if (bigscreen.enabled) { return true; } element = element || document.body; var video = _getVideo(element); if (!video || video.webkitSupportsFullscreen === undefined) { return false; } return video.readyState < video.HAVE_METADATA ? 'maybe' : video.webkitSupportsFullscreen; }, // ### onenter, onexit, onchange, onerror // Populate the global handlers with empty functions. onenter: emptyFunction, onexit: emptyFunction, onchange: emptyFunction, onerror: emptyFunction }; // Define the two properties `element` and `enabled` with getters. try { Object.defineProperties(bigscreen, { // ### element // Get the current element that is displaying full screen. element: { enumerable: true, get: function() { if (lastVideoElement && lastVideoElement.webkitDisplayingFullscreen) { return lastVideoElement; } return document[fn.element] || null; } }, // ### enabled // Check if element full screen is supported. enabled: { enumerable: true, get: function() { // Safari 5.1 supports full screen, but doesn't have a fullScreenEnabled property, // but it should work if not in an iframe. If it doesn't work when tried for the // first time, we'll set this to `false` then. if (fn.exit === 'webkitCancelFullScreen' && !iframe) { return true; } // Chrome on Android reports that fullscreen is enabled, but it isn't really. if (chromeAndroid !== false && chromeAndroid < 32) { return false; } return document[fn.enabled] || false; } } }); // If there is a valid `fullscreenchange` event, set up the listener for it. if (fn.change) { document.addEventListener(fn.change, function onFullscreenChange(event) { bigscreen.onchange(bigscreen.element); if (bigscreen.element) { // This should be treated an exit if the element that is in full screen // is the previous element in our stack. var previousElement = elements[elements.length - 2]; if (previousElement && previousElement.element === bigscreen.element) { callOnExit(); } else { callOnEnter(bigscreen.element); addWindowResizeHack(); } } else { callOnExit(); } }, false); } // Listen for the video-only fullscreen events. Only applies to mobile browsers. // Desktop Safari and Chrome will fire the normal `fullscreenchange` event instead. // Use the capture phase because that seems to be the only way to get them. document.addEventListener('webkitbeginfullscreen', function onBeginFullscreen(event) { var shouldPushElement = true; // When BigScreen.request is called specifically, the element requested // is already pushed onto the stack. If the video element belongs to an // element on the stack, don't push it on here. if (elements.length > 0) { for (var i = 0, length = elements.length; i < length; i++) { var video = _getVideo(elements[i].element); if (video === event.srcElement) { shouldPushElement = false; break; } } } if (shouldPushElement) { elements.push({ element: event.srcElement, enter: emptyFunction, exit: emptyFunction, error: emptyFunction }); } bigscreen.onchange(event.srcElement); callOnEnter(event.srcElement); }, true); document.addEventListener('webkitendfullscreen', function onEndFullscreen(event) { bigscreen.onchange(event.srcElement); callOnExit(event.srcElement); }, true); // If there is a valid `fullscreenerror` event, set up the listener for it. if (fn.error) { document.addEventListener(fn.error, function onFullscreenError(event) { callOnError('not_allowed'); }, false); } } // If the define fails, set them to `null` and `false`, respectively. catch (err) { bigscreen.element = null; bigscreen.enabled = false; } /* eslint-disable no-undef */ if (typeof define === 'function' && define.amd) { define(function() { return bigscreen; }); } else if (typeof module !== 'undefined' && module.exports) { module.exports = bigscreen; } else { root.BigScreen = bigscreen; } /* eslint-enable no-undef */ }(this, document, self !== top));