├── example ├── .npmignore ├── index.html ├── tsconfig.json ├── package.json └── index.tsx ├── dist ├── index.js ├── utils.d.ts ├── index.d.ts ├── LiteYoutubeEmbed.d.ts ├── react-lite-yt-embed.cjs.production.min.js.map ├── react-lite-yt-embed.esm.js.map ├── react-lite-yt-embed.cjs.development.js.map ├── react-lite-yt-embed.cjs.production.min.js ├── react-lite-yt-embed.esm.js └── react-lite-yt-embed.cjs.development.js ├── src ├── index.tsx ├── globals.d.ts ├── typings.d.ts ├── utils.ts ├── __tests__ │ └── LiteYoutubeEmbed.test.tsx ├── styles.module.css └── LiteYoutubeEmbed.tsx ├── .gitignore ├── .npmignore ├── .travis.yml ├── tsdx.config.js ├── LICENSE ├── package.json ├── tsconfig.json └── README.md /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./react-lite-yt-embed.esm.js') 2 | -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const qs: (params: Record) => string; 2 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import LiteYoutubeEmbed from './LiteYoutubeEmbed'; 2 | export { LiteYoutubeEmbed }; 3 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import LiteYoutubeEmbed from './LiteYoutubeEmbed'; 2 | 3 | export { LiteYoutubeEmbed }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | example/node_modules 6 | example/dist 7 | example/.cache -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect"; 2 | 3 | declare module '@testing-library/jest-dom'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | webpack.config.js 3 | tsconfig.json 4 | yarn.lock 5 | .babelrc 6 | .github 7 | example 8 | .travis.yml -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const content: { [className: string]: string }; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "12" 5 | 6 | install: 7 | - yarn 8 | 9 | script: 10 | - npm run test 11 | - npm run build -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const qs = (params: Record) => { 2 | return Object.keys(params) 3 | .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) 4 | .join('&'); 5 | } 6 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | const postcss = require('rollup-plugin-postcss'); 2 | 3 | module.exports = { 4 | rollup(config, options) { 5 | config.plugins.push( 6 | postcss({ 7 | modules: true, 8 | }) 9 | ); 10 | return config; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/__tests__/LiteYoutubeEmbed.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "@testing-library/react"; 3 | import '@testing-library/jest-dom'; 4 | import { LiteYoutubeEmbed } from '../../dist/index'; 5 | 6 | describe("", () => { 7 | test("Render properly", async () => { 8 | const { getByTestId } = render(); 9 | expect(getByTestId('lite-yt-embed')).toBeInTheDocument(); 10 | }); 11 | }); -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dist/LiteYoutubeEmbed.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | declare type ResolutionType = 'maxresdefault' | 'sddefault' | 'hqdefault'; 3 | interface ILiteYouTubeEmbedProps { 4 | id: string; 5 | adLinksPreconnect?: boolean; 6 | defaultPlay?: boolean; 7 | isPlaylist?: boolean; 8 | noCookie?: boolean; 9 | mute?: boolean; 10 | params?: Record; 11 | isMobile?: boolean; 12 | mobileResolution?: ResolutionType; 13 | desktopResolution?: ResolutionType; 14 | lazyImage?: boolean; 15 | iframeTitle?: string; 16 | imageAltText?: string; 17 | } 18 | declare const LiteYoutubeEmbed: ({ id, params, defaultPlay, adLinksPreconnect, isPlaylist, noCookie, mute, isMobile, mobileResolution, desktopResolution, lazyImage, iframeTitle, imageAltText, }: ILiteYouTubeEmbedProps) => React.ReactElement; 19 | export default LiteYoutubeEmbed; 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 kylemocode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { LiteYoutubeEmbed } from '../dist'; 5 | 6 | const App = () => { 7 | return ( 8 | <> 9 |

React Lite Youtube Embed Example

10 | 11 | {/*Default is highest resolution thumbnail */} 12 |

Basic(maxresdefault)

13 | 14 |
15 | {/*sddefault resolution (medium)*/} 16 |

sddefault

17 | 18 |
19 | {/*hqdefault resolution (lowest)*/} 20 |

hqdefault

21 | 22 |
23 | {/*default play: will play the video immediately*/} 24 |

Default play

25 | 26 |
27 |

Give it an outer container

