Skip to content

Commit 73b3dc5

Browse files
authored
Merge pull request #12 from dysfunc/interface-updates
✨ 🚀 performance changes and interface updates
2 parents 3c2a6e6 + ef3f09c commit 73b3dc5

File tree

9 files changed

+162
-49
lines changed

9 files changed

+162
-49
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.0](https://github.com/svelte-plugins/svelte-viewable/releases/tag/v1.0.0) - 2021-04-02
9+
10+
- Interface changes that include exposing observer props and events
11+
- Includes new `on:complete` event
12+
- Replaced `enableObstructionDetection` with `detectObstructions`
13+
814
## [0.1.3](https://github.com/svelte-plugins/svelte-viewable/releases/tag/v0.1.3) - 2021-04-01
915

1016
- Enable SSR support

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,62 @@ npm i -D @svelte-plugins/viewable
4747

4848
Try the basic example in [Svelte REPL](https://svelte.dev/repl/c811481b8e1b48e9bed0f6ff7d1fa9c2).
4949

50+
## API
51+
52+
### Props
53+
| Prop name | Description | Value |
54+
| :----------- | :---------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |
55+
| element | Element to observe | `HTMLElement` |
56+
| rules | Viewability rules | `object` (default: `null`) |
57+
| intervalRate | Rate to check measurement while intersecting (ms) | `number` (default: `200`) |
58+
| gridSize | Size of the obstruction grid | `number` (default: `20`) |
59+
| detectObstructions | If `true`, obstructions impacting the element will affect measurement | 'boolean' (default: `false`) |
60+
| root | Containing element | `null` or `HTMLElement` (default: `null`) |
61+
| rootMargin | Margin offset of the containing element | `string` (default: `"0px"`) |
62+
| intersecting | `true` if the observed element is intersecting | `boolean` (default: `false`) |
63+
| observer | IntersectionObserver instance | [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) |
64+
| entry | Observed element metadata | [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) |
65+
| debug | If `true`, debug ouput will be logged to console | `boolean` (default: `false`) |
66+
67+
#### rules
68+
| Prop name | Description | Value |
69+
| :----------- | :------------------------------------------------------------------ | :---------------------------------- |
70+
| duration | Consecutive time (seconds) that the element must be in view | `number` (default: `0`) |
71+
| percentage | Percentage of the element that must be viewable | `number` (default: `0`) |
72+
| repeat | If `true`, the rule will be applied indefinitely v once | `function` (default: `null`) |
73+
| fn | Callback function to execute when rule has been met | `function` (default: `null`) |
74+
75+
76+
```js
77+
const rules = {
78+
dwell: {
79+
duration: 1,
80+
percentage: 50,
81+
fn: () => {
82+
console.log('50% of the element was visible for at least 1 consecutive second.');
83+
}
84+
}
85+
}
86+
```
87+
88+
### Debug props
89+
90+
The properties below can be used to assist with debugging any issues you might have (ex: `bind:duration`, `bind:percent`, etc.)
91+
92+
| Prop name | Description | Value |
93+
| :----------- | :---------------------------------------------------------------- | :---------------------- |
94+
| duration | Viewable duration of the tracked element | `number` (default: `0`) |
95+
| percent | Percentage of total viewable area (X+Y) | `number` (default: `0`) |
96+
| percentX | Percentage of horizontal viewable area | `number` (default: `0`) |
97+
| percentY | Percentage of vertical viewable area | `number` (default: `0`) |
98+
99+
100+
### Events
101+
102+
- **on:observe**: Fired when an intersection change occurs (type `IntersectionObserverEntry`)
103+
- **on:intersect**: Fired when an intersection change occurs and the element is intersecting (type `IntersectionObserverEntry`)
104+
- **on:complete**: Fired when all rules have been executed
105+
50106
## Changelog
51107

52108
[Changelog](CHANGELOG.md)

docs/src/App.svelte

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
<script>
22
import ContainerExample from './ContainerExample.svelte';
33
4-
let enableObstructionDetection = false;
4+
let detectObstructions = false;
55
let status = 'Hidden';
66
7-
const handleClick = () => (enableObstructionDetection = !enableObstructionDetection);
7+
const handleClick = () => (detectObstructions = !detectObstructions);
88
9-
$: status = enableObstructionDetection ? 'Visible' : 'Hidden';
9+
$: status = detectObstructions ? 'Visible' : 'Hidden';
1010
</script>
1111

12-
{#if enableObstructionDetection}
12+
{#if detectObstructions}
1313
<div id="overlay"></div>
1414
{/if}
1515

1616
<header>
1717
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
18-
<button id="obstructions" class="badge" data-testid="toggle-obstructions" class:on={enableObstructionDetection} on:click={handleClick}>
18+
<button id="obstructions" class="badge" data-testid="toggle-obstructions" class:on={detectObstructions} on:click={handleClick}>
1919
Obstructions: {status}
2020
</button>
2121
<p class="badge"><b>Hint:</b> Open dev console to see debug output.</p>
@@ -48,7 +48,7 @@
4848
</div>
4949
<div class="flex">
5050
<div class="flex-1 h-64 block" data-testid="top">
51-
<ContainerExample enableObstructionDetection={enableObstructionDetection} />
51+
<ContainerExample {detectObstructions} />
5252
</div>
5353
</div>
5454
<div class="flex flex-col w-4/6 space-y-2">
@@ -70,7 +70,7 @@
7070
</div>
7171
<div class="flex">
7272
<div class="flex-1 h-64 block" data-testid="middle">
73-
<ContainerExample enableObstructionDetection={enableObstructionDetection}>
73+
<ContainerExample {detectObstructions}>
7474
Hello World!
7575
</ContainerExample>
7676
</div>

docs/src/ContainerExample.svelte

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script>
22
import Viewable from "@svelte-plugins/viewable";
33
4-
export let enableObstructionDetection = false;
4+
export let detectObstructions = false;
55
66
let events = [];
77
let element;
@@ -24,12 +24,20 @@
2424
whenFourtyForTwo: { duration: 2, percentage: 40, fn },
2525
// do something when this is 50% in view for 4 seconds
2626
whenFiftyForFour: { duration: 4, percentage: 50, fn },
27-
// do something when this is 100% in view for 6 seconds
27+
// do something when this is 100% in view for 6 seconds
2828
whenHundredForSix: { duration: 6, percentage: 100, fn }
2929
};
3030
</script>
3131

32-
<Viewable bind:duration bind:percent bind:percentY bind:percentX {rules} {element} {enableObstructionDetection} {debug}>
32+
<Viewable
33+
bind:duration
34+
bind:percent
35+
bind:percentY
36+
bind:percentX
37+
{rules}
38+
{element}
39+
{detectObstructions}
40+
{debug}>
3341
<div bind:this={element} class={`${activeRule}`}>
3442
<slot></slot>
3543

docs/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
picomatch "^2.2.2"
1212

1313
"@svelte-plugins/viewable@../":
14-
version "0.1.2"
14+
version "0.1.3"
1515

1616
"@sveltejs/vite-plugin-svelte@^1.0.0-next.5":
1717
version "1.0.0-next.5"
@@ -95,9 +95,9 @@ debug@^4.3.2:
9595
ms "2.1.2"
9696

9797
electron-to-chromium@^1.3.649:
98-
version "1.3.703"
99-
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.703.tgz#6d9b9a75c42a40775f5930329e642b22b227317f"
100-
integrity sha512-SVBVhNB+4zPL+rvtWLw7PZQkw/Eqj1HQZs22xtcqW36+xoifzEOEEDEpkxSMfB6RFeSIOcG00w6z5mSqLr1Y6w==
98+
version "1.3.705"
99+
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.705.tgz#9729956782ce44cd93bdb4197818cff71f7d5e9d"
100+
integrity sha512-agtrL5vLSOIK89sE/YSzAgqCw76eZ60gf3J7Tid5RfLbSp5H4nWL28/dIV+H+ZhNNi1JNiaF62jffwYsAyXc0g==
101101

102102
esbuild@^0.9.3:
103103
version "0.9.7"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@svelte-plugins/viewable",
3-
"version": "0.1.3",
3+
"version": "1.0.0",
44
"license": "MIT",
55
"description": "A simple rule-based approach to tracking element viewability.",
66
"author": "Kieran Boyle (https://github.com/dysfunc)",

rollup.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export default () => {
3636
"babelHelpers": "bundled",
3737
"exclude": ["/node_modules/**"]
3838
}),
39-
4039
production && terser()
4140
]
4241
};

