Răsfoiți Sursa

Add rootMargin support to x-intersect with .margin (#2558)

* `x-intersect.margin.200px` — expands in all directions
* `x-intersect.margin.-200px` — contracts in all directions
* `x-intersect.margin.200px.100px` — expands 200px top/bottom, 200px left/right
* `x-intersect.margin.400px.300px.200px.100px` — expands 400px top, 300px right, 200px bottom, 100px left
* `x-intersect.margin.10%.20%` — expands 10% top/bottom, 20% left/right

fixes #2247
fixes #2394
fixes #2510

Co-authored-by: Caleb Porzio <calebporzio@gmail.com>
Mark Jaquith 3 ani în urmă
părinte
comite
d4de6386fe

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

@@ -128,3 +128,25 @@ Useful for elements where it's important to show the whole element.
 ```alpine
 <div x-intersect.full="shown = true">...</div> // when `0.99` of the element is in the viewport
 ```
+
+<a name="margin"></a>
+### .margin
+
+Allows you to control the `rootMargin` property of the underlying `IntersectionObserver`.
+This effectively tweaks the size of the viewport boundary. Positive values
+expand the boundary beyond the viewport, and negative values shrink it inward. The values
+work like CSS margin: one value for all sides, two values for top/bottom, left/right, or
+four values for top, right, bottom, left. You can use `px` and `%` values, or use a bare number to
+get a pixel value.
+
+```alpine
+<div x-intersect.margin.200px="loaded = true">...</div> // Load when the element is within 200px of the viewport
+```
+
+```alpine
+<div x-intersect:leave.margin.10%.25px.25.25px="loaded = false">...</div> // Unload when the element gets within 10% of the top of the viewport, or within 25px of the other three edges
+```
+
+```alpine
+<div x-intersect.margin.-100px="visible = true">...</div> // Mark as visible when element is more than 100 pixels into the viewport.
+```

+ 27 - 1
packages/intersect/src/index.js

@@ -1,9 +1,9 @@
-
 export default function (Alpine) {
     Alpine.directive('intersect', (el, { value, expression, modifiers }, { evaluateLater, cleanup }) => {
         let evaluate = evaluateLater(expression)
 
         let options = {
+            rootMargin: getRootMargin(modifiers),
             threshold: getThreshhold(modifiers),
         }
 
@@ -32,3 +32,29 @@ function getThreshhold(modifiers) {
 
     return 0
 }
+
+export function getLengthValue(rawValue) {
+    // Supported: -10px, -20 (implied px), 30 (implied px), 40px, 50%
+    let match = rawValue.match(/^(-?[0-9]+)(px|%)?$/)
+    return match ? match[1] + (match[2] || 'px') : undefined
+}
+
+export function getRootMargin(modifiers) {
+    const key = 'margin'
+    const fallback = '0px 0px 0px 0px'
+    const index = modifiers.indexOf(key)
+
+    // If the modifier isn't present, use the default.
+    if (index === -1) return fallback
+
+    // Grab the 4 subsequent length values after it: x-intersect.margin.300px.0.50%.0
+    let values = []
+        for (let i = 1; i < 5; i++) {
+            values.push(getLengthValue(modifiers[index + i] || ''))
+        }
+
+    // Filter out undefined values (not a valid length)
+    values = values.filter((v) => v !== undefined)
+
+    return values.length ? values.join(' ').trim() : fallback
+}

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

@@ -123,3 +123,27 @@ test('.once',
         get('span').should(haveText('2'))
     },
 )
+
+test('.margin.100px',
+    [html`
+    <div x-data="{ count: 0 }">
+        <span x-text="count"></span>
+        <div id="buffer-top" style="height: calc(100vh - 50px); margin-top: 100vh; background: pink"></div>
+        <div id="buffer-bottom" style="height: 50px; background: green"></div>
+        <div x-intersect.margin.100px="count++;$nextTick(() => console.log(count))" id="1">hi</div>
+    </div>
+    `],
+    ({ get }) => {
+        get('span').should(haveText('0'))
+        get('#buffer-top').scrollIntoView({duration: 100})
+        get('span').should(haveText('1'))
+        get('#1').scrollIntoView({duration: 100})
+        get('span').should(haveText('1'))
+        get('span').scrollIntoView({duration: 100})
+        get('span').should(haveText('1'))
+        get('#buffer-top').scrollIntoView({duration: 100})
+        get('span').should(haveText('2'))
+        get('#1').scrollIntoView({duration: 100})
+        get('span').should(haveText('2'))
+    },
+)