28 |
29 | 30 |
31 | 32 | ); 33 | }; 34 | 35 | ReactDOM.render(, document.getElementById('root')); 36 | -------------------------------------------------------------------------------- /dist/react-lite-yt-embed.cjs.production.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"react-lite-yt-embed.cjs.production.min.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":["css","ref","insertAt","document","head","getElementsByTagName","style","createElement","type","firstChild","insertBefore","appendChild","styleSheet","cssText","createTextNode"],"mappings":"oWAAA,SAAqBA,EAAKC,QACX,IAARA,IAAiBA,EAAM,IAC5B,IAAIC,EAAWD,EAAIC,SAEnB,GAAgC,oBAAbC,SAAnB,CAEA,IAAIC,EAAOD,SAASC,MAAQD,SAASE,qBAAqB,QAAQ,GAC9DC,EAAQH,SAASI,cAAc,SACnCD,EAAME,KAAO,WAEI,QAAbN,GACEE,EAAKK,WACPL,EAAKM,aAAaJ,EAAOF,EAAKK,YAKhCL,EAAKO,YAAYL,GAGfA,EAAMM,WACRN,EAAMM,WAAWC,QAAUb,EAE3BM,EAAMK,YAAYR,SAASW,eAAed"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.2.7", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist" 8 | ], 9 | "engines": { 10 | "node": ">=10" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "lite-youtube-embed" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/kylemocode/react-lite-yt-embed" 19 | }, 20 | "scripts": { 21 | "start": "tsdx watch", 22 | "build": "tsdx build", 23 | "test": "tsdx test --passWithNoTests", 24 | "lint": "tsdx lint", 25 | "size": "size-limit", 26 | "analyze": "size-limit --why" 27 | }, 28 | "peerDependencies": { 29 | "react": ">=16" 30 | }, 31 | "prettier": { 32 | "printWidth": 80, 33 | "semi": true, 34 | "singleQuote": true, 35 | "trailingComma": "es5" 36 | }, 37 | "name": "react-lite-yt-embed", 38 | "author": "kylemocode", 39 | "size-limit": [ 40 | { 41 | "path": "dist/test.cjs.production.min.js", 42 | "limit": "10 KB" 43 | } 44 | ], 45 | "devDependencies": { 46 | "@size-limit/preset-small-lib": "^4.7.0", 47 | "@testing-library/jest-dom": "^5.11.6", 48 | "@testing-library/react": "^10.4.9", 49 | "@types/jest": "^26.0.19", 50 | "@types/react": "^16.9.56", 51 | "@types/react-dom": "^16.9.9", 52 | "husky": "^4.3.0", 53 | "postcss": "^8.1.7", 54 | "react": "^17.0.1", 55 | "react-dom": "^17.0.1", 56 | "rollup-plugin-postcss": "^3.1.8", 57 | "size-limit": "^4.7.0", 58 | "tsdx": "^0.14.1", 59 | "tslib": "^2.0.3", 60 | "typescript": "^4.0.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": false, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dist/react-lite-yt-embed.esm.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"react-lite-yt-embed.esm.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} -------------------------------------------------------------------------------- /dist/react-lite-yt-embed.cjs.development.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"react-lite-yt-embed.cjs.development.js","sources":["../node_modules/style-inject/dist/style-inject.es.js"],"sourcesContent":["function styleInject(css, ref) {\n if ( ref === void 0 ) ref = {};\n var insertAt = ref.insertAt;\n\n if (!css || typeof document === 'undefined') { return; }\n\n var head = document.head || document.getElementsByTagName('head')[0];\n var style = document.createElement('style');\n style.type = 'text/css';\n\n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild);\n } else {\n head.appendChild(style);\n }\n } else {\n head.appendChild(style);\n }\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n style.appendChild(document.createTextNode(css));\n }\n}\n\nexport default styleInject;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE;AAC/B,EAAE,KAAK,GAAG,KAAK,KAAK,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;AACjC,EAAE,IAAI,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;AAC9B;AACA,EAAE,IAAI,CAAC,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,EAAE,OAAO,EAAE;AAC1D;AACA,EAAE,IAAI,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACvE,EAAE,IAAI,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;AAC9C,EAAE,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;AAC1B;AACA,EAAE,IAAI,QAAQ,KAAK,KAAK,EAAE;AAC1B,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE;AACzB,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AAChD,KAAK,MAAM;AACX,MAAM,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC9B,KAAK;AACL,GAAG,MAAM;AACT,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC5B,GAAG;AACH;AACA,EAAE,IAAI,KAAK,CAAC,UAAU,EAAE;AACxB,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,GAAG,GAAG,CAAC;AACnC,GAAG,MAAM;AACT,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;AACpD,GAAG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} -------------------------------------------------------------------------------- /src/styles.module.css: -------------------------------------------------------------------------------- 1 | .yt-lite { 2 | background-color: #000; 3 | position: relative; 4 | display: block; 5 | contain: content; 6 | background-position: center center; 7 | background-size: cover; 8 | cursor: pointer; 9 | } 10 | 11 | /* gradient */ 12 | .yt-lite::before { 13 | content: ''; 14 | display: block; 15 | position: absolute; 16 | top: 0; 17 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==); 18 | background-position: top; 19 | background-repeat: repeat-x; 20 | height: 60px; 21 | padding-bottom: 50px; 22 | width: 100%; 23 | transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); 24 | } 25 | 26 | /* responsive iframe with a 16:9 aspect ratio 27 | thanks https://css-tricks.com/responsive-iframes/ 28 | */ 29 | .yt-lite::after { 30 | content: ""; 31 | display: block; 32 | padding-bottom: calc(100% / (16 / 9)); 33 | } 34 | .yt-lite > iframe { 35 | width: 100%; 36 | height: 100%; 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | } 41 | 42 | /* play button */ 43 | .yt-lite > .lty-playbtn { 44 | width: 70px; 45 | height: 46px; 46 | background-color: #212121; 47 | z-index: 1; 48 | opacity: 0.8; 49 | border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */ 50 | transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); 51 | } 52 | .yt-lite:hover > .lty-playbtn { 53 | background-color: #f00; 54 | opacity: 1; 55 | } 56 | /* play button triangle */ 57 | .yt-lite > .lty-playbtn:before { 58 | content: ''; 59 | border-style: solid; 60 | border-width: 11px 0 11px 19px; 61 | border-color: transparent transparent transparent #fff; 62 | } 63 | 64 | .yt-lite > .lty-playbtn, 65 | .yt-lite > .lty-playbtn:before { 66 | position: absolute; 67 | top: 50%; 68 | left: 50%; 69 | transform: translate3d(-50%, -50%, 0); 70 | } 71 | 72 | /* Post-click styles */ 73 | .yt-lite.lyt-activated { 74 | cursor: unset; 75 | } 76 | .yt-lite.lyt-activated::before, 77 | .yt-lite.lyt-activated > .lty-playbtn { 78 | opacity: 0; 79 | pointer-events: none; 80 | } 81 | 82 | .yt-lite-thumbnail { 83 | position: absolute; 84 | width: 100%; 85 | height: 100%; 86 | left: 0; 87 | object-fit: cover; 88 | } 89 | -------------------------------------------------------------------------------- /src/LiteYoutubeEmbed.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; 2 | 3 | import styles from './styles.module.css'; 4 | import { qs } from './utils'; 5 | 6 | // https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api 7 | type ResolutionType = 'maxresdefault' | 'sddefault' | 'hqdefault'; 8 | 9 | interface ILiteYouTubeEmbedProps { 10 | id: string; 11 | adLinksPreconnect?: boolean; 12 | defaultPlay?: boolean; // set defaultPlay as `true` will directly show youtube iframe 13 | isPlaylist?: boolean; 14 | noCookie?: boolean; 15 | mute?: boolean; 16 | params?: Record; 17 | isMobile?: boolean; 18 | mobileResolution?: ResolutionType; 19 | desktopResolution?: ResolutionType; 20 | lazyImage?: boolean; 21 | iframeTitle?: string; 22 | imageAltText?: string; 23 | } 24 | 25 | const LiteYoutubeEmbed = ({ 26 | id, 27 | params = {}, 28 | defaultPlay = false, 29 | adLinksPreconnect = true, 30 | isPlaylist = false, 31 | noCookie = true, 32 | mute = true, 33 | isMobile = false, 34 | mobileResolution = 'hqdefault', 35 | desktopResolution = 'maxresdefault', 36 | lazyImage = false, 37 | iframeTitle = "YouTube video.", 38 | imageAltText = "YouTube's thumbnail image for the video.", 39 | }: ILiteYouTubeEmbedProps): React.ReactElement => { 40 | const muteParam = mute || defaultPlay ? '1' : '0'; // Default play must be mute 41 | const queryString = useMemo(() => qs({ autoplay: '1', mute: muteParam, ...params }), []); 42 | const defaultPosterUrl = isMobile ? `https://i.ytimg.com/vi_webp/${id}/${mobileResolution}.webp` : `https://i.ytimg.com/vi_webp/${id}/${desktopResolution}.webp`; 43 | const fallbackPosterUrl = isMobile ? `https://i.ytimg.com/vi/${id}/${mobileResolution}.jpg` : `https://i.ytimg.com/vi/${id}/${desktopResolution}.jpg`; 44 | const ytUrl = noCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com'; 45 | const iframeSrc = isPlaylist ? `${ytUrl}/embed/videoseries?list=${id}` : `${ytUrl}/embed/${id}?${queryString}`; // * Lo, the youtube placeholder image! (aka the thumbnail, poster image, etc) 46 | 47 | const [isPreconnected, setIsPreconnected] = useState(false); 48 | const [iframeLoaded, setIframeLoaded] = useState(defaultPlay); 49 | const [posterUrl, setPosterUrl] = useState(defaultPosterUrl); 50 | const isWebpSupported = useRef(true); 51 | 52 | const warmConnections = useCallback(() => { 53 | if (isPreconnected) return; 54 | setIsPreconnected(true); 55 | }, []); 56 | 57 | const loadIframeFunc = useCallback(() => { 58 | if (iframeLoaded) return; 59 | setIframeLoaded(true); 60 | }, []); 61 | 62 | // fallback to hqdefault resolution if maxresdefault is not supported. 63 | useEffect(() => { 64 | if ((isMobile && mobileResolution === 'hqdefault') || (!isMobile && desktopResolution === 'hqdefault') && !isWebpSupported.current) return; 65 | 66 | // If the image ever loaded one time (in this case is preload link), this part will use cache data, won't cause a new network request. 67 | const img = new Image(); 68 | img.onload = function() { 69 | if (img.width === 120 || img.width === 0) { 70 | if (!isWebpSupported.current) setPosterUrl(`https://i.ytimg.com/vi/${id}/hqdefault.jpg`); 71 | else setPosterUrl(`https://i.ytimg.com/vi_webp/${id}/hqdefault`); 72 | } 73 | }; 74 | img.onerror = function() { 75 | isWebpSupported.current = false; 76 | setPosterUrl(fallbackPosterUrl); 77 | }; 78 | img.src = posterUrl; 79 | }, [id, posterUrl]); 80 | 81 | return ( 82 | <> 83 | {/* Link is "body-ok" element. Reference: https://html.spec.whatwg.org/multipage/links.html#body-ok */} 84 | 85 | {isPreconnected && ( 86 | <> 87 | {/* The iframe document and most of its subresources come right off youtube.com */} 88 | 89 | {/* The botguard script is fetched off from google.com */} 90 | 91 | 92 | )} 93 | {isPreconnected && adLinksPreconnect && ( 94 | <> 95 | {/* Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling. */} 96 | 97 | 98 | 99 | )} 100 |
106 | {imageAltText}/ 111 |
112 | {iframeLoaded && ( 113 | 122 | )} 123 |
124 | 125 | ); 126 | }; 127 | 128 | export default LiteYoutubeEmbed; 129 | -------------------------------------------------------------------------------- /dist/react-lite-yt-embed.cjs.production.min.js: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t=require("react"),n=(e=t)&&"object"==typeof e&&"default"in e?e.default:e;function o(){return(o=Object.assign||function(e){for(var t=1;t iframe {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n/* play button */\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ {\n width: 70px;\n height: 46px;\n background-color: #212121;\n z-index: 1;\n opacity: 0.8;\n border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */\n transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n.styles-module_yt-lite__1-uDa:hover > .styles-module_lty-playbtn__1pxDJ {\n background-color: #f00;\n opacity: 1;\n}\n/* play button triangle */\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ:before {\n content: '';\n border-style: solid;\n border-width: 11px 0 11px 19px;\n border-color: transparent transparent transparent #fff;\n}\n\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ,\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ:before {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate3d(-50%, -50%, 0);\n}\n\n/* Post-click styles */\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0 {\n cursor: unset;\n}\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0::before,\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0 > .styles-module_lty-playbtn__1pxDJ {\n opacity: 0;\n pointer-events: none;\n}\n\n.styles-module_yt-lite-thumbnail__2WX0n {\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n object-fit: cover;\n}\n"),exports.LiteYoutubeEmbed=function(e){var l=e.id,i=e.params,a=void 0===i?{}:i,s=e.defaultPlay,r=void 0!==s&&s,u=e.adLinksPreconnect,c=void 0===u||u,d=e.isPlaylist,p=void 0!==d&&d,m=e.noCookie,y=void 0===m||m,_=e.mute,b=e.isMobile,h=void 0!==b&&b,f=e.mobileResolution,g=void 0===f?"hqdefault":f,v=e.desktopResolution,A=void 0===v?"maxresdefault":v,k=e.lazyImage,w=void 0!==k&&k,E=e.iframeTitle,x=void 0===E?"YouTube video.":E,D=e.imageAltText,O=void 0===D?"YouTube's thumbnail image for the video.":D,R=void 0===_||_||r?"1":"0",C=t.useMemo((function(){return function(e){return Object.keys(e).map((function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])})).join("&")}(o({autoplay:"1",mute:R},a))}),[]),j=h?"https://i.ytimg.com/vi_webp/"+l+"/"+g+".webp":"https://i.ytimg.com/vi_webp/"+l+"/"+A+".webp",J=h?"https://i.ytimg.com/vi/"+l+"/"+g+".jpg":"https://i.ytimg.com/vi/"+l+"/"+A+".jpg",T=y?"https://www.youtube-nocookie.com":"https://www.youtube.com",F=p?T+"/embed/videoseries?list="+l:T+"/embed/"+l+"?"+C,q=t.useState(!1),S=q[0],z=q[1],B=t.useState(r),I=B[0],U=B[1],Y=t.useState(j),N=Y[0],P=Y[1],Q=t.useRef(!0),W=t.useCallback((function(){S||z(!0)}),[]),L=t.useCallback((function(){I||U(!0)}),[]);return t.useEffect((function(){if(!(h&&"hqdefault"===g||!h&&"hqdefault"===A&&!Q.current)){var e=new Image;e.onload=function(){120!==e.width&&0!==e.width||P(Q.current?"https://i.ytimg.com/vi_webp/"+l+"/hqdefault":"https://i.ytimg.com/vi/"+l+"/hqdefault.jpg")},e.onerror=function(){Q.current=!1,P(J)},e.src=N}}),[l,N]),n.createElement(n.Fragment,null,n.createElement("link",{rel:"preload",href:N,as:"image"}),S&&n.createElement(n.Fragment,null,n.createElement("link",{rel:"preconnect",href:T}),n.createElement("link",{rel:"preconnect",href:"https://www.google.com"})),S&&c&&n.createElement(n.Fragment,null,n.createElement("link",{rel:"preconnect",href:"https://static.doubleclick.net"}),n.createElement("link",{rel:"preconnect",href:"https://googleads.g.doubleclick.net"})),n.createElement("div",{onClick:L,onPointerOver:W,className:"styles-module_yt-lite__1-uDa "+(I&&"styles-module_lyt-activated__3ROp0"),"data-testid":"lite-yt-embed"},n.createElement("img",{src:N,className:"styles-module_yt-lite-thumbnail__2WX0n",loading:w?"lazy":void 0,alt:O}),n.createElement("div",{className:"styles-module_lty-playbtn__1pxDJ"}),I&&n.createElement("iframe",{width:"560",height:"315",frameBorder:"0",allow:"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture",allowFullScreen:!0,title:x,src:F})))}; 2 | //# sourceMappingURL=react-lite-yt-embed.cjs.production.min.js.map 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Lite YouTube Embed 2 | React component version of [lite-youtube-embed](https://github.com/paulirish/lite-youtube-embed) scaffolded with tsdx, which focus on visual performance, rendering just like the real thing but much faster. 3 | 4 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkylemocode%2Freact-lite-yt-embed.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkylemocode%2Freact-lite-yt-embed?ref=badge_shield) 5 | ![Download](https://img.shields.io/npm/dt/react-lite-yt-embed) 6 | 7 | ![](https://i.imgur.com/7QkCbgl.gif) 8 | 9 | # Quick Start 10 | 11 | ## Install 12 | Please use version at least above @1.2.1, version below that is experimental and therefore may cause some runtime error. 13 | 14 | [react-lite-yt-embed](https://www.npmjs.com/package/react-lite-yt-embed) 15 | ```shell 16 | $ yarn add react-lite-yt-embed 17 | ``` 18 | 19 | or 20 | 21 | ```shell 22 | $ npm install react-lite-yt-embed 23 | ``` 24 | 25 | ### Sandbox Example 26 | [![codesandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/blissful-fog-d02pi?file=/src/App.js) 27 | 28 | ## Overview 29 | 30 | ### Features 🎉 31 | - Preconnect YouTube domain while mouseover 32 | - Preload thumbnail image to improve user experience 33 | - WebP support, and fallback to jpg if the browser not support it 34 | - Provides many props which can customize frame behaviors 35 | - Lazy load support 36 | - Render fast, improve your web's performance 37 | 38 | ### Introduction 39 | 'react-lite-yt-embed' is a react component version of popular package [lite-youtube-embed](https://github.com/paulirish/lite-youtube-embed), which can use in React project including SSR and CSR project. It renders just like the real iframe but way much faster. 40 | 41 | ### Why is it faster than normal iframe ? 42 | - Preload the YouTube thumbnail image when page loaded. [Make image load faster] 43 | - Preconnect YouTube domain when mouse hover on the component. [Save 3 round-trip-time (DNS lookup + TCP handshake + SSL negotiation) before user click play button, making iframe load faster] 44 | 45 | ### WebP Support (support after version @1.2.4) 46 | 'react-lite-yt-embed' support WebP image format, which is generally 25% - 35% smaller than jpg image, so the network request time will also decrease, making your web app render even faster. 47 | 48 | If you use some browsers that not totally support WebP, for example, Safari, 'react-lite-yt-embed' will fallback the image to jpg automatically. 49 | 50 | You can see WebP browser support [here](https://caniuse.com/?search=webp). 51 | 52 | ## Basic Usage 53 | 54 | ```javascript 55 | import { LiteYoutubeEmbed } from 'react-lite-yt-embed'; 56 | 57 | // In your react component 58 | <> 59 | {/* ID of YouTube video */} 60 | {/* You can add more props, see the description below. */} 61 | {/* You can also give the iframe an outer container */} 62 | 63 | ``` 64 | 65 | ## Component Props 66 | 67 | | props | required | default value | Type | Description | 68 | |-------------------|----------|-----------------|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| 69 | | id | true | none | `string` | The unique id of the youtube video | 70 | | defaultPlay | false | false | `boolean` | Set defaultPlay as `true` will directly show youtube iframe | 71 | | isPlaylist | false | false | `boolean` | If you want to play playlist, set this as `true` and pass the playlist id | 72 | | noCookie | false | true | `boolean` | Use "https://www.youtube-nocookie.com" as path or "https://www.youtube.com" | 73 | | mute | false | true | `boolean` | Set the video is mute or not. | 74 | | params | false | {} | `Object` | Query string params (autoplay and mute are default query string, you do not have to set them), the value have to be a string type. | 75 | | isMobile | false | false | `boolean` | Use in mobile device or not. | 76 | | mobileResolution | false | 'hqdefault' | 'hqdefault' \| 'sddefault' \| 'maxresdefault' | You can specify the resolution of the thumbnail image on the phone (default is hqdefault, which is a lower resolution). | 77 | | desktopResolution | false | 'maxresdefault' | 'hqdefault' \| 'sddefault' \| 'maxresdefault' | You can specify the resolution of the thumbnail image on the desktop (default is maxresdefault, which is the highest resolution). | 78 | | lazyImage | false | false | `boolean` | If true, set the img loading attribute to 'lazy', default is undefined. | 79 | | imageAltText | false | "YouTube's thumbnail for this video." | `string` | You can specify an alternative text description for the thumbnail image for accessibility purposes. | 80 | | iframeTitle | false | "YouTube video." | `string` | You can specify a title for the iframe containing the video for accessibility purposes. | 81 | 82 | ## Run on local development environment 83 | - Clone the repo 84 | - npm install | yarn install 85 | - Make your change or contribution 86 | - npm run build (in root folder) 87 | - cd in example folder & run npm install 88 | - npm run build (in example folder) 89 | - npm run start (in example folder) 90 | - visit localhost:1234 in the browser 91 | 92 | ## Roadmap (Welcome contribution) 93 | - [ ] Testing 94 | - [X] CI/CD pipeline 95 | 96 | ## License 97 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fkylemocode%2Freact-lite-yt-embed.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fkylemocode%2Freact-lite-yt-embed?ref=badge_large) 98 | -------------------------------------------------------------------------------- /dist/react-lite-yt-embed.esm.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useRef, useCallback, useEffect } from 'react'; 2 | 3 | function _extends() { 4 | _extends = Object.assign || function (target) { 5 | for (var i = 1; i < arguments.length; i++) { 6 | var source = arguments[i]; 7 | 8 | for (var key in source) { 9 | if (Object.prototype.hasOwnProperty.call(source, key)) { 10 | target[key] = source[key]; 11 | } 12 | } 13 | } 14 | 15 | return target; 16 | }; 17 | 18 | return _extends.apply(this, arguments); 19 | } 20 | 21 | function styleInject(css, ref) { 22 | if ( ref === void 0 ) ref = {}; 23 | var insertAt = ref.insertAt; 24 | 25 | if (!css || typeof document === 'undefined') { return; } 26 | 27 | var head = document.head || document.getElementsByTagName('head')[0]; 28 | var style = document.createElement('style'); 29 | style.type = 'text/css'; 30 | 31 | if (insertAt === 'top') { 32 | if (head.firstChild) { 33 | head.insertBefore(style, head.firstChild); 34 | } else { 35 | head.appendChild(style); 36 | } 37 | } else { 38 | head.appendChild(style); 39 | } 40 | 41 | if (style.styleSheet) { 42 | style.styleSheet.cssText = css; 43 | } else { 44 | style.appendChild(document.createTextNode(css)); 45 | } 46 | } 47 | 48 | var css_248z = ".styles-module_yt-lite__1-uDa {\n background-color: #000;\n position: relative;\n display: block;\n contain: content;\n background-position: center center;\n background-size: cover;\n cursor: pointer;\n}\n\n/* gradient */\n.styles-module_yt-lite__1-uDa::before {\n content: '';\n display: block;\n position: absolute;\n top: 0;\n background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);\n background-position: top;\n background-repeat: repeat-x;\n height: 60px;\n padding-bottom: 50px;\n width: 100%;\n transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n\n/* responsive iframe with a 16:9 aspect ratio\n thanks https://css-tricks.com/responsive-iframes/\n*/\n.styles-module_yt-lite__1-uDa::after {\n content: \"\";\n display: block;\n padding-bottom: calc(100% / (16 / 9));\n}\n.styles-module_yt-lite__1-uDa > iframe {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n/* play button */\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ {\n width: 70px;\n height: 46px;\n background-color: #212121;\n z-index: 1;\n opacity: 0.8;\n border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */\n transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n.styles-module_yt-lite__1-uDa:hover > .styles-module_lty-playbtn__1pxDJ {\n background-color: #f00;\n opacity: 1;\n}\n/* play button triangle */\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ:before {\n content: '';\n border-style: solid;\n border-width: 11px 0 11px 19px;\n border-color: transparent transparent transparent #fff;\n}\n\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ,\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ:before {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate3d(-50%, -50%, 0);\n}\n\n/* Post-click styles */\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0 {\n cursor: unset;\n}\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0::before,\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0 > .styles-module_lty-playbtn__1pxDJ {\n opacity: 0;\n pointer-events: none;\n}\n\n.styles-module_yt-lite-thumbnail__2WX0n {\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n object-fit: cover;\n}\n"; 49 | var styles = {"yt-lite":"styles-module_yt-lite__1-uDa","lty-playbtn":"styles-module_lty-playbtn__1pxDJ","lyt-activated":"styles-module_lyt-activated__3ROp0","yt-lite-thumbnail":"styles-module_yt-lite-thumbnail__2WX0n"}; 50 | styleInject(css_248z); 51 | 52 | var qs = function qs(params) { 53 | return Object.keys(params).map(function (key) { 54 | return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); 55 | }).join('&'); 56 | }; 57 | 58 | var LiteYoutubeEmbed = function LiteYoutubeEmbed(_ref) { 59 | var id = _ref.id, 60 | _ref$params = _ref.params, 61 | params = _ref$params === void 0 ? {} : _ref$params, 62 | _ref$defaultPlay = _ref.defaultPlay, 63 | defaultPlay = _ref$defaultPlay === void 0 ? false : _ref$defaultPlay, 64 | _ref$adLinksPreconnec = _ref.adLinksPreconnect, 65 | adLinksPreconnect = _ref$adLinksPreconnec === void 0 ? true : _ref$adLinksPreconnec, 66 | _ref$isPlaylist = _ref.isPlaylist, 67 | isPlaylist = _ref$isPlaylist === void 0 ? false : _ref$isPlaylist, 68 | _ref$noCookie = _ref.noCookie, 69 | noCookie = _ref$noCookie === void 0 ? true : _ref$noCookie, 70 | _ref$mute = _ref.mute, 71 | mute = _ref$mute === void 0 ? true : _ref$mute, 72 | _ref$isMobile = _ref.isMobile, 73 | isMobile = _ref$isMobile === void 0 ? false : _ref$isMobile, 74 | _ref$mobileResolution = _ref.mobileResolution, 75 | mobileResolution = _ref$mobileResolution === void 0 ? 'hqdefault' : _ref$mobileResolution, 76 | _ref$desktopResolutio = _ref.desktopResolution, 77 | desktopResolution = _ref$desktopResolutio === void 0 ? 'maxresdefault' : _ref$desktopResolutio, 78 | _ref$lazyImage = _ref.lazyImage, 79 | lazyImage = _ref$lazyImage === void 0 ? false : _ref$lazyImage, 80 | _ref$iframeTitle = _ref.iframeTitle, 81 | iframeTitle = _ref$iframeTitle === void 0 ? "YouTube video." : _ref$iframeTitle, 82 | _ref$imageAltText = _ref.imageAltText, 83 | imageAltText = _ref$imageAltText === void 0 ? "YouTube's thumbnail image for the video." : _ref$imageAltText; 84 | var muteParam = mute || defaultPlay ? '1' : '0'; // Default play must be mute 85 | 86 | var queryString = useMemo(function () { 87 | return qs(_extends({ 88 | autoplay: '1', 89 | mute: muteParam 90 | }, params)); 91 | }, []); 92 | var defaultPosterUrl = isMobile ? "https://i.ytimg.com/vi_webp/" + id + "/" + mobileResolution + ".webp" : "https://i.ytimg.com/vi_webp/" + id + "/" + desktopResolution + ".webp"; 93 | var fallbackPosterUrl = isMobile ? "https://i.ytimg.com/vi/" + id + "/" + mobileResolution + ".jpg" : "https://i.ytimg.com/vi/" + id + "/" + desktopResolution + ".jpg"; 94 | var ytUrl = noCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com'; 95 | var iframeSrc = isPlaylist ? ytUrl + "/embed/videoseries?list=" + id : ytUrl + "/embed/" + id + "?" + queryString; // * Lo, the youtube placeholder image! (aka the thumbnail, poster image, etc) 96 | 97 | var _useState = useState(false), 98 | isPreconnected = _useState[0], 99 | setIsPreconnected = _useState[1]; 100 | 101 | var _useState2 = useState(defaultPlay), 102 | iframeLoaded = _useState2[0], 103 | setIframeLoaded = _useState2[1]; 104 | 105 | var _useState3 = useState(defaultPosterUrl), 106 | posterUrl = _useState3[0], 107 | setPosterUrl = _useState3[1]; 108 | 109 | var isWebpSupported = useRef(true); 110 | var warmConnections = useCallback(function () { 111 | if (isPreconnected) return; 112 | setIsPreconnected(true); 113 | }, []); 114 | var loadIframeFunc = useCallback(function () { 115 | if (iframeLoaded) return; 116 | setIframeLoaded(true); 117 | }, []); // fallback to hqdefault resolution if maxresdefault is not supported. 118 | 119 | useEffect(function () { 120 | if (isMobile && mobileResolution === 'hqdefault' || !isMobile && desktopResolution === 'hqdefault' && !isWebpSupported.current) return; // If the image ever loaded one time (in this case is preload link), this part will use cache data, won't cause a new network request. 121 | 122 | var img = new Image(); 123 | 124 | img.onload = function () { 125 | if (img.width === 120 || img.width === 0) { 126 | if (!isWebpSupported.current) setPosterUrl("https://i.ytimg.com/vi/" + id + "/hqdefault.jpg");else setPosterUrl("https://i.ytimg.com/vi_webp/" + id + "/hqdefault"); 127 | } 128 | }; 129 | 130 | img.onerror = function () { 131 | isWebpSupported.current = false; 132 | setPosterUrl(fallbackPosterUrl); 133 | }; 134 | 135 | img.src = posterUrl; 136 | }, [id, posterUrl]); 137 | return React.createElement(React.Fragment, null, React.createElement("link", { 138 | rel: 'preload', 139 | href: posterUrl, 140 | as: 'image' 141 | }), isPreconnected && React.createElement(React.Fragment, null, React.createElement("link", { 142 | rel: 'preconnect', 143 | href: ytUrl 144 | }), React.createElement("link", { 145 | rel: 'preconnect', 146 | href: 'https://www.google.com' 147 | })), isPreconnected && adLinksPreconnect && React.createElement(React.Fragment, null, React.createElement("link", { 148 | rel: 'preconnect', 149 | href: 'https://static.doubleclick.net' 150 | }), React.createElement("link", { 151 | rel: 'preconnect', 152 | href: 'https://googleads.g.doubleclick.net' 153 | })), React.createElement("div", { 154 | onClick: loadIframeFunc, 155 | onPointerOver: warmConnections, 156 | className: styles['yt-lite'] + " " + (iframeLoaded && styles['lyt-activated']), 157 | "data-testid": 'lite-yt-embed' 158 | }, React.createElement("img", { 159 | src: posterUrl, 160 | className: "" + styles['yt-lite-thumbnail'], 161 | loading: lazyImage ? 'lazy' : undefined, 162 | alt: imageAltText 163 | }), React.createElement("div", { 164 | className: "" + styles['lty-playbtn'] 165 | }), iframeLoaded && React.createElement("iframe", { 166 | width: '560', 167 | height: '315', 168 | frameBorder: '0', 169 | allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', 170 | allowFullScreen: true, 171 | title: iframeTitle, 172 | src: iframeSrc 173 | }))); 174 | }; 175 | 176 | export { LiteYoutubeEmbed }; 177 | //# sourceMappingURL=react-lite-yt-embed.esm.js.map 178 | -------------------------------------------------------------------------------- /dist/react-lite-yt-embed.cjs.development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } 6 | 7 | var React = require('react'); 8 | var React__default = _interopDefault(React); 9 | 10 | function _extends() { 11 | _extends = Object.assign || function (target) { 12 | for (var i = 1; i < arguments.length; i++) { 13 | var source = arguments[i]; 14 | 15 | for (var key in source) { 16 | if (Object.prototype.hasOwnProperty.call(source, key)) { 17 | target[key] = source[key]; 18 | } 19 | } 20 | } 21 | 22 | return target; 23 | }; 24 | 25 | return _extends.apply(this, arguments); 26 | } 27 | 28 | function styleInject(css, ref) { 29 | if ( ref === void 0 ) ref = {}; 30 | var insertAt = ref.insertAt; 31 | 32 | if (!css || typeof document === 'undefined') { return; } 33 | 34 | var head = document.head || document.getElementsByTagName('head')[0]; 35 | var style = document.createElement('style'); 36 | style.type = 'text/css'; 37 | 38 | if (insertAt === 'top') { 39 | if (head.firstChild) { 40 | head.insertBefore(style, head.firstChild); 41 | } else { 42 | head.appendChild(style); 43 | } 44 | } else { 45 | head.appendChild(style); 46 | } 47 | 48 | if (style.styleSheet) { 49 | style.styleSheet.cssText = css; 50 | } else { 51 | style.appendChild(document.createTextNode(css)); 52 | } 53 | } 54 | 55 | var css_248z = ".styles-module_yt-lite__1-uDa {\n background-color: #000;\n position: relative;\n display: block;\n contain: content;\n background-position: center center;\n background-size: cover;\n cursor: pointer;\n}\n\n/* gradient */\n.styles-module_yt-lite__1-uDa::before {\n content: '';\n display: block;\n position: absolute;\n top: 0;\n background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);\n background-position: top;\n background-repeat: repeat-x;\n height: 60px;\n padding-bottom: 50px;\n width: 100%;\n transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n\n/* responsive iframe with a 16:9 aspect ratio\n thanks https://css-tricks.com/responsive-iframes/\n*/\n.styles-module_yt-lite__1-uDa::after {\n content: \"\";\n display: block;\n padding-bottom: calc(100% / (16 / 9));\n}\n.styles-module_yt-lite__1-uDa > iframe {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n/* play button */\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ {\n width: 70px;\n height: 46px;\n background-color: #212121;\n z-index: 1;\n opacity: 0.8;\n border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */\n transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);\n}\n.styles-module_yt-lite__1-uDa:hover > .styles-module_lty-playbtn__1pxDJ {\n background-color: #f00;\n opacity: 1;\n}\n/* play button triangle */\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ:before {\n content: '';\n border-style: solid;\n border-width: 11px 0 11px 19px;\n border-color: transparent transparent transparent #fff;\n}\n\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ,\n.styles-module_yt-lite__1-uDa > .styles-module_lty-playbtn__1pxDJ:before {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate3d(-50%, -50%, 0);\n}\n\n/* Post-click styles */\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0 {\n cursor: unset;\n}\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0::before,\n.styles-module_yt-lite__1-uDa.styles-module_lyt-activated__3ROp0 > .styles-module_lty-playbtn__1pxDJ {\n opacity: 0;\n pointer-events: none;\n}\n\n.styles-module_yt-lite-thumbnail__2WX0n {\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n object-fit: cover;\n}\n"; 56 | var styles = {"yt-lite":"styles-module_yt-lite__1-uDa","lty-playbtn":"styles-module_lty-playbtn__1pxDJ","lyt-activated":"styles-module_lyt-activated__3ROp0","yt-lite-thumbnail":"styles-module_yt-lite-thumbnail__2WX0n"}; 57 | styleInject(css_248z); 58 | 59 | var qs = function qs(params) { 60 | return Object.keys(params).map(function (key) { 61 | return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); 62 | }).join('&'); 63 | }; 64 | 65 | var LiteYoutubeEmbed = function LiteYoutubeEmbed(_ref) { 66 | var id = _ref.id, 67 | _ref$params = _ref.params, 68 | params = _ref$params === void 0 ? {} : _ref$params, 69 | _ref$defaultPlay = _ref.defaultPlay, 70 | defaultPlay = _ref$defaultPlay === void 0 ? false : _ref$defaultPlay, 71 | _ref$adLinksPreconnec = _ref.adLinksPreconnect, 72 | adLinksPreconnect = _ref$adLinksPreconnec === void 0 ? true : _ref$adLinksPreconnec, 73 | _ref$isPlaylist = _ref.isPlaylist, 74 | isPlaylist = _ref$isPlaylist === void 0 ? false : _ref$isPlaylist, 75 | _ref$noCookie = _ref.noCookie, 76 | noCookie = _ref$noCookie === void 0 ? true : _ref$noCookie, 77 | _ref$mute = _ref.mute, 78 | mute = _ref$mute === void 0 ? true : _ref$mute, 79 | _ref$isMobile = _ref.isMobile, 80 | isMobile = _ref$isMobile === void 0 ? false : _ref$isMobile, 81 | _ref$mobileResolution = _ref.mobileResolution, 82 | mobileResolution = _ref$mobileResolution === void 0 ? 'hqdefault' : _ref$mobileResolution, 83 | _ref$desktopResolutio = _ref.desktopResolution, 84 | desktopResolution = _ref$desktopResolutio === void 0 ? 'maxresdefault' : _ref$desktopResolutio, 85 | _ref$lazyImage = _ref.lazyImage, 86 | lazyImage = _ref$lazyImage === void 0 ? false : _ref$lazyImage, 87 | _ref$iframeTitle = _ref.iframeTitle, 88 | iframeTitle = _ref$iframeTitle === void 0 ? "YouTube video." : _ref$iframeTitle, 89 | _ref$imageAltText = _ref.imageAltText, 90 | imageAltText = _ref$imageAltText === void 0 ? "YouTube's thumbnail image for the video." : _ref$imageAltText; 91 | var muteParam = mute || defaultPlay ? '1' : '0'; // Default play must be mute 92 | 93 | var queryString = React.useMemo(function () { 94 | return qs(_extends({ 95 | autoplay: '1', 96 | mute: muteParam 97 | }, params)); 98 | }, []); 99 | var defaultPosterUrl = isMobile ? "https://i.ytimg.com/vi_webp/" + id + "/" + mobileResolution + ".webp" : "https://i.ytimg.com/vi_webp/" + id + "/" + desktopResolution + ".webp"; 100 | var fallbackPosterUrl = isMobile ? "https://i.ytimg.com/vi/" + id + "/" + mobileResolution + ".jpg" : "https://i.ytimg.com/vi/" + id + "/" + desktopResolution + ".jpg"; 101 | var ytUrl = noCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com'; 102 | var iframeSrc = isPlaylist ? ytUrl + "/embed/videoseries?list=" + id : ytUrl + "/embed/" + id + "?" + queryString; // * Lo, the youtube placeholder image! (aka the thumbnail, poster image, etc) 103 | 104 | var _useState = React.useState(false), 105 | isPreconnected = _useState[0], 106 | setIsPreconnected = _useState[1]; 107 | 108 | var _useState2 = React.useState(defaultPlay), 109 | iframeLoaded = _useState2[0], 110 | setIframeLoaded = _useState2[1]; 111 | 112 | var _useState3 = React.useState(defaultPosterUrl), 113 | posterUrl = _useState3[0], 114 | setPosterUrl = _useState3[1]; 115 | 116 | var isWebpSupported = React.useRef(true); 117 | var warmConnections = React.useCallback(function () { 118 | if (isPreconnected) return; 119 | setIsPreconnected(true); 120 | }, []); 121 | var loadIframeFunc = React.useCallback(function () { 122 | if (iframeLoaded) return; 123 | setIframeLoaded(true); 124 | }, []); // fallback to hqdefault resolution if maxresdefault is not supported. 125 | 126 | React.useEffect(function () { 127 | if (isMobile && mobileResolution === 'hqdefault' || !isMobile && desktopResolution === 'hqdefault' && !isWebpSupported.current) return; // If the image ever loaded one time (in this case is preload link), this part will use cache data, won't cause a new network request. 128 | 129 | var img = new Image(); 130 | 131 | img.onload = function () { 132 | if (img.width === 120 || img.width === 0) { 133 | if (!isWebpSupported.current) setPosterUrl("https://i.ytimg.com/vi/" + id + "/hqdefault.jpg");else setPosterUrl("https://i.ytimg.com/vi_webp/" + id + "/hqdefault"); 134 | } 135 | }; 136 | 137 | img.onerror = function () { 138 | isWebpSupported.current = false; 139 | setPosterUrl(fallbackPosterUrl); 140 | }; 141 | 142 | img.src = posterUrl; 143 | }, [id, posterUrl]); 144 | return React__default.createElement(React__default.Fragment, null, React__default.createElement("link", { 145 | rel: 'preload', 146 | href: posterUrl, 147 | as: 'image' 148 | }), isPreconnected && React__default.createElement(React__default.Fragment, null, React__default.createElement("link", { 149 | rel: 'preconnect', 150 | href: ytUrl 151 | }), React__default.createElement("link", { 152 | rel: 'preconnect', 153 | href: 'https://www.google.com' 154 | })), isPreconnected && adLinksPreconnect && React__default.createElement(React__default.Fragment, null, React__default.createElement("link", { 155 | rel: 'preconnect', 156 | href: 'https://static.doubleclick.net' 157 | }), React__default.createElement("link", { 158 | rel: 'preconnect', 159 | href: 'https://googleads.g.doubleclick.net' 160 | })), React__default.createElement("div", { 161 | onClick: loadIframeFunc, 162 | onPointerOver: warmConnections, 163 | className: styles['yt-lite'] + " " + (iframeLoaded && styles['lyt-activated']), 164 | "data-testid": 'lite-yt-embed' 165 | }, React__default.createElement("img", { 166 | src: posterUrl, 167 | className: "" + styles['yt-lite-thumbnail'], 168 | loading: lazyImage ? 'lazy' : undefined, 169 | alt: imageAltText 170 | }), React__default.createElement("div", { 171 | className: "" + styles['lty-playbtn'] 172 | }), iframeLoaded && React__default.createElement("iframe", { 173 | width: '560', 174 | height: '315', 175 | frameBorder: '0', 176 | allow: 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', 177 | allowFullScreen: true, 178 | title: iframeTitle, 179 | src: iframeSrc 180 | }))); 181 | }; 182 | 183 | exports.LiteYoutubeEmbed = LiteYoutubeEmbed; 184 | //# sourceMappingURL=react-lite-yt-embed.cjs.development.js.map 185 | --------------------------------------------------------------------------------