-
Notifications
You must be signed in to change notification settings - Fork 142
/
lazyload-image.ts
135 lines (121 loc) · 4.84 KB
/
lazyload-image.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import {
filter,
tap,
take,
map,
mergeMap,
catchError,
} from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
import { Observable } from 'rxjs/Observable';
import { getScrollListener } from './scroll-listener';
import { Rect } from './rect';
import { cssClassNames } from './constants';
import { hasCssClassName, removeCssClassName, addCssClassName } from './utils';
export function isVisible(element: HTMLElement, threshold = 0, _window: Window, scrollContainer?: HTMLElement) {
const elementBounds = Rect.fromElement(element);
const windowBounds = Rect.fromWindow(_window);
elementBounds.inflate(threshold);
if (scrollContainer) {
const scrollContainerBounds = Rect.fromElement(scrollContainer);
const intersection = scrollContainerBounds.getIntersectionWith(windowBounds);
return elementBounds.intersectsWith(intersection);
} else {
return elementBounds.intersectsWith(windowBounds);
}
}
export function isChildOfPicture(element: HTMLImageElement | HTMLDivElement): boolean {
return Boolean(element.parentElement && element.parentElement.nodeName.toLowerCase() === 'picture');
}
export function isImageElement(element: HTMLImageElement | HTMLDivElement): element is HTMLImageElement {
return element.nodeName.toLowerCase() === 'img';
}
function loadImage(element: HTMLImageElement | HTMLDivElement, imagePath: string, useSrcset: boolean): Observable<string> {
let img: HTMLImageElement;
if (isImageElement(element) && isChildOfPicture(element)) {
const parentClone = element.parentNode.cloneNode(true) as HTMLPictureElement;
img = parentClone.getElementsByTagName('img')[0];
setSourcesToLazy(img);
setImage(img, imagePath, useSrcset);
} else {
img = new Image();
if (isImageElement(element) && element.sizes) {
img.sizes = element.sizes;
}
if (useSrcset) {
img.srcset = imagePath;
} else {
img.src = imagePath;
}
}
return Observable
.create(observer => {
img.onload = () => {
observer.next(imagePath);
observer.complete();
};
img.onerror = err => {
observer.error(null);
};
});
}
function setImage(element: HTMLImageElement | HTMLDivElement, imagePath: string, useSrcset: boolean) {
if (isImageElement(element)) {
if (useSrcset) {
element.srcset = imagePath;
} else {
element.src = imagePath;
}
} else {
element.style.backgroundImage = `url('${imagePath}')`;
}
return element;
}
function setSources(attrName: string) {
return (image: HTMLImageElement) => {
const sources = image.parentElement.getElementsByTagName('source');
for (let i = 0; i < sources.length; i++) {
const attrValue = sources[i].getAttribute(attrName);
if (attrValue) {
sources[i].srcset = attrValue;
}
}
};
}
const setSourcesToDefault = setSources('defaultImage');
const setSourcesToLazy = setSources('lazyLoad');
const setSourcesToError = setSources('errorImage');
function setImageAndSources(setSourcesFn: (image: HTMLImageElement) => void) {
return (element: HTMLImageElement | HTMLDivElement, imagePath: string, useSrcset: boolean) => {
if (isImageElement(element) && isChildOfPicture(element)) {
setSourcesFn(element);
}
if (imagePath) {
setImage(element, imagePath, useSrcset);
}
};
}
const setImageAndSourcesToDefault = setImageAndSources(setSourcesToDefault);
const setImageAndSourcesToLazy = setImageAndSources(setSourcesToLazy);
const setImageAndSourcesToError = setImageAndSources(setSourcesToError);
export function lazyLoadImage(element: HTMLImageElement | HTMLDivElement, imagePath: string, defaultImagePath: string, errorImgPath: string, offset: number, useSrcset: boolean = false, scrollContainer?: HTMLElement) {
setImageAndSourcesToDefault(element, defaultImagePath, useSrcset);
if (hasCssClassName(element, cssClassNames.loaded)) {
removeCssClassName(element, cssClassNames.loaded)
}
return (scrollObservable: Observable<Event>) => {
return scrollObservable.pipe(
filter(() => isVisible(element, offset, window, scrollContainer)),
take(1),
mergeMap(() => loadImage(element, imagePath, useSrcset)),
tap(() => setImageAndSourcesToLazy(element, imagePath, useSrcset)),
map(() => true),
catchError(() => {
setImageAndSourcesToError(element, errorImgPath, useSrcset);
addCssClassName(element, cssClassNames.failed);
return Observable.of(false);
}),
tap(() => addCssClassName(element, cssClassNames.loaded))
);
};
}