Skip to content

Commit 67d6e51

Browse files
committed
fix: refactor into ts
1 parent 1deaf81 commit 67d6e51

File tree

3 files changed

+343
-0
lines changed

3 files changed

+343
-0
lines changed

declarations.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module '*.scss?raw';

src/lazyframe.ts

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// lazyframe.ts
2+
3+
import './scss/lazyframe.scss';
4+
5+
// --- Type Definitions ---
6+
7+
type Vendor = 'youtube' | 'youtube_nocookie' | 'vimeo';
8+
9+
// Options the user can pass during initialization
10+
interface LazyframeOptions {
11+
vendor?: Vendor;
12+
id?: string;
13+
src?: string;
14+
thumbnail?: string;
15+
title?: string;
16+
lazyload?: boolean;
17+
autoplay?: boolean;
18+
initinview?: boolean;
19+
loadThumbnail?: boolean;
20+
showPlayButton?: boolean;
21+
onLoad?: (instance: LazyframeInstance) => void;
22+
onAppend?: (iframe: HTMLIFrameElement) => void;
23+
onThumbnailLoad?: (imgUrl: string) => void;
24+
}
25+
26+
// Fully resolved settings for an instance, merging defaults and data-attributes
27+
interface LazyframeSettings extends LazyframeOptions {
28+
initialized: boolean;
29+
originalSrc?: string;
30+
query?: string | null;
31+
}
32+
33+
// The internal representation of a single lazyframe instance
34+
interface LazyframeInstance {
35+
el: HTMLElement;
36+
settings: LazyframeSettings;
37+
iframe?: DocumentFragment;
38+
}
39+
40+
// --- Library Code ---
41+
42+
const Lazyframe = () => {
43+
let settings: LazyframeOptions;
44+
const elements: LazyframeInstance[] = [];
45+
46+
const defaults: LazyframeSettings = {
47+
initialized: false,
48+
lazyload: true,
49+
autoplay: true,
50+
loadThumbnail: true,
51+
initinview: false,
52+
showPlayButton: true,
53+
onLoad: () => {},
54+
onAppend: () => {},
55+
onThumbnailLoad: () => {}
56+
};
57+
58+
const constants = {
59+
regex: {
60+
youtube_nocookie: /(?:youtube-nocookie\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=)))([a-zA-Z0-9_-]{6,11})/,
61+
youtube: /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/,
62+
vimeo: /vimeo\.com\/(?:video\/)?([0-9]*)(?:\?|)/,
63+
},
64+
condition: {
65+
youtube: (m: RegExpMatchArray | null): string | false => (m && m[1].length === 11 ? m[1] : false),
66+
youtube_nocookie: (m: RegExpMatchArray | null): string | false => (m && m[1].length === 11 ? m[1] : false),
67+
vimeo: (m: RegExpMatchArray | null): string | false =>
68+
(m && (m[1].length === 10 || m[1].length === 9 || m[1].length === 8)) ? m[1] : false,
69+
},
70+
src: {
71+
youtube: (s: LazyframeSettings): string =>
72+
`https://www.youtube.com/embed/${s.id}/?autoplay=${s.autoplay ? "1" : "0"}&${s.query || ''}`,
73+
youtube_nocookie: (s: LazyframeSettings): string =>
74+
`https://www.youtube-nocookie.com/embed/${s.id}/?autoplay=${s.autoplay ? "1" : "0"}&${s.query || ''}`,
75+
vimeo: (s: LazyframeSettings): string =>
76+
`https://player.vimeo.com/video/${s.id}/?autoplay=${s.autoplay ? "1" : "0"}&${s.query || ''}`,
77+
},
78+
endpoint: (s: LazyframeSettings): string => {
79+
if (s.vendor === 'youtube') {
80+
return `https://noembed.com/embed?url=https://www.youtube.com/watch?v=${s.id}`;
81+
}
82+
return `https://noembed.com/embed?url=${s.src}`;
83+
},
84+
response: {
85+
title: (r: { title: string }): string => r.title,
86+
thumbnail: (r: { thumbnail_url: string }): string => r.thumbnail_url,
87+
},
88+
};
89+
90+
function init(selector: string | HTMLElement | NodeListOf<HTMLElement>, userOptions?: LazyframeOptions): void {
91+
settings = { ...defaults, ...userOptions };
92+
93+
const els = typeof selector === 'string' ? document.querySelectorAll<HTMLElement>(selector) : selector;
94+
95+
if (els instanceof HTMLElement) {
96+
loop(els);
97+
} else {
98+
els.forEach(loop);
99+
}
100+
101+
if (settings.lazyload) {
102+
setObservers();
103+
}
104+
}
105+
106+
function loop(el: HTMLElement): void {
107+
if (!(el instanceof HTMLElement) || el.classList.contains('lazyframe--loaded')) return;
108+
109+
const instance: LazyframeInstance = {
110+
el: el,
111+
settings: setup(el),
112+
};
113+
114+
instance.el.addEventListener('click', () => {
115+
if (instance.iframe) {
116+
instance.el.appendChild(instance.iframe);
117+
}
118+
instance.el.classList.add('lazyframe--activated');
119+
120+
const iframe = el.querySelector<HTMLIFrameElement>('iframe');
121+
if (iframe && instance.settings.onAppend) {
122+
instance.settings.onAppend(iframe);
123+
}
124+
});
125+
126+
if (settings.lazyload) {
127+
build(instance);
128+
} else {
129+
api(instance);
130+
}
131+
}
132+
133+
function setup(el: HTMLElement): LazyframeSettings {
134+
const dataAttributes: { [key: string]: any } = Array.from(el.attributes)
135+
.filter(attr => attr.value !== '')
136+
.reduce((obj, curr) => {
137+
const name = curr.name.startsWith('data-') ? curr.name.substring(5) : curr.name;
138+
obj[name] = curr.value;
139+
return obj;
140+
}, {} as { [key: string]: any });
141+
142+
const options: LazyframeSettings = {
143+
...settings,
144+
...dataAttributes,
145+
initialized: false, // Ensure this is reset per-instance
146+
originalSrc: dataAttributes.src,
147+
query: getQuery(dataAttributes.src)
148+
};
149+
150+
// Coerce boolean data-attributes from string to boolean
151+
['lazyload', 'autoplay', 'initinview', 'loadThumbnail', 'showPlayButton'].forEach(option => {
152+
if (options[option as keyof LazyframeOptions] === 'false') {
153+
(options as any)[option] = false;
154+
}
155+
});
156+
157+
if (options.vendor && options.src) {
158+
const match = options.src.match(constants.regex[options.vendor]);
159+
const condition = constants.condition[options.vendor];
160+
const id = condition(match);
161+
if (id) {
162+
options.id = id;
163+
}
164+
}
165+
166+
return options;
167+
}
168+
169+
function getQuery(src: string): string | null {
170+
if (!src) return null;
171+
const query = src.split('?');
172+
return query[1] ? query[1] : null;
173+
}
174+
175+
function useApi(settings: LazyframeSettings): boolean {
176+
if (!settings.vendor) return false;
177+
return !settings.title || !settings.thumbnail;
178+
}
179+
180+
function api(instance: LazyframeInstance): void {
181+
if (useApi(instance.settings)) {
182+
send(instance, (err, data) => {
183+
if (err || !data) return;
184+
185+
const response = data[0];
186+
const _instance = data[1];
187+
188+
if (!_instance.settings.title) {
189+
_instance.settings.title = constants.response.title(response);
190+
}
191+
if (!_instance.settings.thumbnail) {
192+
const url = constants.response.thumbnail(response);
193+
_instance.settings.thumbnail = url;
194+
if (_instance.settings.onThumbnailLoad) {
195+
_instance.settings.onThumbnailLoad(url);
196+
}
197+
}
198+
build(_instance, true);
199+
});
200+
} else {
201+
build(instance, true);
202+
}
203+
}
204+
205+
function send(instance: LazyframeInstance, cb: (err: boolean | null, data?: [any, LazyframeInstance]) => void): void {
206+
const endpoint = constants.endpoint(instance.settings);
207+
const request = new XMLHttpRequest();
208+
209+
request.open('GET', endpoint, true);
210+
211+
request.onload = function () {
212+
if (request.status >= 200 && request.status < 400) {
213+
const data = JSON.parse(request.responseText);
214+
cb(null, [data, instance]);
215+
} else {
216+
cb(true);
217+
}
218+
};
219+
220+
request.onerror = function () { cb(true); };
221+
request.send();
222+
}
223+
224+
function setPlayBtn(btnTxt: string = 'Play'): HTMLButtonElement {
225+
const playButton = document.createElement('button');
226+
playButton.type = 'button';
227+
playButton.classList.add('lf-play-btn');
228+
playButton.innerHTML = `<span class="visually-hidden">${btnTxt}</span>`;
229+
return playButton;
230+
}
231+
232+
function setObservers(): void {
233+
const initElement = (instance: LazyframeInstance) => {
234+
if (instance.settings.initialized) return;
235+
236+
instance.settings.initialized = true;
237+
instance.el.classList.add('lazyframe--loaded');
238+
if (instance.settings.showPlayButton) {
239+
instance.el.appendChild(setPlayBtn());
240+
}
241+
api(instance);
242+
243+
if (instance.settings.initinview) {
244+
instance.el.click();
245+
}
246+
247+
if (instance.settings.onLoad) {
248+
instance.settings.onLoad(instance);
249+
}
250+
}
251+
252+
if ('IntersectionObserver' in window) {
253+
const lazyframeObserver = new IntersectionObserver((entries) => {
254+
entries.forEach((entry) => {
255+
if (entry.isIntersecting) {
256+
const instance = elements.find(element => element.el === entry.target);
257+
if (instance) {
258+
initElement(instance);
259+
lazyframeObserver.unobserve(entry.target);
260+
}
261+
}
262+
});
263+
});
264+
265+
elements.forEach((instance) => {
266+
lazyframeObserver.observe(instance.el);
267+
});
268+
} else {
269+
elements.forEach(initElement);
270+
}
271+
}
272+
273+
function build(instance: LazyframeInstance, loadImage?: boolean): void {
274+
instance.iframe = getIframe(instance.settings);
275+
276+
if (instance.settings.thumbnail && loadImage && instance.settings.loadThumbnail) {
277+
instance.el.style.backgroundImage = `url(${instance.settings.thumbnail})`;
278+
}
279+
280+
if (instance.settings.title && instance.el.children.length === 0) {
281+
const titleNode = document.createElement('span');
282+
titleNode.className = 'lazyframe__title';
283+
titleNode.textContent = instance.settings.title;
284+
instance.el.appendChild(titleNode);
285+
}
286+
287+
if (!settings.lazyload) {
288+
instance.el.classList.add('lazyframe--loaded');
289+
if(instance.settings.onLoad) {
290+
instance.settings.onLoad(instance);
291+
}
292+
}
293+
294+
if (!instance.settings.initialized) {
295+
elements.push(instance);
296+
}
297+
}
298+
299+
function getIframe(settings: LazyframeSettings): DocumentFragment {
300+
const docfrag = document.createDocumentFragment();
301+
const iframeNode = document.createElement('iframe');
302+
303+
if (settings.vendor && constants.src[settings.vendor]) {
304+
settings.src = constants.src[settings.vendor](settings);
305+
}
306+
307+
iframeNode.setAttribute('id', `lazyframe-${settings.id}`);
308+
iframeNode.setAttribute('src', settings.src || '');
309+
iframeNode.setAttribute('frameborder', '0');
310+
iframeNode.setAttribute('allowfullscreen', '');
311+
312+
if (settings.autoplay) {
313+
iframeNode.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
314+
}
315+
316+
docfrag.appendChild(iframeNode);
317+
return docfrag;
318+
}
319+
return init;
320+
}
321+
322+
const lazyframe = Lazyframe();
323+
324+
export default lazyframe;

tsconfig.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2015",
4+
"module": "ESNext",
5+
"moduleResolution": "node",
6+
"strict": true,
7+
"importHelpers": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"declaration": false,
12+
"declarationMap": false,
13+
"pretty": true,
14+
"sourceMap": false,
15+
"outDir": "dist",
16+
},
17+
"include": ["src/**/*"]
18+
}

0 commit comments

Comments
 (0)