|
1 | 1 | <script>
|
2 |
| - import { onDestroy, afterUpdate, tick } from 'svelte'; |
| 2 | + import { afterUpdate, createEventDispatcher, onDestroy, tick } from 'svelte'; |
3 | 3 |
|
4 | 4 | /**
|
5 |
| - * HTML Element to track |
| 5 | + * HTML Element to observe |
6 | 6 | * @type {null | HTMLElement}
|
7 | 7 | */
|
8 | 8 | export let element;
|
9 | 9 | /**
|
10 |
| - * Objecting containging viewability rulesets |
| 10 | + * Viewability rules object for this element |
11 | 11 | * @type {null | Object}
|
12 | 12 | */
|
13 | 13 | export let rules;
|
|
47 | 47 | */
|
48 | 48 | export let gridSize = 20;
|
49 | 49 | /**
|
50 |
| - * Enables checking for elements obstructing the tracked elements view (popups, modals, overlays, etc.) |
| 50 | + * If true, enables checking for anything obstructing the observed elements view (popups, modals, overlays, etc.) |
51 | 51 | * @type {Boolean}
|
52 | 52 | */
|
53 |
| - export let enableObstructionDetection = false; |
| 53 | + export let detectObstructions = false; |
| 54 | + /** |
| 55 | + * Containing element (Defaults to the browser viewport) |
| 56 | + * @type {null | HTMLElement} |
| 57 | + */ |
| 58 | + export let root = null; |
| 59 | + /** |
| 60 | + * Margin offset of the containing element |
| 61 | + * @type {String} |
| 62 | + */ |
| 63 | + export let rootMargin = '0px'; |
| 64 | + /** |
| 65 | + * Array of visibility thresholds that will result in a callback when |
| 66 | + * the observed element crosses that each threshold (.1 = 10% visible inside of its container) |
| 67 | + */ |
| 68 | + export let threshold = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]; |
| 69 | + /** |
| 70 | + * Observed element metadata |
| 71 | + * @type {null | Entry} |
| 72 | + */ |
| 73 | + export let entry = null; |
| 74 | + /** |
| 75 | + * If true, the observed element is intersecting |
| 76 | + * @type {Boolean} |
| 77 | + */ |
| 78 | + export let intersecting = false; |
| 79 | + /** |
| 80 | + * IntersectionObserver instance |
| 81 | + * @type {null | IntersectionObserver} |
| 82 | + */ |
| 83 | + export let observer = null; |
54 | 84 |
|
55 | 85 | let ready = null;
|
56 | 86 | let timer = null;
|
| 87 | + let prevRootMargin = null; |
| 88 | +
|
| 89 | + const dispatch = createEventDispatcher(); |
57 | 90 |
|
58 | 91 | const definitions = [];
|
59 | 92 |
|
|
191 | 224 | percentY = (visibleHeightRatio * 100).toFixed(0);
|
192 | 225 | percent = (percentageViewable * 100).toFixed(0);
|
193 | 226 |
|
194 |
| - if (enableObstructionDetection && isObstructed(rect, threshold)) { |
| 227 | + if (detectObstructions && isObstructed(rect, threshold)) { |
195 | 228 | return 0;
|
196 | 229 | }
|
197 | 230 |
|
|
226 | 259 |
|
227 | 260 | // check if threshold has been met or exceeded
|
228 | 261 | if (duration >= definition.duration) {
|
229 |
| - // if definition timer, reset it |
230 |
| - // if observer, unobserve |
231 |
| - if (definition.observer) { |
232 |
| - definition.observer.unobserve(element); |
233 |
| - } |
234 | 262 | // issue callback to fire beacon
|
235 | 263 | definition.callback(definition);
|
236 | 264 | // update history timestamp
|
237 | 265 | definition.history = Date.now();
|
238 |
| - // remove definition so we aren't duplicating events |
239 |
| - definitions.splice(i, 1); |
240 |
| - i = i - 1; |
| 266 | +
|
| 267 | + if (!definition.repeat) { |
| 268 | + // remove definition so we aren't duplicating events |
| 269 | + definitions.splice(i, 1); |
| 270 | + // update our count |
| 271 | + i = i - 1; |
| 272 | + } |
241 | 273 |
|
242 | 274 | logger(definitions);
|
243 | 275 |
|
244 | 276 | if (!definitions.length) {
|
245 | 277 | logger(`[ Finished - ${definition.history} ]`);
|
246 | 278 |
|
| 279 | + dispatch('complete', rules); |
| 280 | +
|
| 281 | + if (observer) { |
| 282 | + observer.unobserve(element); |
| 283 | + observer.disconnect(); |
| 284 | + } |
| 285 | +
|
247 | 286 | if (timer) {
|
248 | 287 | clearInterval(timer);
|
249 | 288 | timer = null;
|
|
259 | 298 |
|
260 | 299 | const track = (definition) => {
|
261 | 300 | const onIntersection = (entries) => {
|
262 |
| - const entry = entries[0]; |
263 |
| -
|
| 301 | + entry = entries[0]; |
| 302 | + intersecting = entry.isIntersecting; |
264 | 303 | // element has left the viewport, clear definition timer/history/duration
|
265 |
| - if (!entry.isIntersecting) { |
| 304 | + if (!intersecting) { |
266 | 305 | definition.history = null;
|
267 | 306 | } else {
|
268 | 307 | // check if view threshold has been met
|
269 |
| - if (entry.isIntersecting && !timer) { |
| 308 | + if (intersecting && !timer) { |
270 | 309 | timer = setInterval(checkViewability, intervalRate);
|
271 | 310 | checkViewability();
|
272 | 311 | }
|
273 | 312 | }
|
274 | 313 | };
|
275 | 314 |
|
276 |
| - if (!definition.observer) { |
277 |
| - const observer = new IntersectionObserver(onIntersection, { |
278 |
| - threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] |
279 |
| - }); |
| 315 | + if (!observer) { |
| 316 | + observer = new IntersectionObserver(onIntersection, { root, rootMargin, threshold }); |
280 | 317 |
|
281 | 318 | observer.observe(element);
|
282 |
| -
|
283 |
| - definition.observer = observer; |
284 | 319 | }
|
285 | 320 | };
|
286 | 321 |
|
287 | 322 | afterUpdate(async () => {
|
| 323 | + if (entry !== null) { |
| 324 | + dispatch('observe', entry); |
| 325 | +
|
| 326 | + if (entry.isIntersecting) { |
| 327 | + dispatch('intersect', entry); |
| 328 | + } |
| 329 | + } |
| 330 | +
|
288 | 331 | await tick();
|
289 | 332 |
|
290 | 333 | if (element !== null && !ready) {
|
291 | 334 | createRuleDefinitions();
|
292 | 335 | ready = true;
|
293 | 336 | }
|
294 |
| - }); |
295 | 337 |
|
296 |
| - onDestroy(() => { |
297 |
| - definitions.forEach((definition) => { |
298 |
| - if (definition.observer) { |
299 |
| - definition.observer.disconnect(); |
| 338 | + if (prevRootMargin && rootMargin !== prevRootMargin) { |
| 339 | + if (observer) { |
| 340 | + observer.disconnect(); |
| 341 | + ready = false; |
300 | 342 | }
|
301 |
| - }); |
| 343 | + } |
302 | 344 | });
|
| 345 | +
|
| 346 | + onDestroy(() => observer && observer.disconnect()); |
303 | 347 | </script>
|
304 | 348 |
|
305 |
| -<slot /> |
| 349 | +<slot {duration} {entry} {intersecting} {observer} {percent} {percentX} {percentY} /> |
0 commit comments