├── public ├── global.css ├── favicon.png └── index.html ├── src ├── index.js ├── main.js ├── index.d.ts ├── App.svelte └── collapse.js ├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── rollup.config.js └── scripts └── setupTypeScript.js /public/global.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from './collapse.js' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsdavis/svelte-collapse/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | name: 'world' 7 | } 8 | }); 9 | 10 | export default app; -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "svelte-collapse" { 2 | import type { ActionReturn } from "svelte/types/runtime/action"; 3 | 4 | interface CollapseParams { 5 | open: boolean; 6 | duration: number; 7 | easing: string; 8 | } 9 | 10 | export default function collapse(node: HTMLElement, params: Partial): ActionReturn; 11 | } 12 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 |
13 | { #each blocks as _, i } 14 |

15 | {i} Lorem ipsum dolor sit amet consectetur adipisicing elit. Vero impedit id veniam error rem quisquam explicabo expedita fugit recusandae quas. Distinctio quia nemo deserunt, culpa perspiciatis nobis omnis ducimus impedit! 16 |

17 | { /each } 18 |
19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-collapse", 3 | "version": "0.1.4", 4 | "license": "MIT", 5 | "svelte": "src/index.js", 6 | "exports": { 7 | ".": { 8 | "svelte": "src/index.js" 9 | } 10 | }, 11 | "types": "src/index.d.ts", 12 | "scripts": { 13 | "build": "rollup -c", 14 | "dev": "rollup -c -w", 15 | "start": "sirv public" 16 | }, 17 | "devDependencies": { 18 | "@rollup/plugin-commonjs": "^14.0.0", 19 | "@rollup/plugin-node-resolve": "^8.0.0", 20 | "rollup": "^2.3.4", 21 | "rollup-plugin-livereload": "^2.0.0", 22 | "rollup-plugin-svelte": "^6.0.0", 23 | "rollup-plugin-terser": "^7.0.0", 24 | "svelte": "^3.0.0", 25 | "sirv-cli": "^1.0.0" 26 | }, 27 | "dependencies": { 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/rsdavis/svelte-collapse", 32 | "directory": "/" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryan Davis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Collapse 2 | 3 | A Svelte Action that is applied to an element to transition its height between open and closed states. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | npm install svelte-collapse 9 | ``` 10 | 11 | ```html 12 | 16 | 17 |
18 |

Lorem ipsum

19 |

Lorem ipsum

20 |
21 | 22 | 25 | ``` 26 | 27 | Additional parameters that can be used to modify the transition properties, the defaults are shown below. 28 | 29 | ```html 30 |
31 | ``` 32 | 33 | ## Motivation 34 | 35 | Transitioning the height of an element using CSS alone is generally not sufficient. 36 | We need to use some Javascript to keep the height variable, so that it responds naturally to added content or a screen resize. 37 | This action relies on CSS to handle the transitions smoothly, and Javascript to manage the styles. 38 | 39 | By wrapping the logic of this approach in an Action, the elements can be easily styled. 40 | Additionally, by maintaining the open state outside of the action, the user has complete control over the open/close mechanism. 41 | 42 | The transitions are reversible, so that an open or close transition can be safely interrupted. -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | const production = !process.env.ROLLUP_WATCH; 8 | 9 | function serve() { 10 | let server; 11 | 12 | function toExit() { 13 | if (server) server.kill(0); 14 | } 15 | 16 | return { 17 | writeBundle() { 18 | if (server) return; 19 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 20 | stdio: ['ignore', 'inherit', 'inherit'], 21 | shell: true 22 | }); 23 | 24 | process.on('SIGTERM', toExit); 25 | process.on('exit', toExit); 26 | } 27 | }; 28 | } 29 | 30 | export default { 31 | input: 'src/main.js', 32 | output: { 33 | sourcemap: true, 34 | format: 'iife', 35 | name: 'app', 36 | file: 'public/build/bundle.js' 37 | }, 38 | plugins: [ 39 | svelte({ 40 | // enable run-time checks when not in production 41 | dev: !production, 42 | // we'll extract any component CSS out into 43 | // a separate file - better for performance 44 | css: css => { 45 | css.write('bundle.css'); 46 | } 47 | }), 48 | 49 | // If you have external dependencies installed from 50 | // npm, you'll most likely need these plugins. In 51 | // some cases you'll need additional configuration - 52 | // consult the documentation for details: 53 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 54 | resolve({ 55 | browser: true, 56 | dedupe: ['svelte'] 57 | }), 58 | commonjs(), 59 | 60 | // In dev mode, call `npm run start` once 61 | // the bundle has been generated 62 | !production && serve(), 63 | 64 | // Watch the `public` directory and refresh the 65 | // browser on changes when not in production 66 | !production && livereload('public'), 67 | 68 | // If we're building for production (npm run build 69 | // instead of npm run dev), minify 70 | production && terser() 71 | ], 72 | watch: { 73 | clearScreen: false 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /src/collapse.js: -------------------------------------------------------------------------------- 1 | 2 | export default function collapse (node, params) { 3 | 4 | const defaultParams = { 5 | open: true, 6 | duration: 0.2, 7 | easing: 'ease' 8 | } 9 | 10 | params = Object.assign(defaultParams, params) 11 | 12 | const noop = () => {} 13 | let transitionEndResolve = noop 14 | let transitionEndReject = noop 15 | 16 | const listener = () => { 17 | transitionEndResolve(); 18 | transitionEndResolve = noop; 19 | transitionEndReject = noop; 20 | }; 21 | 22 | node.addEventListener('transitionend', listener); 23 | 24 | // convenience functions 25 | async function asyncTransitionEnd () { 26 | return new Promise((resolve, reject) => { 27 | transitionEndResolve = resolve 28 | transitionEndReject = reject 29 | }) 30 | } 31 | 32 | async function nextFrame () { 33 | return new Promise(requestAnimationFrame) 34 | } 35 | 36 | function transition () { 37 | return `height ${params.duration}s ${params.easing}` 38 | } 39 | 40 | // set initial styles 41 | node.style.transition = transition() 42 | node.style.height = params.open ? 'auto' : '0px' 43 | 44 | if (params.open) { 45 | node.style.overflow = 'visible' 46 | } 47 | else { 48 | node.style.overflow = 'hidden' 49 | } 50 | 51 | async function enter () { 52 | 53 | // height is already in pixels 54 | // start the transition 55 | node.style.height = node.scrollHeight + 'px' 56 | 57 | // wait for transition to end, 58 | // then switch back to height auto 59 | try { 60 | await asyncTransitionEnd() 61 | node.style.height = 'auto' 62 | node.style.overflow = 'visible' 63 | } catch(err) { 64 | // interrupted by a leave transition 65 | } 66 | 67 | } 68 | 69 | async function leave () { 70 | 71 | if (node.style.height === 'auto') { 72 | 73 | // temporarily turn transitions off 74 | node.style.transition = 'none' 75 | await nextFrame() 76 | 77 | // set height to pixels, and turn transition back on 78 | node.style.height = node.scrollHeight + 'px' 79 | node.style.transition = transition() 80 | await nextFrame() 81 | 82 | // start the transition 83 | node.style.overflow = 'hidden' 84 | node.style.height = '0px' 85 | 86 | } 87 | else { 88 | 89 | // we are interrupting an enter transition 90 | transitionEndReject() 91 | node.style.overflow = 'hidden' 92 | node.style.height = '0px' 93 | 94 | } 95 | 96 | } 97 | 98 | function update (newParams) { 99 | params = Object.assign(params, newParams) 100 | params.open ? enter() : leave() 101 | } 102 | 103 | function destroy () { 104 | node.removeEventListener('transitionend', listener) 105 | } 106 | 107 | return { update, destroy } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /scripts/setupTypeScript.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** This script modifies the project to support TS code in .svelte files like: 4 | 5 | 8 | 9 | As well as validating the code for CI. 10 | */ 11 | 12 | /** To work on this script: 13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template 14 | */ 15 | 16 | const fs = require("fs") 17 | const path = require("path") 18 | const { argv } = require("process") 19 | 20 | const projectRoot = argv[2] || path.join(__dirname, "..") 21 | 22 | // Add deps to pkg.json 23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) 24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { 25 | "svelte-check": "^1.0.0", 26 | "svelte-preprocess": "^4.0.0", 27 | "@rollup/plugin-typescript": "^6.0.0", 28 | "typescript": "^3.9.3", 29 | "tslib": "^2.0.0", 30 | "@tsconfig/svelte": "^1.0.0" 31 | }) 32 | 33 | // Add script for checking 34 | packageJSON.scripts = Object.assign(packageJSON.scripts, { 35 | "validate": "svelte-check" 36 | }) 37 | 38 | // Write the package JSON 39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) 40 | 41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too 42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js") 43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts") 44 | fs.renameSync(beforeMainJSPath, afterMainTSPath) 45 | 46 | // Switch the app.svelte file to use TS 47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte") 48 | let appFile = fs.readFileSync(appSveltePath, "utf8") 49 | appFile = appFile.replace("