├── .babelrc
├── .github
└── workflows
│ ├── integrate.yml
│ └── npm-publish.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── bower.json
├── index.html
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── lazyframe.js
└── scss
│ ├── _base.scss
│ ├── lazyframe.scss
│ └── themes
│ ├── _vimeo.scss
│ └── _youtube.scss
└── test
├── browser.js
└── server.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/env",
5 | {
6 | "modules": false,
7 | "forceAllTransforms": true
8 | }
9 | ]
10 | ],
11 | "plugins": [
12 | "@babel/plugin-transform-object-assign"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/integrate.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ master ]
6 |
7 |
8 | jobs:
9 | test_pull_request:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: 12
16 | - run: npm ci
17 | - run: npm test
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created, edited]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v2
16 | with:
17 | node-version: 12
18 | - run: npm ci
19 | - run: npm run build
20 | - run: npm test
21 |
22 | publish-npm:
23 | needs: build
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v2
27 | - uses: actions/setup-node@v2
28 | with:
29 | node-version: 12
30 | registry-url: https://registry.npmjs.org/
31 | - run: npm ci
32 | - run: npm run build
33 | - run: npm publish
34 | env:
35 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true
6 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Viktor Bergehall
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lazyframe
2 | [](https://github.com/vb/lazyframe/actions/workflows/npm-publish.yml)
3 | [](https://badge.fury.io/js/lazyframe)
4 |
5 | Dependency-free library for lazyloading iframes. [Demo](https://vb.github.io/lazyframe/)
6 |
7 | ## Why?
8 |
9 | Because embedded content takes time to load.
10 |
11 | - **Youtube** – 11 requests ≈ 580kb
12 | - **Google maps** – 52 requests ≈ 580kb
13 | - **Vimeo** – 8 requests ≈ 145kb
14 |
15 | Lazyframe creates a responsive placeholder for embedded content and requests it when the user interacts with it. This decreases the page load and idle time.
16 |
17 | Lazyframe comes with brand-like themes for Youtube and Vimeo.
18 |
19 | 1. [Install](#install)
20 | 2. [Import](#import)
21 | 3. [Initialize](#Initialize)
22 | 4. [Options](#options)
23 |
24 | ### Install
25 |
26 | NPM
27 |
28 | ```bash
29 | $ npm install lazyframe --save
30 | ```
31 |
32 | Bower
33 |
34 | ```bash
35 | $ bower install lazyframe
36 | ```
37 |
38 | ### Import
39 |
40 | JavaScript ES6 imports
41 |
42 | ```js
43 | import lazyframe from "lazyframe";
44 | ```
45 |
46 | Include JavaScript in html
47 |
48 | ```html
49 |
50 | ```
51 |
52 | Sass import
53 |
54 | ```sass
55 | @import 'src/scss/lazyframe'
56 | ```
57 |
58 | Include css in html
59 |
60 | ```html
61 |
62 | ```
63 |
64 | ### Initialize
65 |
66 | ```js
67 | // Passing a selector
68 | lazyframe(".lazyframe");
69 |
70 | // Passing a nodelist
71 | let elements = document.querySelectorAll(".lazyframe");
72 | lazyframe(elements);
73 |
74 | // Passing a jQuery object
75 | let elements = $(".lazyframe");
76 | lazyframe(elements);
77 | ```
78 |
79 | ## Options
80 |
81 | You can pass general options to lazyframe on initialization. Element-specific options (most options) are set on data attributes on the element itself.
82 |
83 | General options and corresponding defaults
84 |
85 | ```js
86 | lazyframe(elements, {
87 | debounce: 250,
88 | lazyload: true,
89 | autoplay: true,
90 |
91 | // Callbacks
92 | onLoad: (lazyframe) => console.log(lazyframe),
93 | onAppend: (iframe) => console.log(iframe),
94 | onThumbnailLoad: (img) => console.log(img),
95 | });
96 | ```
97 |
98 | ### `debounce`
99 |
100 | Value (in milliseconds) for when the update function should run after the user has scrolled. [More here](https://css-tricks.com/the-difference-between-throttling-and-debouncing/)
101 |
102 | ### `lazyload`
103 |
104 | Set this to `false` if you want all API calls and local images to be loaded on page load (instead of when the element is in view).
105 |
106 | ### `autoplay`
107 |
108 | Set this to `false` to remove autoplay from the `allow` attribute on the iframe tag i.e if set this to `false` if you want don't want your Youtube video to automatically start playing once the user clicks on the play icon.
109 |
110 | ### `onLoad`
111 |
112 | Callback function for when a element is initialized.
113 |
114 | ### `onAppend`
115 |
116 | Callback function for when the iframe is appended to DOM.
117 |
118 | ### `onThumbnailLoad`
119 |
120 | Callback function with the thumbnail URL
121 |
122 | ## Element-specific options
123 |
124 | ```html
125 |
135 | ```
136 |
137 | ### `data-vendor`
138 |
139 | Attribute for theming lazyframe. Currently supported values are `youtube`, `youtube_nocookie` and `vimeo`.
140 |
141 | ### `data-title`
142 |
143 | Attribute for custom title. Leave empty to get value from noembed.com.
144 |
145 | ### `data-thumbnail`
146 |
147 | Attribute for custom thumbnail. Leave empty to get value from noembed.com.
148 |
149 | ### `data-src`
150 |
151 | The source of what you want to lazyload.
152 |
153 | ### `data-ratio`
154 |
155 | The ratio of the lazyframe. Possible values: 16:9, 4:3, 1:1
156 |
157 | ### `data-initinview`
158 |
159 | Set this to true if you want the resource to execute (for example video to play) when the element is in view.
160 |
161 | ## License
162 |
163 | [MIT](https://opensource.org/licenses/MIT). © 2016 Viktor Bergehall
164 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazyframe",
3 | "description": "Dependency-free library for lazyloading iframes",
4 | "main": "dist/lazyframe.min.js",
5 | "authors": [
6 | "Viktor Bergehall"
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "lazyload",
11 | "lazyloading",
12 | "iframe",
13 | "pagespeed",
14 | "pageload"
15 | ],
16 | "homepage": "https://github.com/vb/lazyframe",
17 | "ignore": [
18 | "**/.*",
19 | "node_modules",
20 | "bower_components",
21 | "test",
22 | "tests"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lazyframe Demo
7 |
8 |
9 |
51 |
52 |
53 |
54 |
55 |
Youtube video with thumbnail and title from API
56 |
57 |
58 |
59 |
Youtube video with custom thumbnail and title
60 |
62 |
63 |
64 |
Vimeo video with thumbnail and title from API
65 |
66 |
67 |
68 |
Vimeo video with custom thumbnail and title
69 |
71 |
72 |
73 |
Iframe with custom theme and title
74 |
77 |
78 |
79 |
Google maps iframe with custom thumbnail and title that gets executed when in view
80 |
83 |
84 |
85 |
86 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lazyframe",
3 | "description": "Dependency-free library for lazyloading iframes",
4 | "version": "2.2.7",
5 | "author": "Viktor Bergehall",
6 | "bugs": {
7 | "url": "https://github.com/vb/lazyframe/issues"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.12.17",
11 | "@babel/plugin-transform-object-assign": "^7.12.13",
12 | "@babel/preset-env": "^7.12.17",
13 | "@rollup/plugin-babel": "^5.3.0",
14 | "ava": "^3.15.0",
15 | "jsdom": "15",
16 | "rollup": "^2.39.0",
17 | "rollup-plugin-scss": "^3.0.0",
18 | "rollup-plugin-terser": "^7.0.2",
19 | "sass": "^1.42.1"
20 | },
21 | "homepage": "https://vb.github.io/lazyframe",
22 | "jsnext:main": "src/lazyframe.js",
23 | "keywords": [
24 | "embed",
25 | "iframe",
26 | "javascript",
27 | "lazyload",
28 | "lazyloading",
29 | "pageload",
30 | "pagespeed",
31 | "vimeo",
32 | "youtube"
33 | ],
34 | "license": "MIT",
35 | "main": "dist/lazyframe.min.js",
36 | "repository": {
37 | "type": "git",
38 | "url": "git+https://github.com/vb/lazyframe.git"
39 | },
40 | "scripts": {
41 | "build": "./node_modules/.bin/rollup -c",
42 | "dev": "./node_modules/.bin/rollup -c -w",
43 | "test": "npm run build && ava -s",
44 | "test:watch": "npm run build && ava -s -w"
45 | },
46 | "files": [
47 | "dist",
48 | "src"
49 | ],
50 | "dependencies": {}
51 | }
52 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { babel } from "@rollup/plugin-babel";
2 | import { terser } from "rollup-plugin-terser";
3 | import scss from "rollup-plugin-scss";
4 |
5 | export default {
6 | input: "src/lazyframe.js",
7 | output: {
8 | file: "dist/lazyframe.min.js",
9 | format: "umd",
10 | exports: "default",
11 | name: "lazyframe",
12 | sourcemap: true,
13 | },
14 | plugins: [
15 | babel({
16 | exclude: "node_modules/**",
17 | babelHelpers: "bundled",
18 | }),
19 | terser(),
20 | scss({
21 | output: "dist/lazyframe.css",
22 | outputStyle: "compressed",
23 | }),
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/src/lazyframe.js:
--------------------------------------------------------------------------------
1 | import './scss/lazyframe.scss'
2 |
3 | const Lazyframe = () => {
4 |
5 | let settings;
6 |
7 | const elements = [];
8 |
9 | const defaults = {
10 | vendor: undefined,
11 | id: undefined,
12 | src: undefined,
13 | thumbnail: undefined,
14 | title: undefined,
15 | initialized: false,
16 | y: undefined,
17 | debounce: 250,
18 | lazyload: true,
19 | autoplay: true,
20 | initinview: false,
21 | onLoad: (l) => {},
22 | onAppend: (l) => {},
23 | onThumbnailLoad: (img) => {}
24 | };
25 |
26 | const constants = {
27 | regex: {
28 | youtube_nocookie: /(?:youtube-nocookie\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=)))([a-zA-Z0-9_-]{6,11})/,
29 | youtube: /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/,
30 | vimeo: /vimeo\.com\/(?:video\/)?([0-9]*)(?:\?|)/,
31 | },
32 | condition: {
33 | youtube: (m) => (m && m[1].length == 11 ? m[1] : false),
34 | youtube_nocookie: (m) => (m && m[1].length == 11 ? m[1] : false),
35 | vimeo: (m) =>
36 | (m && m[1].length === 9) || m[1].length === 8 ? m[1] : false,
37 | },
38 | src: {
39 | youtube: (s) =>
40 | `https://www.youtube.com/embed/${s.id}/?autoplay=${
41 | s.autoplay ? "1" : "0"
42 | }&${s.query}`,
43 | youtube_nocookie: (s) =>
44 | `https://www.youtube-nocookie.com/embed/${s.id}/?autoplay=${
45 | s.autoplay ? "1" : "0"
46 | }&${s.query}`,
47 | vimeo: (s) =>
48 | `https://player.vimeo.com/video/${s.id}/?autoplay=${
49 | s.autoplay ? "1" : "0"
50 | }&${s.query}`,
51 | },
52 | endpoint: (s) => `https://noembed.com/embed?url=${s.src}`,
53 | response: {
54 | title: (r) => r.title,
55 | thumbnail: (r) => r.thumbnail_url,
56 | },
57 | };
58 |
59 | function init(elements, ...args) {
60 | settings = Object.assign({}, defaults, args[0]);
61 |
62 | if (typeof elements === 'string') {
63 |
64 | const selector = document.querySelectorAll(elements);
65 | for (let i = 0; i < selector.length; i++) {
66 | loop(selector[i]);
67 | }
68 | } else if (typeof elements.length === 'undefined'){
69 | loop(elements);
70 | } else {
71 | for (let i = 0; i < elements.length; i++) {
72 | loop(elements[i]);
73 | }
74 | }
75 |
76 | if (settings.lazyload) {
77 | scroll();
78 | }
79 |
80 | }
81 |
82 | function loop(el) {
83 |
84 | if(el instanceof HTMLElement === false ||
85 | el.classList.contains('lazyframe--loaded')) return;
86 |
87 | const lazyframe = {
88 | el: el,
89 | settings: setup(el),
90 | };
91 |
92 | lazyframe.el.addEventListener('click', () => {
93 | lazyframe.el.appendChild(lazyframe.iframe);
94 |
95 | const iframe = el.querySelectorAll('iframe');
96 | lazyframe.settings.onAppend.call(this, iframe[0]);
97 | });
98 |
99 | if (settings.lazyload) {
100 | build(lazyframe);
101 | } else {
102 | api(lazyframe, !!lazyframe.settings.thumbnail);
103 | }
104 |
105 | }
106 |
107 | function setup(el) {
108 |
109 | const attr = Array.prototype.slice.apply(el.attributes)
110 | .filter(att => att.value !== '')
111 | .reduce((obj, curr) => {
112 | let name = curr.name.indexOf('data-') === 0 ? curr.name.split('data-')[1] : curr.name;
113 | obj[name] = curr.value;
114 | return obj;
115 | }, {});
116 |
117 | const options = Object.assign({},
118 | settings,
119 | attr,
120 | {
121 | y: el.offsetTop,
122 | originalSrc: attr.src,
123 | query: getQuery(attr.src)
124 | }
125 | );
126 |
127 | if (options.vendor) {
128 | const match = options.src.match(constants.regex[options.vendor]);
129 | options.id = constants.condition[options.vendor](match);
130 | }
131 |
132 | return options;
133 |
134 | }
135 |
136 | function getQuery(src) {
137 | const query = src.split('?');
138 | return query[1] ? query[1] : null
139 | }
140 |
141 | function useApi(settings) {
142 | if (!settings.vendor) return false;
143 | return !settings.title || !settings.thumbnail;
144 | }
145 |
146 | function api(lazyframe) {
147 |
148 | if (useApi(lazyframe.settings)) {
149 | send(lazyframe, (err, data) => {
150 | if (err) return;
151 |
152 | const response = data[0];
153 | const _l = data[1];
154 |
155 | if (!_l.settings.title) {
156 | _l.settings.title = constants.response.title(response);
157 | }
158 | if (!_l.settings.thumbnail) {
159 | const url = constants.response.thumbnail(response);
160 | _l.settings.thumbnail = url;
161 | lazyframe.settings.onThumbnailLoad.call(this, url);
162 | }
163 | build(_l, true);
164 |
165 | });
166 |
167 | }else{
168 | build(lazyframe, true);
169 | }
170 |
171 | }
172 |
173 | function send(lazyframe, cb) {
174 |
175 | const endpoint = constants.endpoint(lazyframe.settings);
176 | const request = new XMLHttpRequest();
177 |
178 | request.open('GET', endpoint, true);
179 |
180 | request.onload = function() {
181 | if (request.status >= 200 && request.status < 400) {
182 | const data = JSON.parse(request.responseText);
183 | cb(null, [data, lazyframe]);
184 | } else {
185 | cb(true);
186 | }
187 | };
188 |
189 | request.onerror = function() {
190 | cb(true);
191 | };
192 |
193 | request.send();
194 |
195 | }
196 |
197 | function scroll() {
198 |
199 | const height = window.innerHeight;
200 | let count = elements.length;
201 | const initElement = (el, i) => {
202 | el.settings.initialized = true;
203 | el.el.classList.add('lazyframe--loaded');
204 | count--;
205 | api(el);
206 |
207 | if (el.settings.initinview) {
208 | el.el.click();
209 | }
210 |
211 | el.settings.onLoad.call(this, el);
212 | }
213 |
214 | elements
215 | .filter(el => el.settings.y < height)
216 | .forEach(initElement);
217 |
218 | const onScroll = debounce(() => {
219 |
220 | up = lastY < window.pageYOffset;
221 | lastY = window.pageYOffset;
222 |
223 | if (up) {
224 | elements
225 | .filter(el => el.settings.y < (height + lastY) && el.settings.initialized === false)
226 | .forEach(initElement);
227 | }
228 |
229 | if (count === 0) {
230 | window.removeEventListener('scroll', onScroll, false);
231 | }
232 |
233 | }, settings.debounce);
234 |
235 | let lastY = 0;
236 | let up = false;
237 |
238 | window.addEventListener('scroll', onScroll, false);
239 |
240 | function debounce(func, wait, immediate) {
241 | let timeout;
242 | return function() {
243 | let context = this, args = arguments;
244 | let later = function() {
245 | timeout = null;
246 | if (!immediate) func.apply(context, args);
247 | };
248 | let callNow = immediate && !timeout;
249 | clearTimeout(timeout);
250 | timeout = setTimeout(later, wait);
251 | if (callNow) func.apply(context, args);
252 | };
253 | };
254 |
255 | }
256 |
257 | function build(lazyframe, loadImage) {
258 |
259 | lazyframe.iframe = getIframe(lazyframe.settings);
260 |
261 | if (lazyframe.settings.thumbnail && loadImage) {
262 | lazyframe.el.style.backgroundImage = `url(${lazyframe.settings.thumbnail})`;
263 | }
264 |
265 | if (lazyframe.settings.title && lazyframe.el.children.length === 0) {
266 | const docfrag = document.createDocumentFragment(),
267 | titleNode = document.createElement('span');
268 |
269 | titleNode.className = 'lazyframe__title';
270 | titleNode.innerHTML = lazyframe.settings.title;
271 | docfrag.appendChild(titleNode);
272 |
273 | lazyframe.el.appendChild(docfrag);
274 | }
275 |
276 | if (!settings.lazyload) {
277 | lazyframe.el.classList.add('lazyframe--loaded');
278 | lazyframe.settings.onLoad.call(this, lazyframe);
279 | elements.push(lazyframe);
280 | }
281 |
282 | if (!lazyframe.settings.initialized) {
283 | elements.push(lazyframe);
284 | }
285 |
286 | }
287 |
288 | function getIframe(settings) {
289 |
290 | const docfrag = document.createDocumentFragment();
291 | const iframeNode = document.createElement('iframe');
292 |
293 | if (settings.vendor) {
294 | settings.src = constants.src[settings.vendor](settings);
295 | }
296 |
297 | iframeNode.setAttribute('id', `lazyframe-${settings.id}`);
298 | iframeNode.setAttribute('src', settings.src);
299 | iframeNode.setAttribute('frameborder', 0);
300 | iframeNode.setAttribute('allowfullscreen', '');
301 |
302 | if (settings.autoplay) {
303 | iframeNode.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
304 | }
305 |
306 | docfrag.appendChild(iframeNode);
307 | return docfrag;
308 |
309 | }
310 |
311 | return init;
312 |
313 | }
314 |
315 | const lf = Lazyframe();
316 |
317 | export default lf;
318 |
--------------------------------------------------------------------------------
/src/scss/_base.scss:
--------------------------------------------------------------------------------
1 | .lazyframe {
2 | position: relative;
3 |
4 | background-color: currentColor;
5 | background-repeat: no-repeat;
6 | background-size: cover;
7 | background-position: center;
8 |
9 | &__title {
10 | position: absolute;
11 | top: 0;
12 | right: 0;
13 | left: 0;
14 | padding: 15px 17px;
15 | z-index: 3;
16 |
17 | &::after {
18 | z-index: -1;
19 | }
20 |
21 | }
22 |
23 | &:hover {
24 | cursor: pointer;
25 | }
26 |
27 | &::before {
28 | display: block;
29 | content: "";
30 | width: 100%;
31 | padding-top: 100%;
32 | }
33 |
34 | &[data-ratio="16:9"]::before { padding-top: 56.25%; }
35 |
36 | &[data-ratio="4:3"]::before { padding-top: 75%; }
37 |
38 | &[data-ratio="1:1"]::before { padding-top: 100%; }
39 |
40 | iframe {
41 | position: absolute;
42 | top: 0;
43 | right: 0;
44 | bottom: 0;
45 | left: 0;
46 | z-index: 5;
47 | width: 100%;
48 | height: 100%;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/scss/lazyframe.scss:
--------------------------------------------------------------------------------
1 | @import "base";
2 | @import "themes/youtube";
3 | @import "themes/vimeo";
4 |
--------------------------------------------------------------------------------
/src/scss/themes/_vimeo.scss:
--------------------------------------------------------------------------------
1 | .lazyframe[data-vendor="vimeo"] {
2 | background-color: #00adef;
3 |
4 | .lazyframe__title {
5 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
6 | color: #00adef;
7 | font-size: 20px;
8 | font-weight: 700;
9 | text-rendering: optimizeLegibility;
10 | user-select: none;
11 | -webkit-font-smoothing: auto;
12 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
13 | background-color: rgba(0, 0, 0, .5);
14 | }
15 |
16 | &::before {
17 | padding-top: 48.25%;
18 | }
19 |
20 | &::after {
21 | content: "";
22 | height: 40px;
23 | width: 65px;
24 | display: block;
25 | bottom: 10px;
26 | left: 10px;
27 | z-index: 3;
28 | background-color: rgba(0, 0, 0, .5);
29 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' baseProfile='tiny' viewBox='0 0 24 24'%3E%3Cpath fill='%23FFF' d='M7.765 16.89l8.47-4.89-8.47-4.89'/%3E%3C/svg%3E");
30 | background-position: center center;
31 | background-size: 100% 100%;
32 | background-repeat: no-repeat;
33 | border-radius: 5px;
34 | position: relative;
35 | }
36 |
37 | &:hover::after {
38 | background-color: #00adef;
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/scss/themes/_youtube.scss:
--------------------------------------------------------------------------------
1 | .lazyframe[data-vendor="youtube"],
2 | .lazyframe[data-vendor="youtube_nocookie"] {
3 | background-color: #e52d27;
4 | font-family: Roboto, Arial, Helvetica, sans-serif;
5 |
6 | .lazyframe__title {
7 | color: rgb(238, 238, 238);
8 | font-family: Roboto, Arial, Helvetica, sans-serif;
9 | font-size: 18px;
10 | text-shadow: rgba(0, 0, 0, .498039) 0 0 2px;
11 | -webkit-font-smoothing: antialiased;
12 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
13 | transition: color .1s cubic-bezier(.4, 0, 1, 1);
14 |
15 | &:hover {
16 | color: #fff;
17 | }
18 |
19 | &::before {
20 | content: "";
21 | display: block;
22 | background: linear-gradient(rgba(0, 0, 0, .2), transparent);
23 | height: 98px;
24 | width: 100%;
25 | pointer-events: none;
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | right: 0;
30 | z-index: -1;
31 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
32 | }
33 |
34 | }
35 |
36 | &::before {
37 | padding-top: 56.25%;
38 | }
39 |
40 | &::after {
41 | content: "";
42 | position: absolute;
43 | left: 50%;
44 | top: 50%;
45 | width: 68px;
46 | height: 48px;
47 | margin-left: -34px;
48 | margin-top: -24px;
49 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%231F1F1F' d='M255.7 446.3c-53.3.3-106.6-.4-159.8-3.3-17.4-1-34.7-2.5-50.4-11C35 426.3 27 418.4 22 407.2 13.2 388.6 10.5 369 9 349c-3.4-41.3-3.6-82.6-1.8-123.8 1-22 1.6-44 6.8-65.5 2-8.4 5-16.6 8.8-24.4C32 117 48 108 67.3 104c16.2-3 32.8-3 49.3-3.7 56-2.3 112-3.5 168-3 43 .6 86.2 1.7 129.3 4 13.2.6 26.6.8 39.3 5.5 17.2 6.4 30 17.2 37 34.7 6.6 16.8 9.2 34.2 10.6 52 3.8 48.7 4 97.3.7 146-1 16.3-2.2 32.7-6.5 48.8-9.7 37-32.8 51.5-66.7 53.8-36.2 2.5-72.5 3.8-108.8 4.3-21.3.2-42.7 0-64 0zM203.2 344L348 264.7l-144.8-79.3V344z'/%3E%3Cpath fill='%23FEFDFD' d='M203.2 344V185.5L348 264.8 203.2 344z'/%3E%3C/svg%3E");
50 | background-position: center center;
51 | background-size: 100%;
52 | background-repeat: no-repeat;
53 | opacity: .81;
54 |
55 | border: none;
56 | z-index: 4;
57 | }
58 |
59 | &:hover::after {
60 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23DD2C28' d='M255.7 446.3c-53.3.3-106.6-.4-159.8-3.3-17.4-1-34.7-2.5-50.4-11C35 426.3 27 418.4 22 407.2 13.2 388.6 10.5 369 9 349c-3.4-41.3-3.6-82.6-1.8-123.8 1-22 1.6-44 6.8-65.5 2-8.4 5-16.6 8.8-24.4C32 117 48 108 67.3 104c16.2-3 32.8-3 49.3-3.7 56-2.3 112-3.5 168-3 43 .6 86.2 1.7 129.3 4 13.2.6 26.6.8 39.3 5.5 17.2 6.4 30 17.2 37 34.7 6.6 16.8 9.2 34.2 10.6 52 3.8 48.7 4 97.3.7 146-1 16.3-2.2 32.7-6.5 48.8-9.7 37-32.8 51.5-66.7 53.8-36.2 2.5-72.5 3.8-108.8 4.3-21.3.2-42.7 0-64 0zM203.2 344L348 264.7l-144.8-79.3V344z'/%3E%3Cpath fill='%23FEFDFD' d='M203.2 344V185.5L348 264.8 203.2 344z'/%3E%3C/svg%3E");
61 | opacity: 1;
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/test/browser.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { readFileSync } = require('fs');
3 | const { Script } = require('vm');
4 |
5 | const test = require('ava');
6 | const { JSDOM, VirtualConsole } = require('jsdom');
7 |
8 | const virtualConsole = new VirtualConsole();
9 | virtualConsole.sendTo(console);
10 |
11 | const script = new Script(
12 | readFileSync(path.join(__dirname, '..', 'dist', 'lazyframe.min.js'))
13 | );
14 |
15 | test.beforeEach(t => {
16 | const dom = new JSDOM(``, {
17 | includeNodeLocations: true,
18 | resources: 'usable',
19 | runScripts: 'dangerously',
20 | virtualConsole
21 | });
22 |
23 | dom.runVMScript(script);
24 | global.document = dom.window.document;
25 | global.window = dom.window;
26 | })
27 |
28 | const createDomNode = (params = {}) => {
29 | const node = document.createElement('div');
30 | node.classList.add('lazyframe');
31 | for (const [ key, value ] of Object.entries(params)) {
32 | node.setAttribute(`data-${key}`, value)
33 | }
34 | document.body.appendChild(node);
35 | return node;
36 | }
37 |
38 | test('should expose lazyframe()', (t) => {
39 | t.true(typeof window.lazyframe === 'function');
40 | });
41 |
42 | test('should initialize one node with a string selector', (t) => {
43 | createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0' });
44 | window.lazyframe('.lazyframe');
45 | t.is(document.querySelectorAll('.lazyframe--loaded').length, 1);
46 | })
47 |
48 | test('should initialize mulitple nodes with a string selector', (t) => {
49 | createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0' });
50 | createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0' });
51 |
52 | window.lazyframe('.lazyframe');
53 | t.is(document.querySelectorAll('.lazyframe--loaded').length, 2);
54 | })
55 |
56 | test('should initialize with a single node', (t) => {
57 | const node = createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0' });
58 |
59 | window.lazyframe(node);
60 | t.is(document.querySelectorAll('.lazyframe--loaded').length, 1);
61 | })
62 |
63 | test('should initialize with a nodelist', (t) => {
64 | createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDB/?rel=0' });
65 | createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDC/?rel=0' });
66 |
67 | const nodes = document.querySelectorAll('.lazyframe')
68 | window.lazyframe(nodes);
69 | t.is(document.querySelectorAll('.lazyframe--loaded').length, 2);
70 | })
71 |
72 | test('should append an iframe on click', (t) => {
73 | const node = createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0' });
74 |
75 | window.lazyframe('.lazyframe');
76 | node.click();
77 |
78 | t.assert(node.querySelector('iframe'))
79 | })
80 |
81 | test('should call onAppend callback function', (t) => {
82 | let i = 0;
83 | const node1 = createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0' });
84 | const node2 = createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0' });
85 |
86 | window.lazyframe('.lazyframe', {
87 | onAppend() {
88 | i++;
89 | }
90 | });
91 | node1.click();
92 | node2.click();
93 |
94 | t.is(i, 2)
95 | })
96 |
97 | test('should use data-title', (t) => {
98 | const title = 'custom title'
99 | const node = createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?rel=0', title });
100 |
101 | window.lazyframe('.lazyframe');
102 |
103 | node.click()
104 | t.is(document.querySelector('.lazyframe__title').textContent, title)
105 | })
106 |
107 | test('should append optional query params from data-src', (t) => {
108 | const query = 'rel=0&p=1'
109 | const node = createDomNode({ vendor: 'youtube', src: 'http://www.youtube.com/embed/iwGFalTRHDA/?' + query });
110 |
111 | window.lazyframe('.lazyframe');
112 |
113 | node.click()
114 | const iframe = node.querySelector('iframe');
115 | const src = iframe.getAttribute('src');
116 | const [,iframQuery] = src.split('?autoplay=1&')
117 |
118 | t.is(iframQuery, query);
119 | })
--------------------------------------------------------------------------------
/test/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const test = require('ava');
4 |
5 | const lazyframe = require('..');
6 |
7 | test('should expose lazyframe()', (t) => {
8 | t.true(typeof lazyframe === 'function');
9 | });
10 |
--------------------------------------------------------------------------------