1
0
Эх сурвалжийг харах

Add x-collapse (#2141)

* Add collapse plugin

* Add tests
Caleb Porzio 3 жил өмнө
parent
commit
b22aaa4886

+ 5 - 2
packages/alpinejs/src/alpine.js

@@ -3,7 +3,9 @@ import { mutateDom, deferMutations, flushAndStopDeferringMutations } from './mut
 import { mapAttributes, directive, setPrefix as prefix } from './directives'
 import { start, addRootSelector, closestRoot, initTree } from './lifecycle'
 import { setEvaluator, evaluate, evaluateLater } from './evaluator'
+import { transition } from './directives/x-transition'
 import { interceptor } from './interceptor'
+import { setStyles } from './utils/styles'
 import { debounce } from './utils/debounce'
 import { throttle } from './utils/throttle'
 import { nextTick } from './nextTick'
@@ -28,8 +30,9 @@ let Alpine = {
     evaluateLater,
     setEvaluator,
     closestRoot,
-    // Warning: interceptor is not public API and is subject to change without major release.
-    interceptor,
+    interceptor, // INTERNAL: not public API and is subject to change without major release.
+    transition, // INTERNAL
+    setStyles, // INTERNAL
     mutateDom,
     directive,
     throttle,

+ 5 - 8
packages/alpinejs/src/directives/x-transition.js

@@ -109,7 +109,6 @@ function registerTransitionObject(el, setFunction, defaultValue = {}) {
                 during: this.enter.during,
                 start: this.enter.start,
                 end: this.enter.end,
-                entering: true,
             }, before, after)
         },
 
@@ -118,7 +117,6 @@ function registerTransitionObject(el, setFunction, defaultValue = {}) {
                 during: this.leave.during,
                 start: this.leave.start,
                 end: this.leave.end,
-                entering: false,
             }, before, after)
         },
     }
@@ -189,7 +187,7 @@ function closestHide(el) {
     return parent._x_hidePromise ? parent : closestHide(parent)
 }
 
-export function transition(el, setFunction, { during, start, end, entering } = {}, before = () => {}, after = () => {}) {
+export function transition(el, setFunction, { during, start, end } = {}, before = () => {}, after = () => {}) {
     if (el._x_transitioning) el._x_transitioning.cancel()
 
     if (Object.keys(during).length === 0 && Object.keys(start).length === 0 && Object.keys(end).length === 0) {
@@ -218,10 +216,10 @@ export function transition(el, setFunction, { during, start, end, entering } = {
             undoDuring()
             undoEnd()
         },
-    }, entering)
+    })
 }
 
