mirror of https://github.com/sveltejs/svelte
parent
ee3ef5bca7
commit
26b3e5171b
@ -0,0 +1,146 @@
|
||||
<script>
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import crossfade from './crossfade.js'; // TODO put this in svelte/transition!
|
||||
|
||||
const { send, receive } = crossfade({
|
||||
fallback(node, params) {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
|
||||
return {
|
||||
duration: 600,
|
||||
easing: quintOut,
|
||||
css: t => `
|
||||
transform: ${transform} scale(${t});
|
||||
opacity: ${t}
|
||||
`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let todos = [
|
||||
{ id: 1, done: false, description: 'write some docs' },
|
||||
{ id: 2, done: false, description: 'start writing JSConf talk' },
|
||||
{ id: 3, done: true, description: 'buy some milk' },
|
||||
{ id: 4, done: false, description: 'mow the lawn' },
|
||||
{ id: 5, done: false, description: 'feed the turtle' },
|
||||
{ id: 6, done: false, description: 'fix some bugs' },
|
||||
];
|
||||
|
||||
let uid = todos.length + 1;
|
||||
|
||||
function add(input) {
|
||||
const todo = {
|
||||
id: uid++,
|
||||
done: false,
|
||||
description: input.value
|
||||
};
|
||||
|
||||
todos = [todo, ...todos];
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function remove(todo) {
|
||||
todos = todos.filter(t => t !== todo);
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.which === 13) {
|
||||
addTodo(event.target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.new-todo {
|
||||
font-size: 1.4em;
|
||||
width: 100%;
|
||||
margin: 2em 0 1em 0;
|
||||
}
|
||||
|
||||
.board {
|
||||
max-width: 36em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.left, .right {
|
||||
float: left;
|
||||
width: 50%;
|
||||
padding: 0 1em 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
font-weight: 200;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
label {
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
padding: 0.5em;
|
||||
margin: 0 auto 0.5em auto;
|
||||
border-radius: 2px;
|
||||
background-color: #eee;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input { margin: 0 }
|
||||
|
||||
.right label {
|
||||
background-color: rgb(180,240,100);
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
height: 1em;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0.5em;
|
||||
line-height: 1;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: rgb(170,30,30);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
label:hover button {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class='board'>
|
||||
<input class="new-todo" placeholder="what needs to be done?" on:enter={add}>
|
||||
|
||||
<div class='left'>
|
||||
<h2>todo</h2>
|
||||
{#each todos.filter(t => !t.done) as todo (todo.id)}
|
||||
<label
|
||||
in:receive="{{key: todo.id}}"
|
||||
out:send="{{key: todo.id}}"
|
||||
>
|
||||
<input type=checkbox bind:checked={todo.done}>
|
||||
{todo.description}
|
||||
<button on:click="{() => remove(todo)}">x</button>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class='right'>
|
||||
<h2>done</h2>
|
||||
{#each todos.filter(t => t.done) as todo (todo.id)}
|
||||
<label
|
||||
in:receive="{{key: todo.id}}"
|
||||
out:send="{{key: todo.id}}"
|
||||
>
|
||||
<input type=checkbox bind:checked={todo.done}>
|
||||
{todo.description}
|
||||
<button on:click="{() => remove(todo)}">x</button>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,65 @@
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
export default function crossfade({ send, receive, fallback }) {
|
||||
let requested = new Map();
|
||||
let provided = new Map();
|
||||
|
||||
function crossfade(from, node) {
|
||||
const to = node.getBoundingClientRect();
|
||||
const dx = from.left - to.left;
|
||||
const dy = from.top - to.top;
|
||||
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
|
||||
return {
|
||||
duration: 400,
|
||||
easing: quintOut,
|
||||
css: (t, u) => `
|
||||
opacity: ${t};
|
||||
transform: ${transform} translate(${u * dx}px,${u * dy}px);
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
send(node, params) {
|
||||
provided.set(params.key, {
|
||||
rect: node.getBoundingClientRect()
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (requested.has(params.key)) {
|
||||
const { rect } = requested.get(params.key);
|
||||
requested.delete(params.key);
|
||||
|
||||
return crossfade(rect, node);
|
||||
}
|
||||
|
||||
// if the node is disappearing altogether
|
||||
// (i.e. wasn't claimed by the other list)
|
||||
// then we need to supply an outro
|
||||
provided.delete(params.key);
|
||||
return fallback(node, params);
|
||||
};
|
||||
},
|
||||
|
||||
receive(node, params) {
|
||||
requested.set(params.key, {
|
||||
rect: node.getBoundingClientRect()
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (provided.has(params.key)) {
|
||||
const { rect } = provided.get(params.key);
|
||||
provided.delete(params.key);
|
||||
|
||||
return crossfade(rect, node);
|
||||
}
|
||||
|
||||
requested.delete(params.key);
|
||||
return fallback(node, params);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
<script>
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import crossfade from './crossfade.js'; // TODO put this in svelte/transition!
|
||||
|
||||
const { send, receive } = crossfade({
|
||||
fallback(node, params) {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
|
||||
return {
|
||||
duration: 600,
|
||||
easing: quintOut,
|
||||
css: t => `
|
||||
transform: ${transform} scale(${t});
|
||||
opacity: ${t}
|
||||
`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let todos = [
|
||||
{ id: 1, done: false, description: 'write some docs' },
|
||||
{ id: 2, done: false, description: 'start writing JSConf talk' },
|
||||
{ id: 3, done: true, description: 'buy some milk' },
|
||||
{ id: 4, done: false, description: 'mow the lawn' },
|
||||
{ id: 5, done: false, description: 'feed the turtle' },
|
||||
{ id: 6, done: false, description: 'fix some bugs' },
|
||||
];
|
||||
|
||||
let uid = todos.length + 1;
|
||||
|
||||
function add(input) {
|
||||
const todo = {
|
||||
id: uid++,
|
||||
done: false,
|
||||
description: input.value
|
||||
};
|
||||
|
||||
todos = [todo, ...todos];
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function remove(todo) {
|
||||
todos = todos.filter(t => t !== todo);
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.which === 13) {
|
||||
addTodo(event.target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.new-todo {
|
||||
font-size: 1.4em;
|
||||
width: 100%;
|
||||
margin: 2em 0 1em 0;
|
||||
}
|
||||
|
||||
.board {
|
||||
max-width: 36em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.left, .right {
|
||||
float: left;
|
||||
width: 50%;
|
||||
padding: 0 1em 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
font-weight: 200;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
label {
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
padding: 0.5em;
|
||||
margin: 0 auto 0.5em auto;
|
||||
border-radius: 2px;
|
||||
background-color: #eee;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input { margin: 0 }
|
||||
|
||||
.right label {
|
||||
background-color: rgb(180,240,100);
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
height: 1em;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0.5em;
|
||||
line-height: 1;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: rgb(170,30,30);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
label:hover button {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class='board'>
|
||||
<input class="new-todo" placeholder="what needs to be done?" on:enter={add}>
|
||||
|
||||
<div class='left'>
|
||||
<h2>todo</h2>
|
||||
{#each todos.filter(t => !t.done) as todo (todo.id)}
|
||||
<label
|
||||
in:receive="{{key: todo.id}}"
|
||||
out:send="{{key: todo.id}}"
|
||||
>
|
||||
<input type=checkbox bind:checked={todo.done}>
|
||||
{todo.description}
|
||||
<button on:click="{() => remove(todo)}">x</button>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class='right'>
|
||||
<h2>done</h2>
|
||||
{#each todos.filter(t => t.done) as todo (todo.id)}
|
||||
<label
|
||||
in:receive="{{key: todo.id}}"
|
||||
out:send="{{key: todo.id}}"
|
||||
>
|
||||
<input type=checkbox bind:checked={todo.done}>
|
||||
{todo.description}
|
||||
<button on:click="{() => remove(todo)}">x</button>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,65 @@
|
||||
import { quintOut } from 'svelte/easing';
|
||||
|
||||
export default function crossfade({ send, receive, fallback }) {
|
||||
let requested = new Map();
|
||||
let provided = new Map();
|
||||
|
||||
function crossfade(from, node) {
|
||||
const to = node.getBoundingClientRect();
|
||||
const dx = from.left - to.left;
|
||||
const dy = from.top - to.top;
|
||||
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
|
||||
return {
|
||||
duration: 400,
|
||||
easing: quintOut,
|
||||
css: (t, u) => `
|
||||
opacity: ${t};
|
||||
transform: ${transform} translate(${u * dx}px,${u * dy}px);
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
send(node, params) {
|
||||
provided.set(params.key, {
|
||||
rect: node.getBoundingClientRect()
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (requested.has(params.key)) {
|
||||
const { rect } = requested.get(params.key);
|
||||
requested.delete(params.key);
|
||||
|
||||
return crossfade(rect, node);
|
||||
}
|
||||
|
||||
// if the node is disappearing altogether
|
||||
// (i.e. wasn't claimed by the other list)
|
||||
// then we need to supply an outro
|
||||
provided.delete(params.key);
|
||||
return fallback(node, params);
|
||||
};
|
||||
},
|
||||
|
||||
receive(node, params) {
|
||||
requested.set(params.key, {
|
||||
rect: node.getBoundingClientRect()
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (provided.has(params.key)) {
|
||||
const { rect } = provided.get(params.key);
|
||||
provided.delete(params.key);
|
||||
|
||||
return crossfade(rect, node);
|
||||
}
|
||||
|
||||
requested.delete(params.key);
|
||||
return fallback(node, params);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: The animate directive
|
||||
---
|
||||
|
||||
TODO fix https://github.com/sveltejs/svelte/issues/2159 before working on `animate`
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "Animations"
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<script>
|
||||
import { spring } from 'svelte/motion';
|
||||
|
||||
const coords = spring({ x: 0, y: 0 }, {
|
||||
stiffness: 0.2,
|
||||
damping: 0.4
|
||||
});
|
||||
|
||||
function handlePanStart() {
|
||||
coords.stiffness = coords.damping = 1;
|
||||
}
|
||||
|
||||
function handlePanMove(event) {
|
||||
coords.update($coords => ({
|
||||
x: $coords.x + event.detail.dx,
|
||||
y: $coords.y + event.detail.dy
|
||||
}));
|
||||
}
|
||||
|
||||
function handlePanEnd(event) {
|
||||
coords.stiffness = 0.2;
|
||||
coords.damping = 0.4;
|
||||
coords.set({ x: 0, y: 0 });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.box {
|
||||
--width: 100px;
|
||||
--height: 100px;
|
||||
position: absolute;
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
left: calc(50% - var(--width) / 2);
|
||||
top: calc(50% - var(--height) / 2);
|
||||
border-radius: 4px;
|
||||
background-color: #ff3e00;
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="box"
|
||||
on:panstart={handlePanStart}
|
||||
on:panmove={handlePanMove}
|
||||
on:panend={handlePanEnd}
|
||||
style="transform: translate({$coords.x}px,{$coords.y}px)"
|
||||
></div>
|
@ -0,0 +1,9 @@
|
||||
export function pannable(node) {
|
||||
// setup work goes here...
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
// ...cleanup goes here
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<script>
|
||||
import { spring } from 'svelte/motion';
|
||||
import { pannable } from './pannable.js';
|
||||
|
||||
const coords = spring({ x: 0, y: 0 }, {
|
||||
stiffness: 0.2,
|
||||
damping: 0.4
|
||||
});
|
||||
|
||||
function handlePanStart() {
|
||||
coords.stiffness = coords.damping = 1;
|
||||
}
|
||||
|
||||
function handlePanMove(event) {
|
||||
coords.update($coords => ({
|
||||
x: $coords.x + event.detail.dx,
|
||||
y: $coords.y + event.detail.dy
|
||||
}));
|
||||
}
|
||||
|
||||
function handlePanEnd(event) {
|
||||
coords.stiffness = 0.2;
|
||||
coords.damping = 0.4;
|
||||
coords.set({ x: 0, y: 0 });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.box {
|
||||
--width: 100px;
|
||||
--height: 100px;
|
||||
position: absolute;
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
left: calc(50% - var(--width) / 2);
|
||||
top: calc(50% - var(--height) / 2);
|
||||
border-radius: 4px;
|
||||
background-color: #ff3e00;
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="box"
|
||||
use:pannable
|
||||
on:panstart={handlePanStart}
|
||||
on:panmove={handlePanMove}
|
||||
on:panend={handlePanEnd}
|
||||
style="transform: translate({$coords.x}px,{$coords.y}px)"
|
||||
></div>
|
@ -0,0 +1,47 @@
|
||||
export function pannable(node) {
|
||||
let x;
|
||||
let y;
|
||||
|
||||
function handleMousedown(event) {
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
|
||||
node.dispatchEvent(new CustomEvent('panstart', {
|
||||
detail: { x, y }
|
||||
}));
|
||||
|
||||
window.addEventListener('mousemove', handleMousemove);
|
||||
window.addEventListener('mouseup', handleMouseup);
|
||||
}
|
||||
|
||||
function handleMousemove(event) {
|
||||
const dx = event.clientX - x;
|
||||
const dy = event.clientY - y;
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
|
||||
node.dispatchEvent(new CustomEvent('panmove', {
|
||||
detail: { x, y, dx, dy }
|
||||
}));
|
||||
}
|
||||
|
||||
function handleMouseup(event) {
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
|
||||
node.dispatchEvent(new CustomEvent('panend', {
|
||||
detail: { x, y }
|
||||
}));
|
||||
|
||||
window.removeEventListener('mousemove', handleMousemove);
|
||||
window.removeEventListener('mouseup', handleMouseup);
|
||||
}
|
||||
|
||||
node.addEventListener('mousedown', handleMousedown);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('mousedown', handleMousedown);
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
---
|
||||
title: The use directive
|
||||
---
|
||||
|
||||
Actions are essentially element-level lifecycle functions. They're useful for things like:
|
||||
|
||||
* interfacing with third-party libraries
|
||||
* lazy-loaded images
|
||||
* tooltips
|
||||
* adding custom event handlers
|
||||
|
||||
In this app, we want to make the orange box 'pannable'. It has event handlers for the `panstart`, `panmove` and `panend` events, but these aren't native DOM events. We have to dispatch them ourselves. First, import the `pannable` function...
|
||||
|
||||
```js
|
||||
import { pannable } from './pannable.js';
|
||||
```
|
||||
|
||||
...then use it with the element:
|
||||
|
||||
```html
|
||||
<div class="box"
|
||||
use:pannable
|
||||
on:panstart={handlePanStart}
|
||||
on:panmove={handlePanMove}
|
||||
on:panend={handlePanEnd}
|
||||
style="transform: translate({$coords.x}px,{$coords.y}px)"
|
||||
></div>
|
||||
```
|
||||
|
||||
Open the `pannable.js` file. Like transition functions, an action function receives a `node` and some optional parameters, and returns an action object. That object must have a `destroy` function, which is called when the element is unmounted.
|
||||
|
||||
We want to fire `panstart` event when the user mouses down on the element, `panmove` events (with `dx` and `dy` properties showing how far the mouse moved) when they drag it, and `panend` events when they mouse up. One possible implementation looks like this:
|
||||
|
||||
```js
|
||||
export function pannable(node) {
|
||||
let x;
|
||||
let y;
|
||||
|
||||
function handleMousedown(event) {
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
|
||||
node.dispatchEvent(new CustomEvent('panstart', {
|
||||
detail: { x, y }
|
||||
}));
|
||||
|
||||
window.addEventListener('mousemove', handleMousemove);
|
||||
window.addEventListener('mouseup', handleMouseup);
|
||||
}
|
||||
|
||||
function handleMousemove(event) {
|
||||
const dx = event.clientX - x;
|
||||
const dy = event.clientY - y;
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
|
||||
node.dispatchEvent(new CustomEvent('panmove', {
|
||||
detail: { x, y, dx, dy }
|
||||
}));
|
||||
}
|
||||
|
||||
function handleMouseup(event) {
|
||||
x = event.clientX;
|
||||
y = event.clientY;
|
||||
|
||||
node.dispatchEvent(new CustomEvent('panend', {
|
||||
detail: { x, y }
|
||||
}));
|
||||
|
||||
window.removeEventListener('mousemove', handleMousemove);
|
||||
window.removeEventListener('mouseup', handleMouseup);
|
||||
}
|
||||
|
||||
node.addEventListener('mousedown', handleMousedown);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('mousedown', handleMousedown);
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Update the `pannable` function and try moving the box around.
|
||||
|
||||
> This implementation is for demonstration purposes — a more complete one would also consider touch events.
|
||||
|
@ -0,0 +1 @@
|
||||
TODO
|
@ -0,0 +1 @@
|
||||
TODO
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Adding parameters
|
||||
---
|
||||
|
||||
TODO example with Prism highlighting
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "Actions"
|
||||
}
|
Loading…
Reference in new issue