spark-html-motion 0.1.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 +82 -0
- package/package.json +37 -0
- package/src/index.d.ts +26 -0
- package/src/index.js +135 -0
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# spark-html-motion
|
|
2
|
+
|
|
3
|
+
Declarative **enter / leave transitions** for
|
|
4
|
+
[spark-html](https://github.com/wilkinnovo/spark) — the Spark way: no compiler,
|
|
5
|
+
no virtual DOM, 0 dependencies (~0.5 KB). When an `<template if>` / `<template
|
|
6
|
+
each>` block adds or removes an element, it animates in/out. A leaving element
|
|
7
|
+
is held in the DOM until its exit animation finishes, then removed.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install spark-html-motion
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
Register once, **before `mount()`**, then opt elements in with a `transition`
|
|
18
|
+
attribute:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { mount } from 'spark-html';
|
|
22
|
+
import { motion } from 'spark-html-motion';
|
|
23
|
+
|
|
24
|
+
motion();
|
|
25
|
+
mount(document.body);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<template each="t in todos">
|
|
30
|
+
<li transition="slide">{t.text}</li>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<template if="open">
|
|
34
|
+
<div class="panel" transition="fade">…</div>
|
|
35
|
+
</template>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `transition="fade | slide | scale"` — or the directive form `transition:fade`.
|
|
39
|
+
- `transition-duration="300"` — milliseconds (per element).
|
|
40
|
+
- `transition-easing="ease-out"` — any CSS easing (per element).
|
|
41
|
+
|
|
42
|
+
The **initial render is not animated** by default (only later enters/leaves) —
|
|
43
|
+
pass `motion({ appear: true })` if you want the first paint to animate too.
|
|
44
|
+
`prefers-reduced-motion: reduce` is honored automatically (no animation).
|
|
45
|
+
|
|
46
|
+
## Options & defaults
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
motion({
|
|
50
|
+
preset: 'fade', // default preset for a bare `transition` attribute
|
|
51
|
+
duration: 200, // ms
|
|
52
|
+
easing: 'ease',
|
|
53
|
+
appear: false, // animate the initial mount?
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Custom presets
|
|
58
|
+
|
|
59
|
+
`presets` is a plain object of `{ in: Keyframe[], out: Keyframe[] }` (standard
|
|
60
|
+
[Web Animations](https://developer.mozilla.org/docs/Web/API/Element/animate)
|
|
61
|
+
keyframes) — add your own:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { presets, motion } from 'spark-html-motion';
|
|
65
|
+
presets.zoom = {
|
|
66
|
+
in: [{ transform: 'scale(0)' }, { transform: 'scale(1)' }],
|
|
67
|
+
out: [{ transform: 'scale(1)' }, { transform: 'scale(0)' }],
|
|
68
|
+
};
|
|
69
|
+
motion();
|
|
70
|
+
// <li transition="zoom">…</li>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
Spark core exposes a tiny `lifecycle({ enter, leave })` seam; this package
|
|
76
|
+
registers into it and drives the Web Animations API. Nothing animates unless you
|
|
77
|
+
call `motion()`, and elements without a `transition` attribute are added/removed
|
|
78
|
+
instantly — so the cost is strictly opt-in.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT © Wilkin Novo
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spark-html-motion",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Declarative enter/leave transitions for spark-html — transition=\"fade|slide|scale\" on if/each blocks, Web Animations API, 0 deps, no compiler.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"homepage": "https://wilkinnovo.github.io/spark",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node test/motion.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/wilkinnovo/spark.git",
|
|
24
|
+
"directory": "packages/spark-html-motion"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"spark-html": "^0.21.6"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"spark-html",
|
|
31
|
+
"motion",
|
|
32
|
+
"transition",
|
|
33
|
+
"animation",
|
|
34
|
+
"enter-leave"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface MotionOptions {
|
|
2
|
+
/** Default preset when an element uses a bare `transition` attribute. Default: "fade". */
|
|
3
|
+
preset?: "fade" | "slide" | "scale" | (string & {});
|
|
4
|
+
/** Default duration in milliseconds. Default: 200. */
|
|
5
|
+
duration?: number;
|
|
6
|
+
/** Default easing. Default: "ease". */
|
|
7
|
+
easing?: string;
|
|
8
|
+
/** Animate the initial mount too (off by default — only later enters animate). */
|
|
9
|
+
appear?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Preset {
|
|
13
|
+
in: Keyframe[];
|
|
14
|
+
out: Keyframe[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Built-in keyframe presets, keyed by name. Mutate to add your own. */
|
|
18
|
+
export const presets: Record<string, Preset>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register enter/leave transitions for Spark if/each blocks. Call once before
|
|
22
|
+
* `mount()`. Opt elements in with `transition="fade|slide|scale"` (or the
|
|
23
|
+
* directive form `transition:fade`).
|
|
24
|
+
*/
|
|
25
|
+
export function motion(options?: MotionOptions): void;
|
|
26
|
+
export default motion;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// spark-html-motion — declarative enter/leave transitions for Spark, the Spark
|
|
2
|
+
// way: no compiler, no virtual DOM. It plugs into the core's lifecycle seam
|
|
3
|
+
// (`lifecycle()` from spark-html) and animates nodes that if/each blocks add or
|
|
4
|
+
// remove. Animation is the Web Animations API (0 deps); a leaving node is held
|
|
5
|
+
// in the DOM until its exit animation finishes, then removed.
|
|
6
|
+
//
|
|
7
|
+
// import { mount } from 'spark-html';
|
|
8
|
+
// import { motion } from 'spark-html-motion';
|
|
9
|
+
// motion(); // register once, before mount()
|
|
10
|
+
// mount(document.body);
|
|
11
|
+
//
|
|
12
|
+
// <template each="t in todos">
|
|
13
|
+
// <li transition="slide">{t.text}</li> <!-- or transition:fade -->
|
|
14
|
+
// </template>
|
|
15
|
+
//
|
|
16
|
+
// Opt in per element with `transition="fade|slide|scale"` (or the directive
|
|
17
|
+
// form `transition:fade`). Tune with `transition-duration="300"` (ms) and
|
|
18
|
+
// `transition-easing="ease-out"`. Honors prefers-reduced-motion.
|
|
19
|
+
|
|
20
|
+
import { lifecycle } from 'spark-html';
|
|
21
|
+
|
|
22
|
+
export const presets = {
|
|
23
|
+
fade: {
|
|
24
|
+
in: [{ opacity: 0 }, { opacity: 1 }],
|
|
25
|
+
out: [{ opacity: 1 }, { opacity: 0 }],
|
|
26
|
+
},
|
|
27
|
+
slide: {
|
|
28
|
+
in: [
|
|
29
|
+
{ opacity: 0, transform: 'translateY(8px)' },
|
|
30
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
31
|
+
],
|
|
32
|
+
out: [
|
|
33
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
34
|
+
{ opacity: 0, transform: 'translateY(8px)' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
scale: {
|
|
38
|
+
in: [
|
|
39
|
+
{ opacity: 0, transform: 'scale(.96)' },
|
|
40
|
+
{ opacity: 1, transform: 'scale(1)' },
|
|
41
|
+
],
|
|
42
|
+
out: [
|
|
43
|
+
{ opacity: 1, transform: 'scale(1)' },
|
|
44
|
+
{ opacity: 0, transform: 'scale(.96)' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const prefersReduced = () =>
|
|
50
|
+
typeof matchMedia === 'function' &&
|
|
51
|
+
matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
52
|
+
|
|
53
|
+
// Resolve an element's transition config from its attributes, or null to skip.
|
|
54
|
+
function configFor(node, defaults) {
|
|
55
|
+
if (!node || node.nodeType !== 1 || !node.getAttribute) return null;
|
|
56
|
+
let name = node.getAttribute('transition');
|
|
57
|
+
if (name == null && node.attributes) {
|
|
58
|
+
for (const a of node.attributes) {
|
|
59
|
+
if (a.name && a.name.indexOf('transition:') === 0) {
|
|
60
|
+
name = a.name.slice('transition:'.length);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (name == null) return null; // not opted in
|
|
66
|
+
name = (name && name.trim()) || defaults.preset;
|
|
67
|
+
const keyframes = presets[name] || presets[defaults.preset];
|
|
68
|
+
const dAttr = node.getAttribute('transition-duration');
|
|
69
|
+
const d = dAttr != null && dAttr !== '' ? Number(dAttr) : NaN;
|
|
70
|
+
const duration = Number.isFinite(d) && d >= 0 ? d : defaults.duration;
|
|
71
|
+
const easing = node.getAttribute('transition-easing') || defaults.easing;
|
|
72
|
+
return { keyframes, duration, easing };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function motion(options = {}) {
|
|
76
|
+
const defaults = {
|
|
77
|
+
preset: options.preset || 'fade',
|
|
78
|
+
duration: options.duration != null ? options.duration : 200,
|
|
79
|
+
easing: options.easing || 'ease',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Don't animate the initial mount unless asked — only later enters. The first
|
|
83
|
+
// render runs synchronously inside mount(); flip `ready` on a later frame.
|
|
84
|
+
let ready = !!options.appear;
|
|
85
|
+
if (!ready && typeof requestAnimationFrame === 'function') {
|
|
86
|
+
requestAnimationFrame(() =>
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
ready = true;
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const canAnimate = (node) =>
|
|
94
|
+
typeof node.animate === 'function' && !prefersReduced();
|
|
95
|
+
|
|
96
|
+
lifecycle({
|
|
97
|
+
enter(node) {
|
|
98
|
+
if (!ready) return;
|
|
99
|
+
const cfg = configFor(node, defaults);
|
|
100
|
+
if (!cfg || !canAnimate(node)) return;
|
|
101
|
+
node.animate(cfg.keyframes.in, {
|
|
102
|
+
duration: cfg.duration,
|
|
103
|
+
easing: cfg.easing,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
leave(node, remove) {
|
|
107
|
+
const cfg = configFor(node, defaults);
|
|
108
|
+
if (!cfg || !canAnimate(node)) {
|
|
109
|
+
remove();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const anim = node.animate(cfg.keyframes.out, {
|
|
113
|
+
duration: cfg.duration,
|
|
114
|
+
easing: cfg.easing,
|
|
115
|
+
fill: 'forwards', // hold the faded-out frame until we detach
|
|
116
|
+
});
|
|
117
|
+
let done = false;
|
|
118
|
+
const finish = () => {
|
|
119
|
+
if (done) return;
|
|
120
|
+
done = true;
|
|
121
|
+
remove();
|
|
122
|
+
};
|
|
123
|
+
if (anim && anim.finished && typeof anim.finished.then === 'function') {
|
|
124
|
+
anim.finished.then(finish, finish);
|
|
125
|
+
} else if (anim && typeof anim.addEventListener === 'function') {
|
|
126
|
+
anim.addEventListener('finish', finish);
|
|
127
|
+
anim.addEventListener('cancel', finish);
|
|
128
|
+
} else {
|
|
129
|
+
finish();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default motion;
|