Skip to content

Commit

Permalink
Merge pull request #12 from dysfunc/interface-updates
Browse files Browse the repository at this point in the history
✨ 🚀 performance changes and interface updates
  • Loading branch information
dysfunc authored Apr 2, 2021
2 parents 3c2a6e6 + ef3f09c commit 73b3dc5
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 49 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0](https://github.com/svelte-plugins/svelte-viewable/releases/tag/v1.0.0) - 2021-04-02

- Interface changes that include exposing observer props and events
- Includes new `on:complete` event
- Replaced `enableObstructionDetection` with `detectObstructions`

## [0.1.3](https://github.com/svelte-plugins/svelte-viewable/releases/tag/v0.1.3) - 2021-04-01

- Enable SSR support
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,62 @@ npm i -D @svelte-plugins/viewable

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

## API

### Props
| Prop name | Description | Value |
| :----------- | :---------------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |
| element | Element to observe | `HTMLElement` |
| rules | Viewability rules | `object` (default: `null`) |
| intervalRate | Rate to check measurement while intersecting (ms) | `number` (default: `200`) |
| gridSize | Size of the obstruction grid | `number` (default: `20`) |
| detectObstructions | If `true`, obstructions impacting the element will affect measurement | 'boolean' (default: `false`) |
| root | Containing element | `null` or `HTMLElement` (default: `null`) |
| rootMargin | Margin offset of the containing element | `string` (default: `"0px"`) |
| intersecting | `true` if the observed element is intersecting | `boolean` (default: `false`) |
| observer | IntersectionObserver instance | [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) |
| entry | Observed element metadata | [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) |
| debug | If `true`, debug ouput will be logged to console | `boolean` (default: `false`) |

#### rules
| Prop name | Description | Value |
| :----------- | :------------------------------------------------------------------ | :---------------------------------- |
| duration | Consecutive time (seconds) that the element must be in view | `number` (default: `0`) |
| percentage | Percentage of the element that must be viewable | `number` (default: `0`) |
| repeat | If `true`, the rule will be applied indefinitely v once | `function` (default: `null`) |
| fn | Callback function to execute when rule has been met | `function` (default: `null`) |


```js
const rules = {
dwell: {
duration: 1,
percentage: 50,
fn: () => {
console.log('50% of the element was visible for at least 1 consecutive second.');
}
}
}
```

### Debug props

The properties below can be used to assist with debugging any issues you might have (ex: `bind:duration`, `bind:percent`, etc.)

| Prop name | Description | Value |
| :----------- | :---------------------------------------------------------------- | :---------------------- |
| duration | Viewable duration of the tracked element | `number` (default: `0`) |
| percent | Percentage of total viewable area (X+Y) | `number` (default: `0`) |
| percentX | Percentage of horizontal viewable area | `number` (default: `0`) |
| percentY | Percentage of vertical viewable area | `number` (default: `0`) |


### Events

- **on:observe**: Fired when an intersection change occurs (type `IntersectionObserverEntry`)
- **on:intersect**: Fired when an intersection change occurs and the element is intersecting (type `IntersectionObserverEntry`)
- **on:complete**: Fired when all rules have been executed

## Changelog

[Changelog](CHANGELOG.md)
Expand Down
14 changes: 7 additions & 7 deletions docs/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<script>
import ContainerExample from './ContainerExample.svelte';
let enableObstructionDetection = false;
let detectObstructions = false;
let status = 'Hidden';
const handleClick = () => (enableObstructionDetection = !enableObstructionDetection);
const handleClick = () => (detectObstructions = !detectObstructions);
$: status = enableObstructionDetection ? 'Visible' : 'Hidden';
$: status = detectObstructions ? 'Visible' : 'Hidden';
</script>

{#if enableObstructionDetection}
{#if detectObstructions}
<div id="overlay"></div>
{/if}

<header>
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<button id="obstructions" class="badge" data-testid="toggle-obstructions" class:on={enableObstructionDetection} on:click={handleClick}>
<button id="obstructions" class="badge" data-testid="toggle-obstructions" class:on={detectObstructions} on:click={handleClick}>
Obstructions: {status}
</button>
<p class="badge"><b>Hint:</b> Open dev console to see debug output.</p>
Expand Down Expand Up @@ -48,7 +48,7 @@
</div>
<div class="flex">
<div class="flex-1 h-64 block" data-testid="top">
<ContainerExample enableObstructionDetection={enableObstructionDetection} />
<ContainerExample {detectObstructions} />
</div>
</div>
<div class="flex flex-col w-4/6 space-y-2">
Expand All @@ -70,7 +70,7 @@
</div>
<div class="flex">
<div class="flex-1 h-64 block" data-testid="middle">
<ContainerExample enableObstructionDetection={enableObstructionDetection}>
<ContainerExample {detectObstructions}>
Hello World!
</ContainerExample>
</div>
Expand Down
14 changes: 11 additions & 3 deletions docs/src/ContainerExample.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script>
import Viewable from "@svelte-plugins/viewable";
export let enableObstructionDetection = false;
export let detectObstructions = false;
let events = [];
let element;
Expand All @@ -24,12 +24,20 @@
whenFourtyForTwo: { duration: 2, percentage: 40, fn },
// do something when this is 50% in view for 4 seconds
whenFiftyForFour: { duration: 4, percentage: 50, fn },
// do something when this is 100% in view for 6 seconds
// do something when this is 100% in view for 6 seconds
whenHundredForSix: { duration: 6, percentage: 100, fn }
};
</script>

<Viewable bind:duration bind:percent bind:percentY bind:percentX {rules} {element} {enableObstructionDetection} {debug}>
<Viewable
bind:duration
bind:percent
bind:percentY
bind:percentX
{rules}
{element}
{detectObstructions}
{debug}>
<div bind:this={element} class={`${activeRule}`}>
<slot></slot>

Expand Down
8 changes: 4 additions & 4 deletions docs/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
picomatch "^2.2.2"

"@svelte-plugins/viewable@../":
version "0.1.2"
version "0.1.3"

"@sveltejs/vite-plugin-svelte@^1.0.0-next.5":
version "1.0.0-next.5"
Expand Down Expand Up @@ -95,9 +95,9 @@ debug@^4.3.2:
ms "2.1.2"

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

esbuild@^0.9.3:
version "0.9.7"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@svelte-plugins/viewable",
"version": "0.1.3",
"version": "1.0.0",
"license": "MIT",
"description": "A simple rule-based approach to tracking element viewability.",
"author": "Kieran Boyle (https://github.com/dysfunc)",
Expand Down
1 change: 0 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export default () => {
"babelHelpers": "bundled",
"exclude": ["/node_modules/**"]
}),

production && terser()
]
};
Expand Down
106 changes: 75 additions & 31 deletions src/Viewable.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script>
import { onDestroy, afterUpdate, tick } from 'svelte';
import { afterUpdate, createEventDispatcher, onDestroy, tick } from 'svelte';
/**
* HTML Element to track
* HTML Element to observe
* @type {null | HTMLElement}
*/
export let element;
/**
* Objecting containging viewability rulesets
* Viewability rules object for this element
* @type {null | Object}
*/
export let rules;
Expand Down Expand Up @@ -47,13 +47,46 @@
*/
export let gridSize = 20;
/**
* Enables checking for elements obstructing the tracked elements view (popups, modals, overlays, etc.)
* If true, enables checking for anything obstructing the observed elements view (popups, modals, overlays, etc.)
* @type {Boolean}
*/
export let enableObstructionDetection = false;
export let detectObstructions = false;
/**
* Containing element (Defaults to the browser viewport)
* @type {null | HTMLElement}
*/
export let root = null;
/**
* Margin offset of the containing element
* @type {String}
*/
export let rootMargin = '0px';
/**
* Array of visibility thresholds that will result in a callback when
* the observed element crosses that each threshold (.1 = 10% visible inside of its container)
*/
export let threshold = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
/**
* Observed element metadata
* @type {null | Entry}
*/
export let entry = null;
/**
* If true, the observed element is intersecting
* @type {Boolean}
*/
export let intersecting = false;
/**
* IntersectionObserver instance
* @type {null | IntersectionObserver}
*/
export let observer = null;
let ready = null;
let timer = null;
let prevRootMargin = null;
const dispatch = createEventDispatcher();
const definitions = [];
Expand Down Expand Up @@ -191,7 +224,7 @@
percentY = (visibleHeightRatio * 100).toFixed(0);
percent = (percentageViewable * 100).toFixed(0);
if (enableObstructionDetection && isObstructed(rect, threshold)) {
if (detectObstructions && isObstructed(rect, threshold)) {
return 0;
}
Expand Down Expand Up @@ -226,24 +259,30 @@
// check if threshold has been met or exceeded
if (duration >= definition.duration) {
// if definition timer, reset it
// if observer, unobserve
if (definition.observer) {
definition.observer.unobserve(element);
}
// issue callback to fire beacon
definition.callback(definition);
// update history timestamp
definition.history = Date.now();
// remove definition so we aren't duplicating events
definitions.splice(i, 1);
i = i - 1;
if (!definition.repeat) {
// remove definition so we aren't duplicating events
definitions.splice(i, 1);
// update our count
i = i - 1;
}
logger(definitions);
if (!definitions.length) {
logger(`[ Finished - ${definition.history} ]`);
dispatch('complete', rules);
if (observer) {
observer.unobserve(element);
observer.disconnect();
}
if (timer) {
clearInterval(timer);
timer = null;
Expand All @@ -259,47 +298,52 @@
const track = (definition) => {
const onIntersection = (entries) => {
const entry = entries[0];
entry = entries[0];
intersecting = entry.isIntersecting;
// element has left the viewport, clear definition timer/history/duration
if (!entry.isIntersecting) {
if (!intersecting) {
definition.history = null;
} else {
// check if view threshold has been met
if (entry.isIntersecting && !timer) {
if (intersecting && !timer) {
timer = setInterval(checkViewability, intervalRate);
checkViewability();
}
}
};
if (!definition.observer) {
const observer = new IntersectionObserver(onIntersection, {
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
});
if (!observer) {
observer = new IntersectionObserver(onIntersection, { root, rootMargin, threshold });
observer.observe(element);
definition.observer = observer;
}
};
afterUpdate(async () => {
if (entry !== null) {
dispatch('observe', entry);
if (entry.isIntersecting) {
dispatch('intersect', entry);
}
}
await tick();
if (element !== null && !ready) {
createRuleDefinitions();
ready = true;
}
});
onDestroy(() => {
definitions.forEach((definition) => {
if (definition.observer) {
definition.observer.disconnect();
if (prevRootMargin && rootMargin !== prevRootMargin) {
if (observer) {
observer.disconnect();
ready = false;
}
});
}
});
onDestroy(() => observer && observer.disconnect());
</script>

<slot />
<slot {duration} {entry} {intersecting} {observer} {percent} {percentX} {percentY} />
4 changes: 2 additions & 2 deletions src/Viewable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe(Viewable.name, () => {
it('should execute rules when duration and percentage have been meet (immediate)', async () => {
const mock = jest.fn();

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

onIntersectionMock([{ isIntersecting: true }]);

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

const { container } = await TestHarness({
enableObstructionDetection: true,
detectObstructions: true,
rules: {
fifty4six: {
percentage: 50,
Expand Down

0 comments on commit 73b3dc5

Please sign in to comment.