|
9 | 9 | template.innerHTML = `
|
10 | 10 |
|
11 | 11 | <style>
|
12 |
| -.lightning-image { |
13 |
| - display: inline-block; |
| 12 | +:host(lightning-image) { |
| 13 | + display: inline; /* if display: contents is not supported, then use the same default display style as an img tag */ |
| 14 | + display: contents; /* treat the lightning-image container as if it isn't there */ |
14 | 15 | }
|
15 | 16 | </style>
|
16 | 17 |
|
17 |
| -<div class="lightning-image"> |
18 |
| -</div> |
19 |
| -
|
20 | 18 | `;
|
21 | 19 |
|
22 | 20 |
|
|
29 | 27 | *
|
30 | 28 | */
|
31 | 29 | class LightningImage extends HTMLElement {
|
| 30 | + /** |
| 31 | + * This is a simple 1x1 white pixel encoded as base64. |
| 32 | + */ |
| 33 | + static PIXEL_BASE_64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQIHWP4DwABAQEANl9ngAAAAABJRU5ErkJggg=='; |
| 34 | + |
32 | 35 | constructor() {
|
33 | 36 | super();
|
34 | 37 |
|
35 |
| - // this is obviously not correct, I'm just not sure of the proper way to keep properties |
36 |
| - // and attributes in sync right now. |
37 |
| - if (!this.src && this.getAttribute('src')) { |
38 |
| - this.src = this.getAttribute('src'); |
39 |
| - } |
40 |
| - |
41 |
| - if (!this.getAttribute('src') && this.src) { |
42 |
| - this.setAttribute('src', this.src); |
43 |
| - } |
44 |
| - |
45 |
| - if (!this.width && this.getAttribute('width')) { |
46 |
| - this.width = this.getAttribute('width'); |
47 |
| - } |
48 |
| - |
49 |
| - if (!this.getAttribute('width') && this.width) { |
50 |
| - this.setAttribute('width', this.width); |
51 |
| - } |
| 38 | + // https://developers.google.com/web/fundamentals/web-components/best-practices#create-your-shadow-root-in-the-constructor |
| 39 | + this.attachShadow({mode: 'open'}); |
| 40 | + this.shadowRoot.appendChild(this.template()); |
| 41 | + } |
52 | 42 |
|
53 |
| - if (!this.height && this.getAttribute('height')) { |
54 |
| - this.height = this.getAttribute('height'); |
55 |
| - } |
| 43 | + get src() { |
| 44 | + return this.getAttribute('src'); |
| 45 | + } |
56 | 46 |
|
57 |
| - if (!this.getAttribute('height') && this.height) { |
58 |
| - this.setAttribute('height', this.height); |
| 47 | + /** |
| 48 | + * Ensure src property is reflected to an attribute. |
| 49 | + * https://developers.google.com/web/fundamentals/web-components/customelements#properties_and_attributes |
| 50 | + */ |
| 51 | + set src(value) { |
| 52 | + if (!value) { |
| 53 | + return this.removeAttribute('src'); |
59 | 54 | }
|
60 | 55 |
|
61 |
| - this.img = this.getImageToBeInserted(); |
62 |
| - |
63 |
| - this.attachShadow({mode: 'open'}); |
64 |
| - this.shadowRoot.appendChild(this.template()); |
| 56 | + this.setAttribute('src', value); |
65 | 57 | }
|
66 | 58 |
|
67 | 59 | connectedCallback() {
|
68 |
| - if (this.supportsNativeLazyLoading()) { |
69 |
| - this.img.loading = 'lazy'; |
70 |
| - this.insertImage(); |
71 |
| - |
72 |
| - return; |
| 60 | + // if the browser supports native lazy loading (only Chrome at time of writing), then |
| 61 | + // simply use that instead of using our own lazy loading implementation. |
| 62 | + if (!this.supportsNativeLazyLoading()) { |
| 63 | + this.setupObserver(); |
73 | 64 | }
|
74 |
| - |
75 |
| - // no native lazy loading for the browser |
76 |
| - this.setupObserver(); |
77 | 65 | }
|
78 | 66 |
|
79 | 67 | template() {
|
| 68 | + // See the note that indicates cloning is better than setting innerHTML |
| 69 | + // https://developers.google.com/web/fundamentals/web-components/customelements#shadowdom |
80 | 70 | const cloned = template.content.cloneNode(true);
|
81 | 71 |
|
82 |
| - const container = cloned.querySelector('.lightning-image') |
83 |
| - const width = this.getAttribute('width') || 0; |
84 |
| - const height = this.getAttribute('height') || 0; |
85 |
| - container.style.width = width + 'px'; |
86 |
| - container.style.height = height + 'px'; |
| 72 | + // we want to create an img tag element that is the same as the lightning-image |
| 73 | + // component, except it has a src that will not cause any additional network requests |
| 74 | + const tpl = document.createElement('template'); |
| 75 | + tpl.innerHTML = replaceTag(this); |
| 76 | + const imgTag = tpl.content.firstChild; |
87 | 77 |
|
88 |
| - return cloned; |
89 |
| - } |
| 78 | + if (this.supportsNativeLazyLoading()) { |
| 79 | + // for browsers that support native lazy loading, return the true img tag |
| 80 | + // with the real src but ensure it has the loading=lazy attribute |
| 81 | + imgTag.loading = 'lazy'; |
| 82 | + imgTag.src = this.src; |
| 83 | + } else { |
| 84 | + // for browsers without native lazy loading support, replace the src with a very simple pixel image. |
| 85 | + imgTag.src = LightningImage.PIXEL_BASE_64; |
| 86 | + } |
90 | 87 |
|
91 |
| - getImageToBeInserted() { |
92 |
| - const template = document.createElement('template'); |
93 |
| - template.innerHTML = replaceTag(this); |
94 |
| - const img = template.content.firstChild; |
| 88 | + // add the dynamic img into our static template |
| 89 | + cloned.appendChild(imgTag); |
95 | 90 |
|
96 |
| - return img; |
| 91 | + return cloned; |
97 | 92 | }
|
98 | 93 |
|
99 | 94 | supportsNativeLazyLoading() {
|
|
104 | 99 | const observer = new IntersectionObserver(entries => {
|
105 | 100 | entries.forEach(entry => {
|
106 | 101 | if (entry.isIntersecting) {
|
107 |
| - this.insertImage(); |
| 102 | + this.insertOriginalImage(); |
108 | 103 | observer.unobserve(this);
|
109 | 104 | }
|
110 | 105 | })
|
111 | 106 | }, { rootMargin: '0px' });
|
112 | 107 |
|
113 |
| - observer.observe(this); |
| 108 | + // observe our pixel image |
| 109 | + observer.observe(this.getImgElement()); |
114 | 110 | }
|
115 | 111 |
|
116 |
| - insertImage() { |
117 |
| - const container = this.shadowRoot.querySelector('.lightning-image'); |
118 |
| - container.appendChild(this.img); |
| 112 | + getImgElement() { |
| 113 | + return this.shadowRoot.querySelector('img'); |
| 114 | + } |
| 115 | + |
| 116 | + insertOriginalImage() { |
| 117 | + this.getImgElement().src = this.src; |
119 | 118 | }
|
120 | 119 | }
|
121 | 120 |
|
|
126 | 125 | * @returns {string}
|
127 | 126 | */
|
128 | 127 | var replaceTag = function (element) {
|
129 |
| - return element.outerHTML.replace(/lightning-image/g, 'img').trim(); |
| 128 | + // custom elements are required to have a closing tag, but regular img tags do not use a closing tag. |
| 129 | + // remove our closing tag here so that we can do a normal search and replace in the next step |
| 130 | + const closingTagRemoved = element.outerHTML.replace(/<\/lightning-image>/g, ''); |
| 131 | + |
| 132 | + return closingTagRemoved.replace(/lightning-image/g, 'img').trim(); |
130 | 133 | };
|
131 | 134 |
|
132 |
| - // check if custom elements are not supported, and fall to showing the original iframe if so |
| 135 | + // check if custom elements are not supported, and fall to showing the original img tag if so |
133 | 136 | if (!('customElements' in window)) {
|
134 | 137 | return [].forEach.call(document.querySelectorAll('lightning-image'), function (el) {
|
135 | 138 | el.outerHTML = replaceTag(el);
|
|
0 commit comments