-export function performTransition(el, stages, entering) {
+export function performTransition(el, stages) {
     // All transitions need to be truly "cancellable". Meaning we need to
     // account for interruptions at ALL stages of the transitions and
     // immediately run the rest of the transition.
@@ -253,7 +251,6 @@ export function performTransition(el, stages, entering) {
         beforeCancel(callback) { this.beforeCancels.push(callback) },
         cancel: once(function () { while (this.beforeCancels.length) { this.beforeCancels.shift()() }; finish(); }),
         finish,
-        entering,
     }
 
     mutateDom(() => {
@@ -295,7 +292,7 @@ export function performTransition(el, stages, entering) {
     })
 }
 
-function modifierValue(modifiers, key, fallback) {
+export function modifierValue(modifiers, key, fallback) {
     // If the modifier isn't present, use the default.
     if (modifiers.indexOf(key) === -1) return fallback
 
@@ -312,7 +309,7 @@ function modifierValue(modifiers, key, fallback) {
     }
 
     if (key === 'duration') {
-        // Support x-show.transition.duration.500ms && duration.500
+        // Support x-transition.duration.500ms && duration.500
         let match = rawValue.match(/([0-9]+)ms/)
         if (match) return match[1]
     }

+ 5 - 0
packages/collapse/builds/cdn.js

@@ -0,0 +1,5 @@
+import collapse from '../src/index.js'
+
+document.addEventListener('alpine:init', () => {
+    window.Alpine.plugin(collapse)
+})

+ 3 - 0
packages/collapse/builds/module.js

@@ -0,0 +1,3 @@
+import collapse from '../src/index.js'
+
+export default collapse

+ 11 - 0
packages/collapse/package.json

@@ -0,0 +1,11 @@
+{
+    "name": "@alpinejs/collapse",
+    "version": "3.4.1",
+    "description": "Collapse and expand elements with robust animations",
+    "author": "Caleb Porzio",
+    "license": "MIT",
+    "main": "dist/module.cjs.js",
+    "module": "dist/module.esm.js",
+    "unpkg": "dist/cdn.min.js",
+    "dependencies": {}
+}

+ 73 - 0
packages/collapse/src/index.js

@@ -0,0 +1,73 @@
+export default function (Alpine) {
+    Alpine.directive('collapse', (el, { expression, modifiers }, { effect, evaluateLater }) => {
+        let duration = modifierValue(modifiers, 'duration', 250) / 1000
+        let floor = 0
+
+        el.style.overflow = 'hidden'
+        if (! el._x_isShown) el.style.height = `${floor}px`
+        if (! el._x_isShown) el.style.removeProperty('display')
+
+        // Override the setStyles function with one that won't
+        // revert updates to the height style.
+        let setFunction = (el, styles) => {
+            let revertFunction = Alpine.setStyles(el, styles);
+
+           return styles.height ? () => {} : revertFunction
+        }
+
+        let transitionStyles = {
+            overflow: 'hidden',
+            transitionProperty: 'height',
+            transitionDuration: `${duration}s`,
+            transitionTimingFunction: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
+        }
+
+        el._x_transition = {
+            in(before = () => {}, after = () => {}) {
+                let current = el.getBoundingClientRect().height
+
+                Alpine.setStyles(el, {
+                    height: 'auto'
+                })
+
+                let full = el.getBoundingClientRect().height
+
+                if (current === full) { current = floor }
+                
+                Alpine.transition(el, Alpine.setStyles, {
+                    during: transitionStyles,
+                    start: { height: current+'px' },
+                    end: { height: full+'px' },
+                }, () => el._x_isShown = true, () => {})
+            },
+    
+            out(before = () => {}, after = () => {}) {
+                let full = el.getBoundingClientRect().height
+
+                Alpine.transition(el, setFunction, {
+                    during: transitionStyles,
+                    start: { height: full+'px' },
+                    end: { height: floor+'px' },
+                }, () => {}, () => el._x_isShown = false)
+            },
+        }
+    })
+}
+
+function modifierValue(modifiers, key, fallback) {
+    // If the modifier isn't present, use the default.
+    if (modifiers.indexOf(key) === -1) return fallback
+
+    // If it IS present, grab the value after it: x-show.transition.duration.500ms
+    const rawValue = modifiers[modifiers.indexOf(key) + 1]
+
+    if (! rawValue) return fallback
+
+    if (key === 'duration') {
+        // Support x-collapse.duration.500ms && duration.500
+        let match = rawValue.match(/([0-9]+)ms/)
+        if (match) return match[1]
+    }
+
+    return rawValue
+}

+ 109 - 0
packages/docs/src/en/plugins/collapse.md

@@ -0,0 +1,109 @@
+---
+order: 4
+title: Collapse
+description: Collapse and expand elements with robust animations
+graph_image: https://alpinejs.dev/social_collapse.jpg
+---
+
+# Collapse Plugin
+
+Alpine's Collapse plugin allows you to expand and collapse elements using smooth animations.
+
+Because this behavior and implementation differs from Alpine's standard transition system, this functionality was made into a dedicated plugin.
+
+<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.
+
+```alpine
+<!-- Alpine Plugins -->
+<script defer src="https://unpkg.com/@alpinejs/collapse@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 Trap from NPM for use inside your bundle like so:
+
+```shell
+npm install @alpinejs/collapse
+```
+
+Then initialize it from your bundle:
+
+```js
+import Alpine from 'alpinejs'
+import collapse from '@alpinejs/collapse'
+
+Alpine.plugin(collapse)
+
+...
+```
+
+<a name="x-collapse"></a>
+## x-collapse
+
+The primary API for using this plugin is the `x-collapse` directive.
+
+`x-collapse` can only exist on an element that already has an `x-show` directive. When added to an `x-show` element, `x-collapse` will smoothly "collapse" and "expand" the element when it's visibility is toggled by animating its height property.
+
+For example:
+
+```alpine
+<div x-data="{ expanded: false }">
+    <button @click="expanded = ! expanded">Toggle Content</button>
+
+    <p x-show="expanded" x-collapse>
+        ...
+    </p>
+</div>
+```
+
+<!-- START_VERBATIM -->
+<div x-data="{ expanded: false }" class="demo">
+    <button @click="expanded = ! expanded">Toggle Content</button>
+
+    <div x-show="expanded" x-collapse>
+        <div class="pt-4">
+            Reprehenderit eu excepteur ullamco esse cillum reprehenderit exercitation labore non. Dolore dolore ea dolore veniam sint in sint ex Lorem ipsum. Sint laborum deserunt deserunt amet voluptate cillum deserunt. Amet nisi pariatur sit ut id. Ipsum est minim est commodo id dolor sint id quis sint Lorem.
+        </div>
+    </div>
+</div>
+<!-- END_VERBATIM -->
+
+<a name="modifiers"></a>
+## Modifiers
+
+<a name="dot-duration"></a>
+### .duration
+
+You can customize the duration of the collapse/expand transition by appending the `.duration` modifier like so:
+
+```alpine
+<div x-data="{ expanded: false }">
+    <button @click="expanded = ! expanded">Toggle Content</button>
+
+    <p x-show="expanded" x-collapse.duration.1000ms>
+        ...
+    </p>
+</div>
+```
+
+<!-- START_VERBATIM -->
+<div x-data="{ expanded: false }" class="demo">
+    <button @click="expanded = ! expanded">Toggle Content</button>
+
+    <div x-show="expanded" x-collapse.duration.1000ms>
+        <div class="pt-4">
+            Reprehenderit eu excepteur ullamco esse cillum reprehenderit exercitation labore non. Dolore dolore ea dolore veniam sint in sint ex Lorem ipsum. Sint laborum deserunt deserunt amet voluptate cillum deserunt. Amet nisi pariatur sit ut id. Ipsum est minim est commodo id dolor sint id quis sint Lorem.
+        </div>
+    </div>
+</div>
+<!-- END_VERBATIM -->

+ 1 - 0
scripts/build.js

@@ -9,6 +9,7 @@ let brotliSize = require('brotli-size');
     'history',
     'intersect',
     'persist',
+    'collapse',
     'morph',
     'trap',
 ]).forEach(package => {

+ 6 - 0
scripts/release.js

@@ -47,6 +47,9 @@ function writeNewAlpineVersion() {
 
     writeToPackageDotJson('trap', 'version', version)
     console.log('Bumping @alpinejs/trap package.json: '+version)
+
+    writeToPackageDotJson('collapse', 'version', version)
+    console.log('Bumping @alpinejs/collapse package.json: '+version)
 }
 
 function writeNewDocsVersion() {
@@ -77,6 +80,9 @@ function publish() {
     console.log('Publishing @alpinejs/trap on NPM...');
     runFromPackage('trap', 'npm publish --access public')
 
+    console.log('Publishing @alpinejs/collapse on NPM...');
+    runFromPackage('collapse', 'npm publish --access public')
+
     log('\n\nFinished!')
 }
 

+ 17 - 0
tests/cypress/integration/plugins/collapse.spec.js

@@ -0,0 +1,17 @@
+import { haveAttribute, haveComputedStyle, html, test } from '../../utils'
+
+test('can collapse and expand element',
+    [html`
+        <div x-data="{ expanded: false }">
+            <button @click="expanded = ! expanded">toggle</button>
+            <h1 x-show="expanded" x-collapse>contents</h1>
+        </div>
+    `],
+    ({ get }, reload) => {
+        get('h1').should(haveComputedStyle('height', '0px'))
+        get('button').click()
+        get('h1').should(haveAttribute('style', 'overflow: hidden; height: auto;'))
+        get('button').click()
+        get('h1').should(haveComputedStyle('height', '0px'))
+    },
+)

+ 1 - 0
tests/cypress/spec.html

@@ -11,6 +11,7 @@
     <script src="/../../packages/persist/dist/cdn.js"></script>
     <script src="/../../packages/trap/dist/cdn.js"></script>
     <script src="/../../packages/intersect/dist/cdn.js"></script>
+    <script src="/../../packages/collapse/dist/cdn.js"></script>
     <script>
         let root = document.querySelector('#root')