浏览代码

Add x-intersect (#1761)

* Basic intersect code

* Basic docs

* wip
Caleb Porzio 4 年之前
父节点
当前提交
3eccac86e7

+ 6 - 0
packages/docs/src/en/plugins.md

@@ -0,0 +1,6 @@
+---
+order: 7
+title: Plugins
+font-type: mono
+type: sub-directory
+---

+ 87 - 0
packages/docs/src/en/plugins/intersect.md

@@ -0,0 +1,87 @@
+---
+order: 1
+title: Intersect
+description: Alpine's Intersect plugin is a convenience wrapper for Intersection Observer that allows you to easily react when an element enters or leaves the viewport.
+---
+
+# Intersect Plugin
+
+Alpine's Intersect plugin is a convenience wrapper for [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) that allows you to easily react when an element enters or leaves the viewport.
+
+This is useful for: lazy loading images and other content, triggering animations, infinite scrolling, logging "views" of content, etc.
+
+<a name="installation"></a>
+## Installation
+
+You can use this plugin by either including it from a `<script>` tag or installing it via NPM:
+
+### Via CDN
+
+You can include the CDN build of this plugin as a `<script>` tag, just make sure to include it BEFORE Alpine's core JS file.
+
+```html
+<!-- Alpine Plugins -->
+<script defer src="https://unpkg.com/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
+
+<!-- Alpine Core -->
+<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+```
+
+### Via NPM
+
+You can install Intersect from NPM for use inside your bundle like so:
+
+```bash
+npm install @alpinejs/intersect
+```
+
+Then initialize it from your bundle:
+
+```js
+import Alpine from 'alpinejs'
+import intersect from '@alpinejs/intersect'
+
+Alpine.plugin(intersect)
+
+...
+```
+
+<a name="x-intersect"></a>
+## x-intersect
+
+The primary API for using this plugin is `x-intersect`. You can add `x-intersect` to any element within an Alpine component, and when that component enters the viewport (is scrolled into view), the provided expression will execute.
+
+For example, in the following snippet, `shown` will remain `false` until the element is scrolled into view. At that point, the expression will execute and `shown` will become `true`:
+
+```html
+<div x-data="{ shown: false }" x-intersect="shown = true">
+    <div x-show="shown" x-transition>
+        I'm in the viewport!
+    </div>
+</div>
+```
+
+<!-- START_VERBATIM -->
+<div class="demo" style="height: 60px; overflow-y: scroll;" x-data x-ref="root">
+    <a href="#" @click.prevent="$refs.root.scrollTo({ top: $refs.root.scrollHeight, behavior: 'smooth' })">Scroll Down 👇</a>
+    <div style="height: 50vh"></div>
+    <div x-data="{ shown: false }" x-intersect="shown = true" id="yoyo">
+        <div x-show="shown" x-transition.duration.1000ms>
+            I'm in the viewport!
+        </div>
+        <div x-show="! shown">&nbsp;</div>
+    </div>
+</div>
+<!-- END_VERBATIM -->
+
+<a name="modifiers"></a>
+## Modifiers
+
+<a name="once"></a>
+### .once
+
+Sometimes it's useful to evaluate an expression only the first time an element enters the viewport and not subsequent times. For example when triggering "enter" animations. In these cases, you can add the `.once` modifier to `x-intersect` to achieve this.
+
+```html
+<div x-intersect.once="shown = true">...</div>
+```

+ 12 - 53
packages/intersect/src/index.js

@@ -1,63 +1,22 @@
-let pauseReactions = false
 
 export default function (Alpine) {
-    Alpine.directive('intersect', (el, { value, modifiers, expression }, { evaluateLater }) => {
+    Alpine.directive('intersect', (el, { expression, modifiers }, { evaluateLater, cleanup }) => {
         let evaluate = evaluateLater(expression)
 
-        if (['out', 'leave'].includes(value)) {
-            el._x_intersectLeave(evaluate, modifiers)
-        } else {
-            el._x_intersectEnter(evaluate, modifiers)
-        }
-    })
-}
-
-window.Element.prototype._x_intersectEnter = function (callback, modifiers) {
-    this._x_intersect((entry, observer) => {
-        if (pauseReactions) return
+        let observer = new IntersectionObserver(entries => {
+            entries.forEach(entry => {
+                if (entry.intersectionRatio === 0) return
 
-        pauseReactions = true
-        if (entry.intersectionRatio > 0) {
+                evaluate()
 
-            callback()
+                modifiers.includes('once') && observer.disconnect()
+            })
+        })
 
-            modifiers.includes('once') && observer.unobserve(this)
+        observer.observe(el)
 
-
-        }
-        setTimeout(() => {
-            pauseReactions = false
-        }, 100);
+        cleanup(() => {
+            observer.disconnect()
+        })
     })
 }
-
-window.Element.prototype._x_intersectLeave = function (callback, modifiers) {
-    this._x_intersect((entry, observer) => {
-        if (pauseReactions) return
-
-        pauseReactions = true
-        if (! entry.intersectionRatio > 0) {
-
-            callback()
-
-            modifiers.includes('once') && observer.unobserve(this)
-
-
-        }
-        setTimeout(() => {
-            pauseReactions = false
-        }, 100);
-    })
-}
-
-window.Element.prototype._x_intersect = function (callback) {
-    let observer = new IntersectionObserver(entries => {
-        entries.forEach(entry => callback(entry, observer))
-    }, {
-        // threshold: 1,
-    })
-
-    observer.observe(this);
-
-    return observer
-}

+ 20 - 0
tests/cypress/integration/plugins/intersect.spec.js

@@ -19,3 +19,23 @@ test('can intersect',
         get('span').should(haveText('2'))
     },
 )
+
+test('.once',
+    [html`
+    <div x-data="{ count: 0 }" x-init="setTimeout(() => count++, 300)">
+        <span x-text="count"></span>
+
+        <div x-intersect.once="count++" style="margin-top: 100vh;" id="1">hi</div>
+    </div>
+    `],
+    ({ get }, reload) => {
+        get('span').should(haveText('0'))
+        get('span').should(haveText('1'))
+        get('#1').scrollIntoView()
+        get('span').should(haveText('1'))
+        get('span').scrollIntoView()
+        get('span').should(haveText('1'))
+        get('#1').scrollIntoView()
+        get('span').should(haveText('1'))
+    },
+)