src/Viewable.svelte

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<script>
2-
import { onDestroy, afterUpdate, tick } from 'svelte';
2+
import { afterUpdate, createEventDispatcher, onDestroy, tick } from 'svelte';
33
44
/**
5-
* HTML Element to track
5+
* HTML Element to observe
66
* @type {null | HTMLElement}
77
*/
88
export let element;
99
/**
10-
* Objecting containging viewability rulesets
10+
* Viewability rules object for this element
1111
* @type {null | Object}
1212
*/
1313
export let rules;
@@ -47,13 +47,46 @@
4747
*/
4848
export let gridSize = 20;
4949
/**
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.)
5151
* @type {Boolean}
5252
*/
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;
5484
5585
let ready = null;
5686
let timer = null;
87+
let prevRootMargin = null;
88+
89+
const dispatch = createEventDispatcher();
5790
5891
const definitions = [];
5992
@@ -191,7 +224,7 @@
191224
percentY = (visibleHeightRatio * 100).toFixed(0);
192225
percent = (percentageViewable * 100).toFixed(0);
193226
194-
if (enableObstructionDetection && isObstructed(rect, threshold)) {
227+
if (detectObstructions && isObstructed(rect, threshold)) {
195228
return 0;
196229
}
197230
@@ -226,24 +259,30 @@
226259
227260
// check if threshold has been met or exceeded
228261
if (duration >= definition.duration) {
229-
// if definition timer, reset it
230-
// if observer, unobserve
231-
if (definition.observer) {
232-
definition.observer.unobserve(element);
233-
}
234262
// issue callback to fire beacon
235263
definition.callback(definition);
236264
// update history timestamp
237265
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+
}
241273
242274
logger(definitions);
243275
244276
if (!definitions.length) {
245277
logger(`[ Finished - ${definition.history} ]`);
246278
279+
dispatch('complete', rules);
280+
281+
if (observer) {
282+
observer.unobserve(element);
283+
observer.disconnect();
284+
}
285+
247286
if (timer) {
248287
clearInterval(timer);
249288
timer = null;
@@ -259,47 +298,52 @@
259298
260299
const track = (definition) => {
261300
const onIntersection = (entries) => {
262-
const entry = entries[0];
263-
301+
entry = entries[0];
302+
intersecting = entry.isIntersecting;
264303
// element has left the viewport, clear definition timer/history/duration
265-
if (!entry.isIntersecting) {
304+
if (!intersecting) {
266305
definition.history = null;
267306
} else {
268307
// check if view threshold has been met
269-
if (entry.isIntersecting && !timer) {
308+
if (intersecting && !timer) {
270309
timer = setInterval(checkViewability, intervalRate);
271310
checkViewability();
272311
}
273312
}
274313
};
275314
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 });
280317
281318
observer.observe(element);
282-
283-
definition.observer = observer;
284319
}
285320
};
286321
287322
afterUpdate(async () => {
323+
if (entry !== null) {
324+
dispatch('observe', entry);
325+
326+
if (entry.isIntersecting) {
327+
dispatch('intersect', entry);
328+
}
329+
}
330+
288331
await tick();
289332
290333
if (element !== null && !ready) {
291334
createRuleDefinitions();
292335
ready = true;
293336
}
294-
});
295337
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;
300342
}
301-
});
343+
}
302344
});
345+
346+
onDestroy(() => observer && observer.disconnect());
303347
</script>
304348

305-
<slot />
349+
<slot {duration} {entry} {intersecting} {observer} {percent} {percentX} {percentY} />

src/Viewable.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe(Viewable.name, () => {
5454
it('should execute rules when duration and percentage have been meet (immediate)', async () => {
5555
const mock = jest.fn();
5656

57-
await TestHarness({ debug: true, enableObstructionDetection: true, rules: { immediate: { fn: mock } } });
57+
await TestHarness({ debug: true, detectObstructions: true, rules: { immediate: { fn: mock } } });
5858

5959
onIntersectionMock([{ isIntersecting: true }]);
6060

@@ -71,7 +71,7 @@ describe(Viewable.name, () => {
7171
const fn = jest.fn();
7272

7373
const { container } = await TestHarness({
74-
enableObstructionDetection: true,
74+
detectObstructions: true,
7575
rules: {
7676
fifty4six: {
7777
percentage: 50,

0 commit comments

Comments
 (0)