real-view 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/dist/angular.cjs +174 -0
- package/dist/angular.d.cts +14 -0
- package/dist/angular.d.ts +14 -0
- package/dist/angular.js +37 -0
- package/dist/chunk-UW5FKFTH.js +123 -0
- package/dist/index.cjs +136 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +6 -0
- package/dist/react.cjs +149 -0
- package/dist/react.d.cts +6 -0
- package/dist/react.d.ts +6 -0
- package/dist/react.js +19 -0
- package/dist/solid.cjs +150 -0
- package/dist/solid.d.cts +6 -0
- package/dist/solid.d.ts +6 -0
- package/dist/solid.js +20 -0
- package/dist/svelte.cjs +146 -0
- package/dist/svelte.d.cts +11 -0
- package/dist/svelte.d.ts +11 -0
- package/dist/svelte.js +16 -0
- package/dist/vue.cjs +150 -0
- package/dist/vue.d.cts +6 -0
- package/dist/vue.d.ts +6 -0
- package/dist/vue.js +20 -0
- package/license +9 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Real View 👁️
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/real-view)
|
|
4
|
+
[](https://bundlephobia.com/package/real-view)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
> **Stop guessing. Start knowing.**
|
|
8
|
+
> The only visibility tracker that knows if your user *actually* sees the element.
|
|
9
|
+
|
|
10
|
+
## Why? 🤔
|
|
11
|
+
|
|
12
|
+
You use `IntersectionObserver` to track impressions. **You are lying to your analytics.**
|
|
13
|
+
|
|
14
|
+
Native observers fail in these common scenarios:
|
|
15
|
+
- ❌ **Occlusion:** A sticky header, modal, or dropdown covers the element.
|
|
16
|
+
- ❌ **Opacity:** The element is transparent (`opacity: 0`) or `visibility: hidden`.
|
|
17
|
+
- ❌ **Background Tabs:** The user switched tabs or minimized the browser.
|
|
18
|
+
- ❌ **Zero Size:** The element collapsed to 0x0 pixels.
|
|
19
|
+
|
|
20
|
+
**Real View** solves this. It combines `IntersectionObserver` with **DOM Raycasting**, **Computed Styles**, and **Page Visibility API** to guarantee physical visibility.
|
|
21
|
+
|
|
22
|
+
- **Universal:** First-class support for React, Vue, Svelte, Solid, Angular, and Vanilla.
|
|
23
|
+
- **Tiny:** ~1KB gzipped.
|
|
24
|
+
- **Smart:** Uses `requestIdleCallback` to prevent main-thread blocking.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation 📦
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install real-view
|
|
32
|
+
# or
|
|
33
|
+
pnpm add real-view
|
|
34
|
+
# or
|
|
35
|
+
yarn add real-view
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Usage 🚀
|
|
41
|
+
|
|
42
|
+
### React
|
|
43
|
+
|
|
44
|
+
Use the `useRealView` hook.
|
|
45
|
+
|
|
46
|
+
```jsx
|
|
47
|
+
import { useEffect } from 'react'
|
|
48
|
+
import { useRealView } from 'real-view/react'
|
|
49
|
+
|
|
50
|
+
const AdBanner = () => {
|
|
51
|
+
const [ref, isVisible] = useRealView({ pollInterval: 1000 })
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (isVisible) console.log("User is ACTUALLY looking at this!")
|
|
55
|
+
}, [isVisible])
|
|
56
|
+
|
|
57
|
+
return <div ref={ref}>Buy Now</div>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Vue 3
|
|
63
|
+
|
|
64
|
+
Use the `useRealView` composable.
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<script setup>
|
|
68
|
+
import { ref } from 'vue'
|
|
69
|
+
import { useRealView } from 'real-view/vue'
|
|
70
|
+
|
|
71
|
+
const el = ref(null)
|
|
72
|
+
const isVisible = useRealView(el)
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div ref="el">
|
|
77
|
+
Status: {{ isVisible ? 'SEEN' : 'HIDDEN' }}
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Svelte
|
|
84
|
+
|
|
85
|
+
Use the `realView` action.
|
|
86
|
+
|
|
87
|
+
```svelte
|
|
88
|
+
<script>
|
|
89
|
+
import { realView } from 'real-view/svelte'
|
|
90
|
+
let visible = false;
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<div use:realView={{ onUpdate: (v) => visible = v }}>
|
|
94
|
+
I am {visible ? 'visible' : 'hidden'}
|
|
95
|
+
</div>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### SolidJS
|
|
99
|
+
|
|
100
|
+
Use the `realView` directive.
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
iimport { createSignal } from 'solid-js';
|
|
104
|
+
import { realView } from 'real-view/solid';
|
|
105
|
+
|
|
106
|
+
// Typescript: declare module 'solid-js' { namespace JSX { interface Directives { realView: any; } } }
|
|
107
|
+
|
|
108
|
+
function App() {
|
|
109
|
+
const [visible, setVisible] = createSignal(false);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div use:realView={{ onUpdate: setVisible }}>
|
|
113
|
+
{visible() ? "I see you!" : "Where are you?"}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Angular (14+)
|
|
121
|
+
|
|
122
|
+
Use the standalone `RealViewDirective`.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { Component } from '@angular/core';
|
|
126
|
+
import { RealViewDirective } from 'real-view/angular';
|
|
127
|
+
|
|
128
|
+
@Component({
|
|
129
|
+
selector: 'app-tracker',
|
|
130
|
+
standalone: true,
|
|
131
|
+
imports: [RealViewDirective],
|
|
132
|
+
template: `
|
|
133
|
+
<div (realView)="onVisibilityChange($event)">
|
|
134
|
+
Track Me
|
|
135
|
+
</div>
|
|
136
|
+
`
|
|
137
|
+
})
|
|
138
|
+
export class TrackerComponent {
|
|
139
|
+
onVisibilityChange(isVisible: boolean) {
|
|
140
|
+
console.log('Visibility:', isVisible);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Vanilla JS
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { RealView } from 'real-view'
|
|
149
|
+
|
|
150
|
+
const el = document.querySelector('#banner')
|
|
151
|
+
|
|
152
|
+
const cleanup = RealView.observe(el, (isVisible) => {
|
|
153
|
+
console.log(isVisible ? 'Visible' : 'Hidden')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Later
|
|
157
|
+
// cleanup()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Configuration ⚙️
|
|
163
|
+
|
|
164
|
+
You can customize the strictness of the detection.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// React example
|
|
168
|
+
useRealView({
|
|
169
|
+
threshold: 0.5,
|
|
170
|
+
pollInterval: 500,
|
|
171
|
+
trackTab: true
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
| Option | Type | Default | Description |
|
|
175
|
+
|---|---|---|---|
|
|
176
|
+
| `threshold` | `number` | `0` | How much of the element must be in viewport (0.0 - 1.0). |
|
|
177
|
+
| `pollInterval` | `number` | `1000` | How often (in ms) to check for occlusion (z-index). |
|
|
178
|
+
| `trackTab` | `boolean` | `true` | If `true`, reports `false` when user switches browser tabs. |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## How it works 🧊
|
|
183
|
+
|
|
184
|
+
Real View uses a **"Lazy Raycasting"** architecture to keep performance high:
|
|
185
|
+
|
|
186
|
+
1. **Gatekeeper:** It uses `IntersectionObserver` first. If the element is off-screen, the CPU usage is **0%**.
|
|
187
|
+
2. **Raycasting:** Once on-screen, it fires a ray (`document.elementFromPoint`) at the center of your element. If the ray hits a modal, a sticky header, or a dropdown menu instead of your element, visibility is `false`.
|
|
188
|
+
3. **Style Audit:** It recursively checks `opacity`, `visibility`, and `display` up the DOM tree.
|
|
189
|
+
4. **Tab Hygiene:** It listens to the Page Visibility API to pause tracking when the tab is backgrounded.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
|
194
|
+
|
|
195
|
+
## Keywords
|
|
196
|
+
`visibility` `viewport` `intersection` `occlusion` `tracking` `analytics` `impression` `react` `vue` `svelte` `angular` `solid` `dom` `monitor` `viewability`
|
|
197
|
+
|
package/dist/angular.cjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
19
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
20
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
21
|
+
if (decorator = decorators[i])
|
|
22
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
23
|
+
if (kind && result) __defProp(target, key, result);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/angular.ts
|
|
28
|
+
var angular_exports = {};
|
|
29
|
+
__export(angular_exports, {
|
|
30
|
+
realViewDirective: () => realViewDirective
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(angular_exports);
|
|
33
|
+
var import_core = require("@angular/core");
|
|
34
|
+
|
|
35
|
+
// src/core/utils.ts
|
|
36
|
+
var isBrowser = () => {
|
|
37
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/core/engine.ts
|
|
41
|
+
var realViewEngine = class _realViewEngine {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.observer = null;
|
|
44
|
+
this.watchers = /* @__PURE__ */ new Map();
|
|
45
|
+
if (!isBrowser()) return;
|
|
46
|
+
this.initObserver();
|
|
47
|
+
this.initTabListener();
|
|
48
|
+
}
|
|
49
|
+
static get() {
|
|
50
|
+
if (!this.instance) this.instance = new _realViewEngine();
|
|
51
|
+
return this.instance;
|
|
52
|
+
}
|
|
53
|
+
initObserver() {
|
|
54
|
+
this.observer = new IntersectionObserver(this.handleIntersect.bind(this), {
|
|
55
|
+
threshold: 0
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
initTabListener() {
|
|
59
|
+
document.addEventListener("visibilitychange", () => {
|
|
60
|
+
const isHidden = document.hidden;
|
|
61
|
+
this.watchers.forEach((w) => {
|
|
62
|
+
if (w.opts.trackTab && isHidden) {
|
|
63
|
+
this.notify(w, false);
|
|
64
|
+
} else if (w.state.inViewport && !isHidden) {
|
|
65
|
+
this.checkOcclusion(w);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
handleIntersect(entries) {
|
|
71
|
+
entries.forEach((entry) => {
|
|
72
|
+
const watcher = this.watchers.get(entry.target);
|
|
73
|
+
if (!watcher) return;
|
|
74
|
+
watcher.state.inViewport = entry.isIntersecting;
|
|
75
|
+
if (entry.isIntersecting) {
|
|
76
|
+
this.startPolling(watcher);
|
|
77
|
+
} else {
|
|
78
|
+
this.stopPolling(watcher);
|
|
79
|
+
this.notify(watcher, false);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
checkOcclusion(w) {
|
|
84
|
+
if (w.opts.trackTab && document.hidden) return this.notify(w, false);
|
|
85
|
+
const rect = w.el.getBoundingClientRect();
|
|
86
|
+
if (rect.width === 0 || rect.height === 0) return this.notify(w, false);
|
|
87
|
+
const style = window.getComputedStyle(w.el);
|
|
88
|
+
if (style.opacity === "0" || style.visibility === "hidden") return this.notify(w, false);
|
|
89
|
+
const x = rect.left + rect.width / 2;
|
|
90
|
+
const y = rect.top + rect.height / 2;
|
|
91
|
+
const topEl = document.elementFromPoint(x, y);
|
|
92
|
+
const isVisible = topEl ? w.el.contains(topEl) || w.el === topEl : false;
|
|
93
|
+
this.notify(w, isVisible);
|
|
94
|
+
}
|
|
95
|
+
startPolling(w) {
|
|
96
|
+
if (w.state.timer) return;
|
|
97
|
+
const tick = () => {
|
|
98
|
+
if ("requestIdleCallback" in window) {
|
|
99
|
+
window.requestIdleCallback(() => this.checkOcclusion(w));
|
|
100
|
+
} else {
|
|
101
|
+
this.checkOcclusion(w);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
tick();
|
|
105
|
+
w.state.timer = window.setInterval(tick, w.opts.pollInterval);
|
|
106
|
+
}
|
|
107
|
+
stopPolling(w) {
|
|
108
|
+
if (w.state.timer) {
|
|
109
|
+
clearInterval(w.state.timer);
|
|
110
|
+
w.state.timer = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
notify(w, isVisible) {
|
|
114
|
+
if (w.state.isOccluded !== !isVisible) {
|
|
115
|
+
w.state.isOccluded = !isVisible;
|
|
116
|
+
w.cb(isVisible);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
observe(el, cb, options = {}) {
|
|
120
|
+
if (!isBrowser()) return () => {
|
|
121
|
+
};
|
|
122
|
+
const opts = {
|
|
123
|
+
threshold: 0,
|
|
124
|
+
pollInterval: 1e3,
|
|
125
|
+
trackTab: true,
|
|
126
|
+
...options
|
|
127
|
+
};
|
|
128
|
+
this.watchers.set(el, {
|
|
129
|
+
el,
|
|
130
|
+
cb,
|
|
131
|
+
opts,
|
|
132
|
+
state: { inViewport: false, isOccluded: false, timer: null }
|
|
133
|
+
});
|
|
134
|
+
this.observer?.observe(el);
|
|
135
|
+
return () => {
|
|
136
|
+
this.stopPolling(this.watchers.get(el));
|
|
137
|
+
this.observer?.unobserve(el);
|
|
138
|
+
this.watchers.delete(el);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/angular.ts
|
|
144
|
+
var realViewDirective = class {
|
|
145
|
+
constructor(el) {
|
|
146
|
+
this.el = el;
|
|
147
|
+
this.visibleChange = new import_core.EventEmitter();
|
|
148
|
+
this.stop = null;
|
|
149
|
+
}
|
|
150
|
+
ngOnInit() {
|
|
151
|
+
this.stop = realViewEngine.get().observe(this.el.nativeElement, (v) => {
|
|
152
|
+
this.visibleChange.emit(v);
|
|
153
|
+
}, this.options);
|
|
154
|
+
}
|
|
155
|
+
ngOnDestroy() {
|
|
156
|
+
this.stop?.();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
__decorateClass([
|
|
160
|
+
(0, import_core.Input)()
|
|
161
|
+
], realViewDirective.prototype, "options", 2);
|
|
162
|
+
__decorateClass([
|
|
163
|
+
(0, import_core.Output)()
|
|
164
|
+
], realViewDirective.prototype, "visibleChange", 2);
|
|
165
|
+
realViewDirective = __decorateClass([
|
|
166
|
+
(0, import_core.Directive)({
|
|
167
|
+
selector: "[realView]",
|
|
168
|
+
standalone: true
|
|
169
|
+
})
|
|
170
|
+
], realViewDirective);
|
|
171
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
172
|
+
0 && (module.exports = {
|
|
173
|
+
realViewDirective
|
|
174
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OnInit, OnDestroy, EventEmitter, ElementRef } from '@angular/core';
|
|
2
|
+
import { realViewOptions } from './index.cjs';
|
|
3
|
+
|
|
4
|
+
declare class realViewDirective implements OnInit, OnDestroy {
|
|
5
|
+
private el;
|
|
6
|
+
options?: realViewOptions;
|
|
7
|
+
visibleChange: EventEmitter<boolean>;
|
|
8
|
+
private stop;
|
|
9
|
+
constructor(el: ElementRef);
|
|
10
|
+
ngOnInit(): void;
|
|
11
|
+
ngOnDestroy(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { realViewDirective };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OnInit, OnDestroy, EventEmitter, ElementRef } from '@angular/core';
|
|
2
|
+
import { realViewOptions } from './index.js';
|
|
3
|
+
|
|
4
|
+
declare class realViewDirective implements OnInit, OnDestroy {
|
|
5
|
+
private el;
|
|
6
|
+
options?: realViewOptions;
|
|
7
|
+
visibleChange: EventEmitter<boolean>;
|
|
8
|
+
private stop;
|
|
9
|
+
constructor(el: ElementRef);
|
|
10
|
+
ngOnInit(): void;
|
|
11
|
+
ngOnDestroy(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { realViewDirective };
|
package/dist/angular.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__decorateClass,
|
|
3
|
+
realViewEngine
|
|
4
|
+
} from "./chunk-UW5FKFTH.js";
|
|
5
|
+
|
|
6
|
+
// src/angular.ts
|
|
7
|
+
import { Directive, Output, EventEmitter, Input } from "@angular/core";
|
|
8
|
+
var realViewDirective = class {
|
|
9
|
+
constructor(el) {
|
|
10
|
+
this.el = el;
|
|
11
|
+
this.visibleChange = new EventEmitter();
|
|
12
|
+
this.stop = null;
|
|
13
|
+
}
|
|
14
|
+
ngOnInit() {
|
|
15
|
+
this.stop = realViewEngine.get().observe(this.el.nativeElement, (v) => {
|
|
16
|
+
this.visibleChange.emit(v);
|
|
17
|
+
}, this.options);
|
|
18
|
+
}
|
|
19
|
+
ngOnDestroy() {
|
|
20
|
+
this.stop?.();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
__decorateClass([
|
|
24
|
+
Input()
|
|
25
|
+
], realViewDirective.prototype, "options", 2);
|
|
26
|
+
__decorateClass([
|
|
27
|
+
Output()
|
|
28
|
+
], realViewDirective.prototype, "visibleChange", 2);
|
|
29
|
+
realViewDirective = __decorateClass([
|
|
30
|
+
Directive({
|
|
31
|
+
selector: "[realView]",
|
|
32
|
+
standalone: true
|
|
33
|
+
})
|
|
34
|
+
], realViewDirective);
|
|
35
|
+
export {
|
|
36
|
+
realViewDirective
|
|
37
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/core/utils.ts
|
|
13
|
+
var isBrowser = () => {
|
|
14
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/core/engine.ts
|
|
18
|
+
var realViewEngine = class _realViewEngine {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.observer = null;
|
|
21
|
+
this.watchers = /* @__PURE__ */ new Map();
|
|
22
|
+
if (!isBrowser()) return;
|
|
23
|
+
this.initObserver();
|
|
24
|
+
this.initTabListener();
|
|
25
|
+
}
|
|
26
|
+
static get() {
|
|
27
|
+
if (!this.instance) this.instance = new _realViewEngine();
|
|
28
|
+
return this.instance;
|
|
29
|
+
}
|
|
30
|
+
initObserver() {
|
|
31
|
+
this.observer = new IntersectionObserver(this.handleIntersect.bind(this), {
|
|
32
|
+
threshold: 0
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
initTabListener() {
|
|
36
|
+
document.addEventListener("visibilitychange", () => {
|
|
37
|
+
const isHidden = document.hidden;
|
|
38
|
+
this.watchers.forEach((w) => {
|
|
39
|
+
if (w.opts.trackTab && isHidden) {
|
|
40
|
+
this.notify(w, false);
|
|
41
|
+
} else if (w.state.inViewport && !isHidden) {
|
|
42
|
+
this.checkOcclusion(w);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
handleIntersect(entries) {
|
|
48
|
+
entries.forEach((entry) => {
|
|
49
|
+
const watcher = this.watchers.get(entry.target);
|
|
50
|
+
if (!watcher) return;
|
|
51
|
+
watcher.state.inViewport = entry.isIntersecting;
|
|
52
|
+
if (entry.isIntersecting) {
|
|
53
|
+
this.startPolling(watcher);
|
|
54
|
+
} else {
|
|
55
|
+
this.stopPolling(watcher);
|
|
56
|
+
this.notify(watcher, false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
checkOcclusion(w) {
|
|
61
|
+
if (w.opts.trackTab && document.hidden) return this.notify(w, false);
|
|
62
|
+
const rect = w.el.getBoundingClientRect();
|
|
63
|
+
if (rect.width === 0 || rect.height === 0) return this.notify(w, false);
|
|
64
|
+
const style = window.getComputedStyle(w.el);
|
|
65
|
+
if (style.opacity === "0" || style.visibility === "hidden") return this.notify(w, false);
|
|
66
|
+
const x = rect.left + rect.width / 2;
|
|
67
|
+
const y = rect.top + rect.height / 2;
|
|
68
|
+
const topEl = document.elementFromPoint(x, y);
|
|
69
|
+
const isVisible = topEl ? w.el.contains(topEl) || w.el === topEl : false;
|
|
70
|
+
this.notify(w, isVisible);
|
|
71
|
+
}
|
|
72
|
+
startPolling(w) {
|
|
73
|
+
if (w.state.timer) return;
|
|
74
|
+
const tick = () => {
|
|
75
|
+
if ("requestIdleCallback" in window) {
|
|
76
|
+
window.requestIdleCallback(() => this.checkOcclusion(w));
|
|
77
|
+
} else {
|
|
78
|
+
this.checkOcclusion(w);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
tick();
|
|
82
|
+
w.state.timer = window.setInterval(tick, w.opts.pollInterval);
|
|
83
|
+
}
|
|
84
|
+
stopPolling(w) {
|
|
85
|
+
if (w.state.timer) {
|
|
86
|
+
clearInterval(w.state.timer);
|
|
87
|
+
w.state.timer = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
notify(w, isVisible) {
|
|
91
|
+
if (w.state.isOccluded !== !isVisible) {
|
|
92
|
+
w.state.isOccluded = !isVisible;
|
|
93
|
+
w.cb(isVisible);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
observe(el, cb, options = {}) {
|
|
97
|
+
if (!isBrowser()) return () => {
|
|
98
|
+
};
|
|
99
|
+
const opts = {
|
|
100
|
+
threshold: 0,
|
|
101
|
+
pollInterval: 1e3,
|
|
102
|
+
trackTab: true,
|
|
103
|
+
...options
|
|
104
|
+
};
|
|
105
|
+
this.watchers.set(el, {
|
|
106
|
+
el,
|
|
107
|
+
cb,
|
|
108
|
+
opts,
|
|
109
|
+
state: { inViewport: false, isOccluded: false, timer: null }
|
|
110
|
+
});
|
|
111
|
+
this.observer?.observe(el);
|
|
112
|
+
return () => {
|
|
113
|
+
this.stopPolling(this.watchers.get(el));
|
|
114
|
+
this.observer?.unobserve(el);
|
|
115
|
+
this.watchers.delete(el);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
__decorateClass,
|
|
122
|
+
realViewEngine
|
|
123
|
+
};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/index.ts
|
|
20
|
+
var index_exports = {};
|
|
21
|
+
__export(index_exports, {
|
|
22
|
+
realViewEngine: () => realViewEngine
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(index_exports);
|
|
25
|
+
|
|
26
|
+
// src/core/utils.ts
|
|
27
|
+
var isBrowser = () => {
|
|
28
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/core/engine.ts
|
|
32
|
+
var realViewEngine = class _realViewEngine {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.observer = null;
|
|
35
|
+
this.watchers = /* @__PURE__ */ new Map();
|
|
36
|
+
if (!isBrowser()) return;
|
|
37
|
+
this.initObserver();
|
|
38
|
+
this.initTabListener();
|
|
39
|
+
}
|
|
40
|
+
static get() {
|
|
41
|
+
if (!this.instance) this.instance = new _realViewEngine();
|
|
42
|
+
return this.instance;
|
|
43
|
+
}
|
|
44
|
+
initObserver() {
|
|
45
|
+
this.observer = new IntersectionObserver(this.handleIntersect.bind(this), {
|
|
46
|
+
threshold: 0
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
initTabListener() {
|
|
50
|
+
document.addEventListener("visibilitychange", () => {
|
|
51
|
+
const isHidden = document.hidden;
|
|
52
|
+
this.watchers.forEach((w) => {
|
|
53
|
+
if (w.opts.trackTab && isHidden) {
|
|
54
|
+
this.notify(w, false);
|
|
55
|
+
} else if (w.state.inViewport && !isHidden) {
|
|
56
|
+
this.checkOcclusion(w);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
handleIntersect(entries) {
|
|
62
|
+
entries.forEach((entry) => {
|
|
63
|
+
const watcher = this.watchers.get(entry.target);
|
|
64
|
+
if (!watcher) return;
|
|
65
|
+
watcher.state.inViewport = entry.isIntersecting;
|
|
66
|
+
if (entry.isIntersecting) {
|
|
67
|
+
this.startPolling(watcher);
|
|
68
|
+
} else {
|
|
69
|
+
this.stopPolling(watcher);
|
|
70
|
+
this.notify(watcher, false);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
checkOcclusion(w) {
|
|
75
|
+
if (w.opts.trackTab && document.hidden) return this.notify(w, false);
|
|
76
|
+
const rect = w.el.getBoundingClientRect();
|
|
77
|
+
if (rect.width === 0 || rect.height === 0) return this.notify(w, false);
|
|
78
|
+
const style = window.getComputedStyle(w.el);
|
|
79
|
+
if (style.opacity === "0" || style.visibility === "hidden") return this.notify(w, false);
|
|
80
|
+
const x = rect.left + rect.width / 2;
|
|
81
|
+
const y = rect.top + rect.height / 2;
|
|
82
|
+
const topEl = document.elementFromPoint(x, y);
|
|
83
|
+
const isVisible = topEl ? w.el.contains(topEl) || w.el === topEl : false;
|
|
84
|
+
this.notify(w, isVisible);
|
|
85
|
+
}
|
|
86
|
+
startPolling(w) {
|
|
87
|
+
if (w.state.timer) return;
|
|
88
|
+
const tick = () => {
|
|
89
|
+
if ("requestIdleCallback" in window) {
|
|
90
|
+
window.requestIdleCallback(() => this.checkOcclusion(w));
|
|
91
|
+
} else {
|
|
92
|
+
this.checkOcclusion(w);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
tick();
|
|
96
|
+
w.state.timer = window.setInterval(tick, w.opts.pollInterval);
|
|
97
|
+
}
|
|
98
|
+
stopPolling(w) {
|
|
99
|
+
if (w.state.timer) {
|
|
100
|
+
clearInterval(w.state.timer);
|
|
101
|
+
w.state.timer = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
notify(w, isVisible) {
|
|
105
|
+
if (w.state.isOccluded !== !isVisible) {
|
|
106
|
+
w.state.isOccluded = !isVisible;
|
|
107
|
+
w.cb(isVisible);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
observe(el, cb, options = {}) {
|
|
111
|
+
if (!isBrowser()) return () => {
|
|
112
|
+
};
|
|
113
|
+
const opts = {
|
|
114
|
+
threshold: 0,
|
|
115
|
+
pollInterval: 1e3,
|
|
116
|
+
trackTab: true,
|
|
117
|
+
...options
|
|
118
|
+
};
|
|
119
|
+
this.watchers.set(el, {
|
|
120
|
+
el,
|
|
121
|
+
cb,
|
|
122
|
+
opts,
|
|
123
|
+
state: { inViewport: false, isOccluded: false, timer: null }
|
|
124
|
+
});
|
|
125
|
+
this.observer?.observe(el);
|
|
126
|
+
return () => {
|
|
127
|
+
this.stopPolling(this.watchers.get(el));
|
|
128
|
+
this.observer?.unobserve(el);
|
|
129
|
+
this.watchers.delete(el);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
134
|
+
0 && (module.exports = {
|
|
135
|
+
realViewEngine
|
|
136
|
+
});
|