Repository: appleple/react-modal-video Branch: master Commit: 237250e30c26 Files: 16 Total size: 39.6 KB Directory structure: gitextract_so3v8z3u/ ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .node-version ├── LICENSE ├── babel.config.js ├── circle.yml ├── css/ │ └── modal-video.css ├── lib/ │ └── index.js ├── nodemon.json ├── package.json ├── readme.md ├── scss/ │ └── modal-video.scss ├── src/ │ └── index.jsx └── test/ ├── index.html └── src/ └── index.jsx ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*.js] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.html] # すべてのファイルに適用する charset = utf-8 # 文字コードを統一 indent_style = tab #インデントを統一する。「tab」か「 space」 indent_size = 2 # インデントの数を統一 trim_trailing_whitespace = true # 行末のホワイトスペースを削除 insert_final_newline = true # フォルダの最後の行に改行 end_of_line = lf ================================================ FILE: .eslintrc ================================================ { "env": { "browser": true }, "globals": { "document": true, "window": true }, "rules":{ "comma-dangle":0 }, "parserOptions": { "sourceType": "module", "ecmaVersion": 2015, "ecmaFeatures": { "jsx": true } } } ================================================ FILE: .gitignore ================================================ node_modules/* yarn.lock .DS_Store .idea test/dist ================================================ FILE: .node-version ================================================ 18.12.1 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 appleple Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: babel.config.js ================================================ module.exports = { presets: [ ['@babel/preset-env', { targets: { ie: 11, }, useBuiltIns: 'usage', corejs: 3, }, ], ['@babel/preset-react'], ] }; ================================================ FILE: circle.yml ================================================ machine: node: version: 6.2.0 dependencies: override: - "npm install" test: override: - "npm run test" ================================================ FILE: css/modal-video.css ================================================ @keyframes modal-video { from { opacity: 0; } to { opacity: 1; } } @keyframes modal-video-inner { from { transform: translate(0, 100px); } to { transform: translate(0, 0); } } .modal-video { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1000000; cursor: pointer; opacity: 1; animation-timing-function: ease-out; animation-duration: 0.3s; animation-name: modal-video; -webkit-transition: opacity 0.3s ease-out; -moz-transition: opacity 0.3s ease-out; -ms-transition: opacity 0.3s ease-out; -o-transition: opacity 0.3s ease-out; transition: opacity 0.3s ease-out; } .modal-video-effect-exit { opacity: 0; } .modal-video-effect-exit .modal-video-movie-wrap { -webkit-transform: translate(0, 100px); -moz-transform: translate(0, 100px); -ms-transform: translate(0, 100px); -o-transform: translate(0, 100px); transform: translate(0, 100px); } .modal-video-body { max-width: 960px; width: 100%; height: 100%; margin: 0 auto; padding: 0 10px; display: flex; justify-content: center; box-sizing: border-box; } .modal-video-inner { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; } @media (orientation: landscape) { .modal-video-inner { padding: 10px 60px; box-sizing: border-box; } } .modal-video-movie-wrap { width: 100%; height: 0; position: relative; padding-bottom: 56.25%; background-color: #333; animation-timing-function: ease-out; animation-duration: 0.3s; animation-name: modal-video-inner; -webkit-transform: translate(0, 0); -moz-transform: translate(0, 0); -ms-transform: translate(0, 0); -o-transform: translate(0, 0); transform: translate(0, 0); -webkit-transition: -webkit-transform 0.3s ease-out; -moz-transition: -moz-transform 0.3s ease-out; -ms-transition: -ms-transform 0.3s ease-out; -o-transition: -o-transform 0.3s ease-out; transition: transform 0.3s ease-out; } .modal-video-movie-wrap iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .modal-video-close-btn { position: absolute; z-index: 2; top: -45px; right: 0px; display: inline-block; width: 35px; height: 35px; overflow: hidden; border: none; background: transparent; } @media (orientation: landscape) { .modal-video-close-btn { top: 0; right: -45px; } } .modal-video-close-btn:before { transform: rotate(45deg); } .modal-video-close-btn:after { transform: rotate(-45deg); } .modal-video-close-btn:before, .modal-video-close-btn:after { content: ""; position: absolute; height: 2px; width: 100%; top: 50%; left: 0; margin-top: -1px; background: #fff; border-radius: 5px; margin-top: -6px; } ================================================ FILE: lib/index.js ================================================ "use strict"; function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } require("core-js/modules/es.object.to-string.js"); require("core-js/modules/es.reflect.construct.js"); require("core-js/modules/es.symbol.to-primitive.js"); require("core-js/modules/es.date.to-primitive.js"); require("core-js/modules/es.symbol.js"); require("core-js/modules/es.symbol.description.js"); require("core-js/modules/es.symbol.iterator.js"); require("core-js/modules/es.array.iterator.js"); require("core-js/modules/es.string.iterator.js"); require("core-js/modules/web.dom-collections.iterator.js"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; require("core-js/modules/es.array.concat.js"); require("core-js/modules/es.number.constructor.js"); require("core-js/modules/es.object.get-prototype-of.js"); var _react = _interopRequireDefault(require("react")); var _CSSTransition = _interopRequireDefault(require("react-transition-group/CSSTransition")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } var ModalVideo = /*#__PURE__*/function (_React$Component) { _inherits(ModalVideo, _React$Component); var _super = _createSuper(ModalVideo); function ModalVideo(props) { var _this; _classCallCheck(this, ModalVideo); _this = _super.call(this, props); _this.state = { isOpen: false, modalVideoWidth: '100%' }; _this.closeModal = _this.closeModal.bind(_assertThisInitialized(_this)); _this.updateFocus = _this.updateFocus.bind(_assertThisInitialized(_this)); _this.timeout; // used for resizing video. return _this; } _createClass(ModalVideo, [{ key: "openModal", value: function openModal() { this.setState({ isOpen: true }); } }, { key: "closeModal", value: function closeModal() { this.setState({ isOpen: false }); if (typeof this.props.onClose === 'function') { this.props.onClose(); } } }, { key: "keydownHandler", value: function keydownHandler(e) { if (e.keyCode === 27) { this.closeModal(); } } }, { key: "componentDidMount", value: function componentDidMount() { document.addEventListener('keydown', this.keydownHandler.bind(this)); window.addEventListener('resize', this.resizeModalVideoWhenHeightGreaterThanWindowHeight.bind(this)); this.setState({ modalVideoWidth: this.getWidthFulfillAspectRatio(this.props.ratio, window.innerHeight, window.innerWidth) }); } }, { key: "componentWillUnmount", value: function componentWillUnmount() { document.removeEventListener('keydown', this.keydownHandler.bind(this)); window.removeEventListener('resize', this.resizeModalVideoWhenHeightGreaterThanWindowHeight.bind(this)); } }, { key: "componentDidUpdate", value: function componentDidUpdate() { if (this.state.isOpen && this.modal) { this.modal.focus(); } } }, { key: "updateFocus", value: function updateFocus(e) { if (this.state.isOpen) { e.preventDefault(); e.stopPropagation(); if (e.keyCode === 9) { if (this.modal === document.activeElement) { this.modaliflame.focus(); } else if (this.modalbtn === document.activeElement) { this.modal.focus(); } } } } /** * Resize modal-video-iframe-wrap when window size changed when the height of the video is greater than the height of the window. */ }, { key: "resizeModalVideoWhenHeightGreaterThanWindowHeight", value: function resizeModalVideoWhenHeightGreaterThanWindowHeight() { var _this2 = this; clearTimeout(this.timeout); this.timeout = setTimeout(function () { var width = _this2.getWidthFulfillAspectRatio(_this2.props.ratio, window.innerHeight, window.innerWidth); if (_this2.state.modalVideoWidth != width) { _this2.setState({ modalVideoWidth: width }); } }, 10); } }, { key: "getQueryString", value: function getQueryString(obj) { var url = ''; for (var key in obj) { if (obj.hasOwnProperty(key)) { if (obj[key] !== null) { url += "".concat(key, "=").concat(obj[key], "&"); } } } return url.substr(0, url.length - 1); } }, { key: "getYoutubeUrl", value: function getYoutubeUrl(youtube, videoId) { var query = this.getQueryString(youtube); return "//www.youtube.com/embed/".concat(videoId, "?").concat(query); } }, { key: "getVimeoUrl", value: function getVimeoUrl(vimeo, videoId) { var query = this.getQueryString(vimeo); return "//player.vimeo.com/video/".concat(videoId, "?").concat(query); } }, { key: "getYoukuUrl", value: function getYoukuUrl(youku, videoId) { var query = this.getQueryString(youku); return "//player.youku.com/embed/".concat(videoId, "?").concat(query); } }, { key: "getVideoUrl", value: function getVideoUrl(opt, videoId) { if (opt.channel === 'youtube') { return this.getYoutubeUrl(opt.youtube, videoId); } if (opt.channel === 'vimeo') { return this.getVimeoUrl(opt.vimeo, videoId); } if (opt.channel === 'youku') { return this.getYoukuUrl(opt.youku, videoId); } if (opt.channel === 'custom') { return opt.url; } } }, { key: "getPadding", value: function getPadding(ratio) { var arr = ratio.split(':'); var width = Number(arr[0]); var height = Number(arr[1]); var padding = height * 100 / width; return "".concat(padding, "%"); } /** * Calculate the width of the video fulfill aspect ratio. * When the height of the video is greater than the height of the window, * this function return the width that fulfill the aspect ratio for the height of the window. * In other cases, this function return '100%'(the height relative to the width is determined by css). * * @param string ratio * @param number maxWidth * @returns number | '100%' */ }, { key: "getWidthFulfillAspectRatio", value: function getWidthFulfillAspectRatio(ratio, maxHeight, maxWidth) { var arr = ratio.split(':'); var width = Number(arr[0]); var height = Number(arr[1]); // Height that fulfill the aspect ratio for maxWidth. var videoHeight = maxWidth * (height / width); if (maxHeight < videoHeight) { // when the height of the video is greater than the height of the window. // calculate the width that fulfill the aspect ratio for the height of the window. // ex: 16:9 aspect ratio // 16:9 = width : height // → width = 16 / 9 * height return Math.floor(width / height * maxHeight); } return '100%'; } }, { key: "render", value: function render() { var _this3 = this; var modalVideoInnerStyle = { width: this.state.modalVideoWidth }; var modalVideoIframeWrapStyle = { paddingBottom: this.getPadding(this.props.ratio) }; return /*#__PURE__*/_react.default.createElement(_CSSTransition.default, { classNames: this.props.classNames.modalVideoEffect, timeout: this.props.animationSpeed }, function () { if (!_this3.state.isOpen) { return null; } return /*#__PURE__*/_react.default.createElement("div", { className: _this3.props.classNames.modalVideo, tabIndex: "-1", role: "dialog", "area-modal": "true", "aria-label": _this3.props.aria.openMessage, onClick: _this3.closeModal, ref: function ref(node) { _this3.modal = node; }, onKeyDown: _this3.updateFocus }, /*#__PURE__*/_react.default.createElement("div", { className: _this3.props.classNames.modalVideoBody }, /*#__PURE__*/_react.default.createElement("div", { className: _this3.props.classNames.modalVideoInner, style: modalVideoInnerStyle }, /*#__PURE__*/_react.default.createElement("div", { className: _this3.props.classNames.modalVideoIframeWrap, style: modalVideoIframeWrapStyle }, _this3.props.children || /*#__PURE__*/_react.default.createElement("iframe", { width: "460", height: "230", src: _this3.getVideoUrl(_this3.props, _this3.props.videoId), frameBorder: "0", allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', allowFullScreen: _this3.props.allowFullScreen, onKeyDown: _this3.updateFocus, ref: function ref(node) { _this3.modaliflame = node; }, tabIndex: "-1" }), /*#__PURE__*/_react.default.createElement("button", { className: _this3.props.classNames.modalVideoCloseBtn, "aria-label": _this3.props.aria.dismissBtnMessage, ref: function ref(node) { _this3.modalbtn = node; }, onKeyDown: _this3.updateFocus }))))); }); } }], [{ key: "getDerivedStateFromProps", value: function getDerivedStateFromProps(props) { return { isOpen: props.isOpen }; } }]); return ModalVideo; }(_react.default.Component); exports.default = ModalVideo; ModalVideo.defaultProps = { channel: 'youtube', isOpen: false, youtube: { autoplay: 1, cc_load_policy: 1, color: null, controls: 1, disablekb: 0, enablejsapi: 0, end: null, fs: 1, h1: null, iv_load_policy: 1, list: null, listType: null, loop: 0, modestbranding: null, origin: null, playlist: null, playsinline: null, rel: 0, showinfo: 1, start: 0, wmode: 'transparent', theme: 'dark', mute: 0 }, ratio: '16:9', vimeo: { api: false, autopause: true, autoplay: true, byline: true, callback: null, color: null, height: null, loop: false, maxheight: null, maxwidth: null, player_id: null, portrait: true, title: true, width: null, xhtml: false }, youku: { autoplay: 1, show_related: 0 }, allowFullScreen: true, animationSpeed: 300, classNames: { modalVideoEffect: 'modal-video-effect', modalVideo: 'modal-video', modalVideoClose: 'modal-video-close', modalVideoBody: 'modal-video-body', modalVideoInner: 'modal-video-inner', modalVideoIframeWrap: 'modal-video-movie-wrap', modalVideoCloseBtn: 'modal-video-close-btn' }, aria: { openMessage: 'You just opened the modal video', dismissBtnMessage: 'Close the modal by clicking here' } }; ================================================ FILE: nodemon.json ================================================ { "execMap": { "js": "node", "jsx": "jsx {{filename}} | node" }, "ext": "jsx scss", "ignore": [ "test/dist", "node_modules", "lib" ], "verbose": true } ================================================ FILE: package.json ================================================ { "name": "react-modal-video", "version": "2.0.2", "main": "lib/index.js", "description": "Modal Video Viewer", "author": "appleple", "license": "MIT", "scripts": { "test": "eslint ./src/index.jsx --fix", "build:js": "npm-run-all -p build:lib build:test", "build:lib": "npm run babel", "build:test": "browserify ./test/src/index.jsx -t babelify -o ./test/dist/index.js", "build:sass": "npm-run-all -p sass sass:min", "babel": "babel src --out-dir lib", "sass": "sass ./scss/modal-video.scss ./css/modal-video.css --style expanded --no-source-map", "sass:min": "sass ./scss/modal-video.scss ./css/modal-video.min.css --style compressed --no-source-map", "watch:js": "onchange \"src/\" -- npm run build:js", "watch:sass": "onchange \"scss\" -- npm run build:sass", "watch:test": "onchange \"test/src\" -- npm run build:test", "sync": "browser-sync start --server './' --files './test/dist/*.js' './css/*.css' --startPath '/test/index.html'", "start": "npm-run-all -p watch:js watch:sass watch:test sync", "deploy": "np --no-cleanup" }, "repository": { "type": "git", "url": "https://github.com/appleple/react-modal-video.git" }, "devDependencies": { "@babel/cli": "^7.17.10", "@babel/core": "^7.18.5", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "babelify": "^10.0.0", "browser-sync": "^2.27.10", "browserify": "^17.0.0", "eslint": "^8.17.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.26.0", "npm-run-all": "^4.1.5", "sass": "^1.52.3", "onchange": "^7.1.0", "np": "^7.6.1" }, "dependencies": { "core-js": "^3.27.2", "react-transition-group": "^4.4.2" }, "peerDependencies": { "react": "^17.0.0 || ^18.2.0", "react-dom": "^17.0.0 || ^18.2.0" } } ================================================ FILE: readme.md ================================================ # react-modal-video React Modal Video Component ## Features - Not affected by dom structure. - Beautiful transition - Accessible for keyboard navigation and screen readers. - Rich options for youtube API and Vimeo API ## Demo [https://unpkg.com/react-modal-video@latest/test/index.html](https://unpkg.com/react-modal-video@latest/test/index.html) ## Install ### npm ```sh npm install react-modal-video ``` ## Usage import sass file to your project ```scss @import 'node_modules/react-modal-video/scss/modal-video.scss'; ``` ### Functional Implementation with Hooks ```jsx import React, { useState } from 'react'; import ReactDOM from 'react-dom'; import ModalVideo from 'react-modal-video'; const App = () => { const [isOpen, setOpen] = useState(false); return ( setOpen(false)} /> ); }; ReactDOM.render(, document.getElementById('root')); ``` ### Class Implementation change "isOpen" property to open and close the modal-video ```jsx import React from 'react'; import ReactDOM from 'react-dom'; import ModalVideo from 'react-modal-video'; class App extends React.Component { constructor() { super(); this.state = { isOpen: false, }; this.openModal = this.openModal.bind(this); } openModal() { this.setState({ isOpen: true }); } render() { return ( this.setState({ isOpen: false })} /> ); } } ReactDOM.render(, document.getElementById('root')); ``` ## Options - About YouTube options, please refer to https://developers.google.com/youtube/player_parameters?hl=en - About Vimeo options, please refer to https://developer.vimeo.com/apis/oembed
properties default
channel 'youtube'
youtube autoplay 1
cc_load_policy 1
color null
controls 1
disablekb 0
enablejsapi 0
end null
fs 1
h1 null
iv_load_policy 1
list null
listType null
loop 0
modestbranding null
origin null
playlist null
playsinline null
rel 0
showinfo 1
start 0
wmode 'transparent'
theme 'dark'
mute 0
vimeo api false
autopause true
autoplay true
byline true
callback null
color null
height null
loop false
maxheight null
maxwidth null
player_id null
portrait true
title true
width null
xhtml false
youku autoplay 1
show_related 0
custom url MP4 URL / iframe URL
ratio '16:9'
allowFullScreen true
animationSpeed 300
classNames modalVideo 'modal-video'
modalVideoClose 'modal-video-close'
modalVideoBody 'modal-video-body'
modalVideoInner 'modal-video-inner'
modalVideoIframeWrap 'modal-video-movie-wrap'
modalVideoCloseBtn 'modal-video-close-btn'
aria openMessage 'You just opened the modal video'
dismissBtnMessage 'Close the modal by clicking here'
## FAQ ### How to track YouTube videos playing in modal-video by GA4? 1. Enable JS API. Turn `enablejsapi` property to `1`. 2. Load YouTube Iframe API. Add `` in HTML file. ## Licence [MIT](https://github.com/appleple/modal-video.js/blob/master/LICENSE) ================================================ FILE: scss/modal-video.scss ================================================ $animation-speed: .3s; $animation-function: ease-out; $backdrop-color: rgba(0, 0, 0, .5); @keyframes modal-video { from { opacity: 0; } to { opacity: 1; } } @keyframes modal-video-inner { from { transform: translate(0, 100px); } to { transform: translate(0, 0); } } .modal-video { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: $backdrop-color; z-index: 1000000; cursor: pointer; opacity: 1; animation-timing-function: $animation-function; animation-duration: $animation-speed; animation-name: modal-video; -webkit-transition: opacity $animation-speed $animation-function; -moz-transition: opacity $animation-speed $animation-function; -ms-transition: opacity $animation-speed $animation-function; -o-transition: opacity $animation-speed $animation-function; transition: opacity $animation-speed $animation-function; } .modal-video-effect-exit { opacity: 0; & .modal-video-movie-wrap { -webkit-transform: translate(0, 100px); -moz-transform: translate(0, 100px); -ms-transform: translate(0, 100px); -o-transform: translate(0, 100px); transform: translate(0, 100px); } } .modal-video-body { max-width: 960px; width: 100%; height: 100%; margin: 0 auto; padding: 0 10px; display: flex; justify-content: center; box-sizing: border-box; } .modal-video-inner { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; @media (orientation: landscape) { padding: 10px 60px; box-sizing: border-box; } } .modal-video-movie-wrap { width: 100%; height: 0; position: relative; padding-bottom: 56.25%; background-color: #333; animation-timing-function: $animation-function; animation-duration: $animation-speed; animation-name: modal-video-inner; -webkit-transform: translate(0, 0); -moz-transform: translate(0, 0); -ms-transform: translate(0, 0); -o-transform: translate(0, 0); transform: translate(0, 0); -webkit-transition: -webkit-transform $animation-speed $animation-function; -moz-transition: -moz-transform $animation-speed $animation-function; -ms-transition: -ms-transform $animation-speed $animation-function; -o-transition: -o-transform $animation-speed $animation-function; transition: transform $animation-speed $animation-function; & iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } } .modal-video-close-btn { position: absolute; z-index: 2; top: -45px; right: 0px; display: inline-block; width: 35px; height: 35px; overflow: hidden; border: none; background: transparent; @media (orientation: landscape) { top: 0; right: -45px; } &:before { transform: rotate(45deg); } &:after { transform: rotate(-45deg); } &:before, &:after { content: ''; position: absolute; height: 2px; width: 100%; top: 50%; left: 0; margin-top: -1px; background: #fff; border-radius: 5px; margin-top: -6px; } } ================================================ FILE: src/index.jsx ================================================ import React from 'react'; import CSSTransition from 'react-transition-group/CSSTransition'; export default class ModalVideo extends React.Component { constructor(props) { super(props); this.state = { isOpen: false, modalVideoWidth: '100%' }; this.closeModal = this.closeModal.bind(this); this.updateFocus = this.updateFocus.bind(this); this.timeout; // used for resizing video. } openModal() { this.setState({ isOpen: true }); } closeModal() { this.setState({ isOpen: false }); if (typeof this.props.onClose === 'function') { this.props.onClose(); } } keydownHandler(e) { if (e.keyCode === 27) { this.closeModal(); } } componentDidMount() { document.addEventListener('keydown', this.keydownHandler.bind(this)); window.addEventListener('resize', this.resizeModalVideoWhenHeightGreaterThanWindowHeight.bind(this)); this.setState({ modalVideoWidth: this.getWidthFulfillAspectRatio(this.props.ratio, window.innerHeight, window.innerWidth) }); } componentWillUnmount() { document.removeEventListener('keydown', this.keydownHandler.bind(this)); window.removeEventListener('resize', this.resizeModalVideoWhenHeightGreaterThanWindowHeight.bind(this)); } static getDerivedStateFromProps(props) { return { isOpen: props.isOpen }; } componentDidUpdate() { if (this.state.isOpen && this.modal) { this.modal.focus(); } } updateFocus(e) { if (this.state.isOpen) { e.preventDefault(); e.stopPropagation(); if (e.keyCode === 9) { if (this.modal === document.activeElement) { this.modaliflame.focus(); } else if (this.modalbtn === document.activeElement) { this.modal.focus(); } } } } /** * Resize modal-video-iframe-wrap when window size changed when the height of the video is greater than the height of the window. */ resizeModalVideoWhenHeightGreaterThanWindowHeight() { clearTimeout(this.timeout); this.timeout = setTimeout(() => { const width = this.getWidthFulfillAspectRatio(this.props.ratio, window.innerHeight, window.innerWidth); if (this.state.modalVideoWidth != width) { this.setState({ modalVideoWidth: width }); } }, 10); } getQueryString(obj) { let url = ''; for (const key in obj) { if (obj.hasOwnProperty(key)) { if (obj[key] !== null) { url += `${key}=${obj[key]}&`; } } } return url.substr(0, url.length - 1); } getYoutubeUrl(youtube, videoId) { const query = this.getQueryString(youtube); return `//www.youtube.com/embed/${videoId}?${query}`; } getVimeoUrl(vimeo, videoId) { const query = this.getQueryString(vimeo); return `//player.vimeo.com/video/${videoId}?${query}`; } getYoukuUrl(youku, videoId) { const query = this.getQueryString(youku); return `//player.youku.com/embed/${videoId}?${query}`; } getVideoUrl(opt, videoId) { if (opt.channel === 'youtube') { return this.getYoutubeUrl(opt.youtube, videoId); } if (opt.channel === 'vimeo') { return this.getVimeoUrl(opt.vimeo, videoId); } if (opt.channel === 'youku') { return this.getYoukuUrl(opt.youku, videoId); } if (opt.channel === 'custom') { return opt.url; } } getPadding(ratio) { const arr = ratio.split(':'); const width = Number(arr[0]); const height = Number(arr[1]); const padding = height * 100 / width; return `${padding}%`; } /** * Calculate the width of the video fulfill aspect ratio. * When the height of the video is greater than the height of the window, * this function return the width that fulfill the aspect ratio for the height of the window. * In other cases, this function return '100%'(the height relative to the width is determined by css). * * @param string ratio * @param number maxWidth * @returns number | '100%' */ getWidthFulfillAspectRatio(ratio, maxHeight, maxWidth) { const arr = ratio.split(':'); const width = Number(arr[0]); const height = Number(arr[1]); // Height that fulfill the aspect ratio for maxWidth. const videoHeight = maxWidth * (height / width); if (maxHeight < videoHeight) { // when the height of the video is greater than the height of the window. // calculate the width that fulfill the aspect ratio for the height of the window. // ex: 16:9 aspect ratio // 16:9 = width : height // → width = 16 / 9 * height return Math.floor(width / height * maxHeight); } return '100%'; } render() { const modalVideoInnerStyle = { width: this.state.modalVideoWidth }; const modalVideoIframeWrapStyle = { paddingBottom: this.getPadding(this.props.ratio) }; return ( {() => { if (!this.state.isOpen) { return null; } return (
{this.modal = node; }} onKeyDown={this.updateFocus}>
{ this.props.children ||