|
1 /*---------------------------------------- |
|
2 * objectFitPolyfill 2.3.5 |
|
3 * |
|
4 * Made by Constance Chen |
|
5 * Released under the ISC license |
|
6 * |
|
7 * https://github.com/constancecchen/object-fit-polyfill |
|
8 *--------------------------------------*/ |
|
9 |
|
10 (function() { |
|
11 'use strict'; |
|
12 |
|
13 // if the page is being rendered on the server, don't continue |
|
14 if (typeof window === 'undefined') return; |
|
15 |
|
16 // Workaround for Edge 16-18, which only implemented object-fit for <img> tags |
|
17 var edgeMatch = window.navigator.userAgent.match(/Edge\/(\d{2})\./); |
|
18 var edgeVersion = edgeMatch ? parseInt(edgeMatch[1], 10) : null; |
|
19 var edgePartialSupport = edgeVersion |
|
20 ? edgeVersion >= 16 && edgeVersion <= 18 |
|
21 : false; |
|
22 |
|
23 // If the browser does support object-fit, we don't need to continue |
|
24 var hasSupport = 'objectFit' in document.documentElement.style !== false; |
|
25 if (hasSupport && !edgePartialSupport) { |
|
26 window.objectFitPolyfill = function() { |
|
27 return false; |
|
28 }; |
|
29 return; |
|
30 } |
|
31 |
|
32 /** |
|
33 * Check the container's parent element to make sure it will |
|
34 * correctly handle and clip absolutely positioned children |
|
35 * |
|
36 * @param {node} $container - parent element |
|
37 */ |
|
38 var checkParentContainer = function($container) { |
|
39 var styles = window.getComputedStyle($container, null); |
|
40 var position = styles.getPropertyValue('position'); |
|
41 var overflow = styles.getPropertyValue('overflow'); |
|
42 var display = styles.getPropertyValue('display'); |
|
43 |
|
44 if (!position || position === 'static') { |
|
45 $container.style.position = 'relative'; |
|
46 } |
|
47 if (overflow !== 'hidden') { |
|
48 $container.style.overflow = 'hidden'; |
|
49 } |
|
50 // Guesstimating that people want the parent to act like full width/height wrapper here. |
|
51 // Mostly attempts to target <picture> elements, which default to inline. |
|
52 if (!display || display === 'inline') { |
|
53 $container.style.display = 'block'; |
|
54 } |
|
55 if ($container.clientHeight === 0) { |
|
56 $container.style.height = '100%'; |
|
57 } |
|
58 |
|
59 // Add a CSS class hook, in case people need to override styles for any reason. |
|
60 if ($container.className.indexOf('object-fit-polyfill') === -1) { |
|
61 $container.className = $container.className + ' object-fit-polyfill'; |
|
62 } |
|
63 }; |
|
64 |
|
65 /** |
|
66 * Check for pre-set max-width/height, min-width/height, |
|
67 * positioning, or margins, which can mess up image calculations |
|
68 * |
|
69 * @param {node} $media - img/video element |
|
70 */ |
|
71 var checkMediaProperties = function($media) { |
|
72 var styles = window.getComputedStyle($media, null); |
|
73 var constraints = { |
|
74 'max-width': 'none', |
|
75 'max-height': 'none', |
|
76 'min-width': '0px', |
|
77 'min-height': '0px', |
|
78 top: 'auto', |
|
79 right: 'auto', |
|
80 bottom: 'auto', |
|
81 left: 'auto', |
|
82 'margin-top': '0px', |
|
83 'margin-right': '0px', |
|
84 'margin-bottom': '0px', |
|
85 'margin-left': '0px', |
|
86 }; |
|
87 |
|
88 for (var property in constraints) { |
|
89 var constraint = styles.getPropertyValue(property); |
|
90 |
|
91 if (constraint !== constraints[property]) { |
|
92 $media.style[property] = constraints[property]; |
|
93 } |
|
94 } |
|
95 }; |
|
96 |
|
97 /** |
|
98 * Calculate & set object-position |
|
99 * |
|
100 * @param {string} axis - either "x" or "y" |
|
101 * @param {node} $media - img or video element |
|
102 * @param {string} objectPosition - e.g. "50% 50%", "top left" |
|
103 */ |
|
104 var setPosition = function(axis, $media, objectPosition) { |
|
105 var position, other, start, end, side; |
|
106 objectPosition = objectPosition.split(' '); |
|
107 |
|
108 if (objectPosition.length < 2) { |
|
109 objectPosition[1] = objectPosition[0]; |
|
110 } |
|
111 |
|
112 /* istanbul ignore else */ |
|
113 if (axis === 'x') { |
|
114 position = objectPosition[0]; |
|
115 other = objectPosition[1]; |
|
116 start = 'left'; |
|
117 end = 'right'; |
|
118 side = $media.clientWidth; |
|
119 } else if (axis === 'y') { |
|
120 position = objectPosition[1]; |
|
121 other = objectPosition[0]; |
|
122 start = 'top'; |
|
123 end = 'bottom'; |
|
124 side = $media.clientHeight; |
|
125 } else { |
|
126 return; // Neither x or y axis specified |
|
127 } |
|
128 |
|
129 if (position === start || other === start) { |
|
130 $media.style[start] = '0'; |
|
131 return; |
|
132 } |
|
133 |
|
134 if (position === end || other === end) { |
|
135 $media.style[end] = '0'; |
|
136 return; |
|
137 } |
|
138 |
|
139 if (position === 'center' || position === '50%') { |
|
140 $media.style[start] = '50%'; |
|
141 $media.style['margin-' + start] = side / -2 + 'px'; |
|
142 return; |
|
143 } |
|
144 |
|
145 // Percentage values (e.g., 30% 10%) |
|
146 if (position.indexOf('%') >= 0) { |
|
147 position = parseInt(position, 10); |
|
148 |
|
149 if (position < 50) { |
|
150 $media.style[start] = position + '%'; |
|
151 $media.style['margin-' + start] = side * (position / -100) + 'px'; |
|
152 } else { |
|
153 position = 100 - position; |
|
154 $media.style[end] = position + '%'; |
|
155 $media.style['margin-' + end] = side * (position / -100) + 'px'; |
|
156 } |
|
157 |
|
158 return; |
|
159 } |
|
160 // Length-based values (e.g. 10px / 10em) |
|
161 else { |
|
162 $media.style[start] = position; |
|
163 } |
|
164 }; |
|
165 |
|
166 /** |
|
167 * Calculate & set object-fit |
|
168 * |
|
169 * @param {node} $media - img/video/picture element |
|
170 */ |
|
171 var objectFit = function($media) { |
|
172 // IE 10- data polyfill |
|
173 var fit = $media.dataset |
|
174 ? $media.dataset.objectFit |
|
175 : $media.getAttribute('data-object-fit'); |
|
176 var position = $media.dataset |
|
177 ? $media.dataset.objectPosition |
|
178 : $media.getAttribute('data-object-position'); |
|
179 |
|
180 // Default fallbacks |
|
181 fit = fit || 'cover'; |
|
182 position = position || '50% 50%'; |
|
183 |
|
184 // If necessary, make the parent container work with absolutely positioned elements |
|
185 var $container = $media.parentNode; |
|
186 checkParentContainer($container); |
|
187 |
|
188 // Check for any pre-set CSS which could mess up image calculations |
|
189 checkMediaProperties($media); |
|
190 |
|
191 // Reset any pre-set width/height CSS and handle fit positioning |
|
192 $media.style.position = 'absolute'; |
|
193 $media.style.width = 'auto'; |
|
194 $media.style.height = 'auto'; |
|
195 |
|
196 // `scale-down` chooses either `none` or `contain`, whichever is smaller |
|
197 if (fit === 'scale-down') { |
|
198 if ( |
|
199 $media.clientWidth < $container.clientWidth && |
|
200 $media.clientHeight < $container.clientHeight |
|
201 ) { |
|
202 fit = 'none'; |
|
203 } else { |
|
204 fit = 'contain'; |
|
205 } |
|
206 } |
|
207 |
|
208 // `none` (width/height auto) and `fill` (100%) and are straightforward |
|
209 if (fit === 'none') { |
|
210 setPosition('x', $media, position); |
|
211 setPosition('y', $media, position); |
|
212 return; |
|
213 } |
|
214 |
|
215 if (fit === 'fill') { |
|
216 $media.style.width = '100%'; |
|
217 $media.style.height = '100%'; |
|
218 setPosition('x', $media, position); |
|
219 setPosition('y', $media, position); |
|
220 return; |
|
221 } |
|
222 |
|
223 // `cover` and `contain` must figure out which side needs covering, and add CSS positioning & centering |
|
224 $media.style.height = '100%'; |
|
225 |
|
226 if ( |
|
227 (fit === 'cover' && $media.clientWidth > $container.clientWidth) || |
|
228 (fit === 'contain' && $media.clientWidth < $container.clientWidth) |
|
229 ) { |
|
230 $media.style.top = '0'; |
|
231 $media.style.marginTop = '0'; |
|
232 setPosition('x', $media, position); |
|
233 } else { |
|
234 $media.style.width = '100%'; |
|
235 $media.style.height = 'auto'; |
|
236 $media.style.left = '0'; |
|
237 $media.style.marginLeft = '0'; |
|
238 setPosition('y', $media, position); |
|
239 } |
|
240 }; |
|
241 |
|
242 /** |
|
243 * Initialize plugin |
|
244 * |
|
245 * @param {node} media - Optional specific DOM node(s) to be polyfilled |
|
246 */ |
|
247 var objectFitPolyfill = function(media) { |
|
248 if (typeof media === 'undefined' || media instanceof Event) { |
|
249 // If left blank, or a default event, all media on the page will be polyfilled. |
|
250 media = document.querySelectorAll('[data-object-fit]'); |
|
251 } else if (media && media.nodeName) { |
|
252 // If it's a single node, wrap it in an array so it works. |
|
253 media = [media]; |
|
254 } else if (typeof media === 'object' && media.length && media[0].nodeName) { |
|
255 // If it's an array of DOM nodes (e.g. a jQuery selector), it's fine as-is. |
|
256 media = media; |
|
257 } else { |
|
258 // Otherwise, if it's invalid or an incorrect type, return false to let people know. |
|
259 return false; |
|
260 } |
|
261 |
|
262 for (var i = 0; i < media.length; i++) { |
|
263 if (!media[i].nodeName) continue; |
|
264 |
|
265 var mediaType = media[i].nodeName.toLowerCase(); |
|
266 |
|
267 if (mediaType === 'img') { |
|
268 if (edgePartialSupport) continue; // Edge supports object-fit for images (but nothing else), so no need to polyfill |
|
269 |
|
270 if (media[i].complete) { |
|
271 objectFit(media[i]); |
|
272 } else { |
|
273 media[i].addEventListener('load', function() { |
|
274 objectFit(this); |
|
275 }); |
|
276 } |
|
277 } else if (mediaType === 'video') { |
|
278 if (media[i].readyState > 0) { |
|
279 objectFit(media[i]); |
|
280 } else { |
|
281 media[i].addEventListener('loadedmetadata', function() { |
|
282 objectFit(this); |
|
283 }); |
|
284 } |
|
285 } else { |
|
286 objectFit(media[i]); |
|
287 } |
|
288 } |
|
289 |
|
290 return true; |
|
291 }; |
|
292 |
|
293 if (document.readyState === 'loading') { |
|
294 // Loading hasn't finished yet |
|
295 document.addEventListener('DOMContentLoaded', objectFitPolyfill); |
|
296 } else { |
|
297 // `DOMContentLoaded` has already fired |
|
298 objectFitPolyfill(); |
|
299 } |
|
300 |
|
301 window.addEventListener('resize', objectFitPolyfill); |
|
302 |
|
303 window.objectFitPolyfill = objectFitPolyfill; |
|
304 })(); |